mirror of https://github.com/garrytan/gstack.git
Merge 3b7d791e7d into 2beb636f7c
This commit is contained in:
commit
cce8e1d2a3
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PLATFORM="${1:-$(uname -s)}"
|
||||
case "$PLATFORM" in
|
||||
MINGW*|MSYS*|CYGWIN*|Windows_NT) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
ROOT="${2:-$(cd "$(dirname "$0")/../.." && pwd -P)}"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
ROOT="$(cygpath -u "$ROOT" 2>/dev/null || printf '%s' "$ROOT")"
|
||||
fi
|
||||
ROOT="$(cd "$ROOT" && pwd -P)"
|
||||
|
||||
BUN_CMD="${BUN_CMD:-bun}"
|
||||
SOURCE="$ROOT/browse/bin/windows-browse"
|
||||
DESTINATION="$ROOT/browse/dist/browse"
|
||||
|
||||
if [ ! -f "$SOURCE" ]; then
|
||||
echo "ERROR: Windows browse launcher source is missing: $SOURCE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$DESTINATION")"
|
||||
|
||||
# MSYS cp resolves an extensionless destination to an existing .exe and can
|
||||
# overwrite browse.exe. Bun writes the exact extensionless path.
|
||||
GSTACK_LAUNCHER_SOURCE="$SOURCE" \
|
||||
GSTACK_LAUNCHER_DESTINATION="$DESTINATION" \
|
||||
"$BUN_CMD" -e 'await Bun.write(process.env.GSTACK_LAUNCHER_DESTINATION, Bun.file(process.env.GSTACK_LAUNCHER_SOURCE))'
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
is_complete_gstack_source() {
|
||||
local root="$1"
|
||||
[ -f "$root/package.json" ] &&
|
||||
[ -f "$root/browse/src/cli.ts" ] &&
|
||||
[ -f "$root/browse/dist/server-node.mjs" ] &&
|
||||
[ -f "$root/node_modules/playwright/package.json" ] &&
|
||||
[ -f "$root/node_modules/diff/package.json" ]
|
||||
}
|
||||
|
||||
normalize_candidate() {
|
||||
local candidate="$1"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
candidate="$(cygpath -u "$candidate" 2>/dev/null || printf '%s' "$candidate")"
|
||||
fi
|
||||
(cd "$candidate" 2>/dev/null && pwd -P) || true
|
||||
}
|
||||
|
||||
SOURCE_ROOT=""
|
||||
if [ -n "${GSTACK_SOURCE_ROOT:-}" ]; then
|
||||
SOURCE_ROOT="$(normalize_candidate "$GSTACK_SOURCE_ROOT")"
|
||||
if [ -z "$SOURCE_ROOT" ] || ! is_complete_gstack_source "$SOURCE_ROOT"; then
|
||||
echo "ERROR: GSTACK_SOURCE_ROOT does not contain a complete gstack source install: $GSTACK_SOURCE_ROOT" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/../.." \
|
||||
"$HOME/.gstack/repos/gstack" \
|
||||
"$HOME/.claude/skills/gstack" \
|
||||
"$HOME/.codex/skills/gstack"; do
|
||||
resolved="$(normalize_candidate "$candidate")"
|
||||
if [ -n "$resolved" ] && is_complete_gstack_source "$resolved"; then
|
||||
SOURCE_ROOT="$resolved"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$SOURCE_ROOT" ]; then
|
||||
echo "ERROR: no complete gstack source install was found." >&2
|
||||
echo "Expected browse/src/cli.ts, browse/dist/server-node.mjs, and installed dependencies." >&2
|
||||
echo "Set GSTACK_SOURCE_ROOT to the full gstack checkout and retry." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo "ERROR: Bun is required for the Windows gstack browser launcher." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "ERROR: Node.js is required for the Windows gstack browser server." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
cd "$SOURCE_ROOT"
|
||||
exec bun run "$SOURCE_ROOT/browse/src/cli.ts" "$@"
|
||||
|
|
@ -30,6 +30,10 @@ esac
|
|||
"$BUN_CMD" build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf
|
||||
"$BUN_CMD" build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover
|
||||
bash browse/scripts/build-node-server.sh
|
||||
# Smart App Control can block the locally compiled unsigned browse.exe.
|
||||
# Browser skills use an extensionless source launcher through the installed Bun
|
||||
# runtime while the native binary remains available for signed release workflows.
|
||||
BUN_CMD="$BUN_CMD" bash browse/bin/stage-windows-browse-launcher "$(uname -s)" "$ROOT"
|
||||
bash scripts/write-version-files.sh browse/dist/.version design/dist/.version make-pdf/dist/.version
|
||||
chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover
|
||||
rm -f .*.bun-build
|
||||
|
|
|
|||
2
setup
2
setup
|
|
@ -380,6 +380,8 @@ if [ ! -x "$BROWSE_BIN" ]; then
|
|||
NEEDS_BUILD=1
|
||||
elif [ -n "$(find "$SOURCE_GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then
|
||||
NEEDS_BUILD=1
|
||||
elif [ "$IS_WINDOWS" -eq 1 ] && { [ "$SOURCE_GSTACK_DIR/browse/bin/windows-browse" -nt "$BROWSE_BIN" ] || [ "$SOURCE_GSTACK_DIR/browse/bin/stage-windows-browse-launcher" -nt "$BROWSE_BIN" ]; }; then
|
||||
NEEDS_BUILD=1
|
||||
elif [ "$SOURCE_GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then
|
||||
NEEDS_BUILD=1
|
||||
elif [ -f "$SOURCE_GSTACK_DIR/bun.lock" ] && [ "$SOURCE_GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { basename, join, resolve } from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..');
|
||||
const POSIX_LAUNCHER = join(ROOT, 'browse', 'bin', 'windows-browse');
|
||||
const STAGE_LAUNCHER = join(ROOT, 'browse', 'bin', 'stage-windows-browse-launcher');
|
||||
const BUILD_SCRIPT = readFileSync(join(ROOT, 'scripts', 'build.sh'), 'utf8');
|
||||
const SETUP_SCRIPT = readFileSync(join(ROOT, 'setup'), 'utf8');
|
||||
const BASH = process.platform === 'win32'
|
||||
? join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe')
|
||||
: 'bash';
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeCompleteSource(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'gstack-appcontrol-source-'));
|
||||
tempRoots.push(root);
|
||||
for (const relative of [
|
||||
'browse/src',
|
||||
'browse/dist',
|
||||
'node_modules/playwright',
|
||||
'node_modules/diff',
|
||||
]) {
|
||||
mkdirSync(join(root, relative), { recursive: true });
|
||||
}
|
||||
writeFileSync(join(root, 'package.json'), '{}\n');
|
||||
writeFileSync(
|
||||
join(root, 'browse/src/cli.ts'),
|
||||
'console.log(`cwd=${process.cwd().replace(/\\\\/g, "/")}`);\n' +
|
||||
'for (const arg of Bun.argv.slice(2)) console.log(`arg=${arg}`);\n',
|
||||
);
|
||||
writeFileSync(join(root, 'browse/dist/server-node.mjs'), '// fixture\n');
|
||||
writeFileSync(join(root, 'node_modules/playwright/package.json'), '{}\n');
|
||||
writeFileSync(join(root, 'node_modules/diff/package.json'), '{}\n');
|
||||
return root;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Windows Smart App Control browse launcher', () => {
|
||||
test('source launcher discovers complete installs without a hardcoded user path', () => {
|
||||
const posix = readFileSync(POSIX_LAUNCHER, 'utf8');
|
||||
|
||||
expect(posix).toContain('GSTACK_SOURCE_ROOT');
|
||||
expect(posix).toContain('browse/src/cli.ts');
|
||||
expect(posix).toContain('browse/dist/server-node.mjs');
|
||||
expect(posix).toContain('node_modules');
|
||||
expect(posix).not.toMatch(/Users[\\/]Administrator|Users[\\/]garry/i);
|
||||
expect(posix).toContain('$HOME/.gstack/repos/gstack');
|
||||
expect(posix).toContain('$HOME/.claude/skills/gstack');
|
||||
});
|
||||
|
||||
test('POSIX launcher forwards arguments through Bun from the discovered source root', () => {
|
||||
const sourceRoot = makeCompleteSource();
|
||||
if (process.platform === 'win32') expect(existsSync(BASH)).toBe(true);
|
||||
|
||||
const result = spawnSync(BASH, ['browse/bin/windows-browse', 'goto', 'http://127.0.0.1:4322/'], {
|
||||
encoding: 'utf8',
|
||||
cwd: ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
GSTACK_SOURCE_ROOT: sourceRoot,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
const cwdLine = result.stdout.split(/\r?\n/).find(line => line.startsWith('cwd='));
|
||||
expect(cwdLine?.replace(/\\/g, '/').endsWith(`/${basename(sourceRoot)}`)).toBe(true);
|
||||
expect(result.stdout).toContain('arg=goto');
|
||||
expect(result.stdout).toContain('arg=http://127.0.0.1:4322/');
|
||||
});
|
||||
|
||||
test('explicit incomplete source roots fail closed', () => {
|
||||
const incomplete = mkdtempSync(join(tmpdir(), 'gstack-appcontrol-incomplete-'));
|
||||
tempRoots.push(incomplete);
|
||||
const result = spawnSync(BASH, ['browse/bin/windows-browse', 'status'], {
|
||||
encoding: 'utf8',
|
||||
cwd: ROOT,
|
||||
env: { ...process.env, GSTACK_SOURCE_ROOT: incomplete },
|
||||
});
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stderr).toContain('does not contain a complete gstack source install');
|
||||
});
|
||||
|
||||
test('stager no-ops off Windows and preserves browse.exe while installing the exact launcher', () => {
|
||||
const stageRoot = mkdtempSync(join(tmpdir(), 'gstack-appcontrol-stage-'));
|
||||
tempRoots.push(stageRoot);
|
||||
mkdirSync(join(stageRoot, 'browse/bin'), { recursive: true });
|
||||
mkdirSync(join(stageRoot, 'browse/dist'), { recursive: true });
|
||||
writeFileSync(join(stageRoot, 'browse/bin/windows-browse'), '#!/usr/bin/env bash\necho staged\n');
|
||||
writeFileSync(join(stageRoot, 'browse/dist/browse.exe'), 'native sentinel\n');
|
||||
const shellRoot = stageRoot.replace(/\\/g, '/');
|
||||
|
||||
const nonWindows = spawnSync(BASH, ['browse/bin/stage-windows-browse-launcher', 'Linux', shellRoot], {
|
||||
encoding: 'utf8',
|
||||
cwd: ROOT,
|
||||
});
|
||||
expect(nonWindows.status).toBe(0);
|
||||
expect(existsSync(join(stageRoot, 'browse/dist/browse'))).toBe(false);
|
||||
|
||||
const windows = spawnSync(BASH, ['browse/bin/stage-windows-browse-launcher', 'MINGW64_NT-10.0', shellRoot], {
|
||||
encoding: 'utf8',
|
||||
cwd: ROOT,
|
||||
});
|
||||
expect(windows.status).toBe(0);
|
||||
expect(readFileSync(join(stageRoot, 'browse/dist/browse'), 'utf8')).toBe(
|
||||
readFileSync(join(stageRoot, 'browse/bin/windows-browse'), 'utf8'),
|
||||
);
|
||||
expect(readFileSync(join(stageRoot, 'browse/dist/browse.exe'), 'utf8')).toBe('native sentinel\n');
|
||||
});
|
||||
|
||||
test('Windows build stages the extensionless launcher after compiling', () => {
|
||||
expect(BUILD_SCRIPT).toContain('bash browse/bin/stage-windows-browse-launcher');
|
||||
expect(BUILD_SCRIPT.indexOf('build --compile browse/src/cli.ts')).toBeLessThan(
|
||||
BUILD_SCRIPT.indexOf('bash browse/bin/stage-windows-browse-launcher'),
|
||||
);
|
||||
});
|
||||
|
||||
test('setup rebuilds when either Windows launcher file changes', () => {
|
||||
expect(SETUP_SCRIPT).toContain('browse/bin/windows-browse" -nt "$BROWSE_BIN');
|
||||
expect(SETUP_SCRIPT).toContain('browse/bin/stage-windows-browse-launcher" -nt "$BROWSE_BIN');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue