mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-20 20:00:45 +02:00
fix: harden browser provider activation
This commit is contained in:
@@ -70,11 +70,11 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/inst
|
||||
RUN npm i -g @anthropic-ai/claude-code
|
||||
|
||||
# Playwright system deps (Chromium) — needed for browse E2E tests
|
||||
RUN npx playwright install-deps chromium
|
||||
RUN npx playwright-core install-deps chromium
|
||||
|
||||
# Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back
|
||||
# to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't
|
||||
# render in DejaVu Sans. playwright install-deps happens to pull this in today,
|
||||
# render in DejaVu Sans. playwright-core install-deps happens to pull this in today,
|
||||
# but the dep is implicit and could change — install explicitly so upgrades
|
||||
# can't silently regress rendering.
|
||||
#
|
||||
@@ -100,12 +100,12 @@ RUN bun install --frozen-lockfile && rm -rf /tmp/*
|
||||
|
||||
# Install Playwright Chromium to a shared location accessible by all users
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
|
||||
RUN npx playwright install chromium \
|
||||
RUN npx playwright-core install chromium \
|
||||
&& chmod -R a+rX /opt/playwright-browsers
|
||||
|
||||
# Verify everything works
|
||||
RUN bun --version && node --version && claude --version && jq --version && gh --version \
|
||||
&& npx playwright --version \
|
||||
&& npx playwright-core --version \
|
||||
&& fc-match "Liberation Sans" | grep -qi "Liberation" \
|
||||
|| (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: bunx playwright install chromium
|
||||
run: bunx playwright-core install chromium
|
||||
|
||||
- name: Build binaries
|
||||
run: bun run build
|
||||
|
||||
@@ -382,6 +382,7 @@ export class BrowserManager {
|
||||
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
||||
// Extensions only work in headed mode, so we use an off-screen window.
|
||||
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
||||
if (extensionsDir) assertHeadedBrowserProvider();
|
||||
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
|
||||
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
|
||||
let useHeadless = true;
|
||||
@@ -1587,6 +1588,7 @@ export class BrowserManager {
|
||||
* If step 2 fails → return error, headless browser untouched
|
||||
*/
|
||||
async handoff(message: string): Promise<string> {
|
||||
assertHeadedBrowserProvider();
|
||||
if (this.connectionMode === 'headed' || this.isHeaded) {
|
||||
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
|
||||
}
|
||||
|
||||
+10
-5
@@ -118,7 +118,7 @@ interface ServerState {
|
||||
serverPath: string;
|
||||
binaryVersion?: string;
|
||||
mode?: 'launched' | 'headed';
|
||||
/** Hash of (proxyUrl + headed flag), used by D2 daemon-mismatch check. */
|
||||
/** Hash of proxy, headed mode, and browser-provider intent, used by daemon-mismatch checks. */
|
||||
configHash?: string;
|
||||
/** Xvfb child PID for cleanup on disconnect. */
|
||||
xvfbPid?: number;
|
||||
@@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
|
||||
// hint. No silent restart — that would drop tab state, cookies, and
|
||||
// logged-in sessions without warning.
|
||||
if (desiredHash && state.configHash && state.configHash !== desiredHash) {
|
||||
console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`);
|
||||
console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`);
|
||||
console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`);
|
||||
console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Same path: existing daemon is plain (no flags) but caller passes
|
||||
@@ -782,7 +782,7 @@ export interface GlobalFlags {
|
||||
proxyUrl: string | null;
|
||||
/** Whether --headed was passed. */
|
||||
headed: boolean;
|
||||
/** Hash of (proxy + headed) for daemon-mismatch check. */
|
||||
/** Hash of proxy, headed mode, and browser-provider intent for daemon-mismatch checks. */
|
||||
configHash: string;
|
||||
/** Redacted form of proxyUrl, safe for logs. */
|
||||
redactedProxyUrl: string;
|
||||
@@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G
|
||||
args: out,
|
||||
proxyUrl: canonicalProxyUrl,
|
||||
headed,
|
||||
configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }),
|
||||
configHash: computeConfigHash({
|
||||
proxyUrl: canonicalProxyUrl,
|
||||
headed,
|
||||
browserProvider: env.GSTACK_BROWSER_PROVIDER,
|
||||
browserExecutable: env.GSTACK_CHROMIUM_PATH,
|
||||
}),
|
||||
redactedProxyUrl: redactProxyUrl(canonicalProxyUrl),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a stable hash of (proxyUrl + headed flag) for daemon-mismatch
|
||||
* Compute a stable hash of proxy, headed mode, and browser-provider intent for daemon-mismatch
|
||||
* detection (D2). The hash is deterministic across CLI invocations on the
|
||||
* same machine and survives daemon restarts via the state file.
|
||||
*
|
||||
@@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
|
||||
export function computeConfigHash(opts: {
|
||||
proxyUrl: string | null | undefined;
|
||||
headed: boolean;
|
||||
browserProvider?: string | null;
|
||||
browserExecutable?: string | null;
|
||||
}): string {
|
||||
const proxyKey = canonicalizeProxyUrl(opts.proxyUrl);
|
||||
const input = JSON.stringify({ proxy: proxyKey, headed: opts.headed });
|
||||
const browserProvider = opts.browserProvider || null;
|
||||
const browserExecutable = browserProvider === "installed" ? opts.browserExecutable || null : null;
|
||||
const input = JSON.stringify({
|
||||
proxy: proxyKey,
|
||||
headed: opts.headed,
|
||||
browserProvider,
|
||||
browserExecutable,
|
||||
});
|
||||
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
|
||||
@@ -355,11 +355,18 @@ export async function handleWriteCommand(
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
|
||||
const isOption = 'locator' in resolved
|
||||
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
|
||||
: await target.locator(resolved.selector).evaluate(
|
||||
el => el.tagName === 'OPTION'
|
||||
).catch(() => false);
|
||||
// Do not start a second auto-wait after the click has already timed out.
|
||||
// Missing selectors used to spend 5s in click(), then block again in
|
||||
// evaluate() until the outer client killed the command. count() is an
|
||||
// immediate query and keeps the helpful option guidance only when one
|
||||
// unique element actually exists.
|
||||
const optionLocator = 'locator' in resolved
|
||||
? resolved.locator
|
||||
: target.locator(resolved.selector);
|
||||
const optionCount = await optionLocator.count().catch(() => 0);
|
||||
const isOption = optionCount === 1
|
||||
? await optionLocator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
|
||||
: false;
|
||||
if (isOption) {
|
||||
throw new Error(
|
||||
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
|
||||
|
||||
@@ -478,6 +478,20 @@ describe('Interaction', () => {
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
test('click on a missing selector does not start a second locator wait', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const started = performance.now();
|
||||
try {
|
||||
await handleWriteCommand('click', ['#definitely-missing-regression-node'], bm);
|
||||
expect(true).toBe(false); // Should not reach here
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('#definitely-missing-regression-node');
|
||||
}
|
||||
// click() intentionally retains Playwright's 5s auto-wait. The regression
|
||||
// was a second default locator wait that pushed the total beyond 8s.
|
||||
expect(performance.now() - started).toBeLessThan(6500);
|
||||
}, 8000);
|
||||
|
||||
test('hover works', async () => {
|
||||
const result = await handleWriteCommand('hover', ['h1'], bm);
|
||||
expect(result).toContain('Hovered');
|
||||
|
||||
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
test('refuses to reuse a same-version daemon from a different browser provider', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-provider-mismatch-'));
|
||||
const stateFile = path.join(tmpDir, 'browse.json');
|
||||
const fakeServer = await startFakeHealthServer('fake-token');
|
||||
const { computeConfigHash } = await import('../src/proxy-config');
|
||||
const managedHash = computeConfigHash({
|
||||
proxyUrl: null,
|
||||
headed: false,
|
||||
browserProvider: 'managed',
|
||||
});
|
||||
|
||||
fs.writeFileSync(stateFile, JSON.stringify({
|
||||
pid: process.pid,
|
||||
port: fakeServer.port,
|
||||
token: 'fake-token',
|
||||
startedAt: new Date().toISOString(),
|
||||
serverPath: '',
|
||||
mode: 'launched',
|
||||
configHash: managedHash,
|
||||
}, null, 2));
|
||||
|
||||
const cliEnv: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined) cliEnv[key] = value;
|
||||
}
|
||||
cliEnv.BROWSE_STATE_FILE = stateFile;
|
||||
cliEnv.GSTACK_BROWSER_PROVIDER = 'installed';
|
||||
cliEnv.GSTACK_CHROMIUM_PATH = process.execPath;
|
||||
|
||||
try {
|
||||
const result = await runCli(['status'], cliEnv);
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('different config');
|
||||
expect(result.stderr).toContain('browse disconnect');
|
||||
} finally {
|
||||
await fakeServer.close();
|
||||
try { fs.unlinkSync(stateFile); } catch { /* ignore */ }
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
test('refuses when existing plain daemon meets a --proxy invocation', async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
|
||||
const stateFile = path.join(tmpDir, 'browse.json');
|
||||
|
||||
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
|
||||
);
|
||||
expect(a.configHash).not.toBe(b.configHash);
|
||||
});
|
||||
|
||||
test('configHash changes with browser provider and installed executable', () => {
|
||||
const managed = extractGlobalFlags(['goto', 'x'], {
|
||||
GSTACK_BROWSER_PROVIDER: 'managed',
|
||||
} as NodeJS.ProcessEnv);
|
||||
const installedA = extractGlobalFlags(['goto', 'x'], {
|
||||
GSTACK_BROWSER_PROVIDER: 'installed',
|
||||
GSTACK_CHROMIUM_PATH: '/browser/a',
|
||||
} as NodeJS.ProcessEnv);
|
||||
const installedB = extractGlobalFlags(['goto', 'x'], {
|
||||
GSTACK_BROWSER_PROVIDER: 'installed',
|
||||
GSTACK_CHROMIUM_PATH: '/browser/b',
|
||||
} as NodeJS.ProcessEnv);
|
||||
expect(managed.configHash).not.toBe(installedA.configHash);
|
||||
expect(installedA.configHash).not.toBe(installedB.configHash);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1416,7 +1416,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1591,7 +1591,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1820,7 +1820,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2162,7 +2162,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2288,7 +2288,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2466,7 +2466,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2565,7 +2565,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2631,7 +2631,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2710,7 +2710,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2798,7 +2798,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2873,7 +2873,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2934,7 +2934,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4136,7 +4136,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -5775,7 +5775,7 @@
|
||||
"owner_tree": "qa",
|
||||
"consumer_tree": "ship",
|
||||
"target": "skills/ship/references/legacy/canary.md",
|
||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
||||
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||
"disposition": "SHARED_MODULE"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ manifest contains only official GitHub Release URLs and the fixed workflow
|
||||
certificate identity.
|
||||
|
||||
Browser-capable archives include the Playwright-managed Chromium directory at
|
||||
`.gstack-runtime-browsers`. The builder runs `playwright install chromium`
|
||||
`.gstack-runtime-browsers`. The builder runs `playwright-core install chromium`
|
||||
only—never `--with-deps` or `sudo`—copies physical files into the immutable
|
||||
slot, and launch-smokes that exact Chromium on every native release runner.
|
||||
The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active
|
||||
|
||||
+16
-4
@@ -57,13 +57,13 @@ PR, or PR-ready claim is authorized by this status.
|
||||
three retained Claude Haiku live samples are classified `REGRESSION`; they
|
||||
are preserved as noisy supplemental evidence, never cherry-picked as a
|
||||
primary gate or represented as green.
|
||||
- [x] The current macOS GStack 2 suite is green: 151 pass / 0 fail and 1,194
|
||||
assertions across 16 files.
|
||||
- [x] The current macOS GStack 2 suite is green: 218 pass / 0 fail and 2,229
|
||||
assertions across 20 files.
|
||||
- [x] Optional host-neutral runtime implemented with canonical paths,
|
||||
repo/worktree state identity, locks, atomic writes, effect claims,
|
||||
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
|
||||
- [x] Managed runtime installer coverage is green at 25 pass / 0 fail and 341
|
||||
assertions. The deterministic clean macOS arm64 managed-bundle audit records
|
||||
- [x] Managed runtime installer coverage is green at 34 pass / 0 fail. The
|
||||
deterministic clean macOS arm64 managed-bundle audit records
|
||||
110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers.
|
||||
This is a platform-specific bundle measurement, not a universal byte count;
|
||||
platform-native package payloads differ. Setup installs frozen
|
||||
@@ -75,6 +75,18 @@ PR, or PR-ready claim is authorized by this status.
|
||||
production-only install with the development SDK absent, completed a local
|
||||
browser journey and Sharp full-page screenshot, and uninstalled while
|
||||
preserving state.
|
||||
- [x] Browser-backed setup now fails closed until the user explicitly chooses
|
||||
GStack-managed Chromium or one detected installed Chromium executable. The
|
||||
local-only options step performs no network or state mutation; the signed
|
||||
preview reports exact incremental bytes before a separate install approval;
|
||||
only a successful install persists the host-neutral choice. Installed-browser
|
||||
mode keeps the Playwright adapter while omitting managed Chromium payloads.
|
||||
Visible extension-bearing GStack Browser remains managed-only. Focused setup
|
||||
UX coverage is green at 22 pass / 0 fail, including no-network refusal,
|
||||
provider-aware component planning, launcher propagation, and doctor launch.
|
||||
The official live bootstrap remains pending until the corresponding signed
|
||||
`v2.0.0-rc.6` component release is published; deterministic local evidence
|
||||
does not relabel that production gate as passed.
|
||||
- [x] The current candidate additionally captures a runtime-owned Bun 1.3.14
|
||||
executable under `.gstack-runtime-tools`, records its path/version in the
|
||||
bundle manifest, vendors the tagged license/source notices, and routes the
|
||||
|
||||
@@ -10,7 +10,7 @@ pass from deterministic, offline, or filesystem-only evidence.
|
||||
| Command / probe | Observed result | What it proves / does not prove |
|
||||
|---|---|---|
|
||||
| Earlier focused macOS `bun test test/gstack2-*.test.ts` candidate run | **Exit 0: 136 pass / 0 fail**, 1,128 assertions across 15 files. Log: `/tmp/gstack2-test-command-candidate-final2.log`. | Historical focused checkpoint retained rather than overwritten. |
|
||||
| Current macOS `bun run test:gstack2` | **Exit 0: 151 pass / 0 fail**, 1,194 assertions across 16 files. | Current generated freshness, routing, runtime, privacy, installer, upgrade, parity, and adversarial-harness surface is green. Native and broad evidence are recorded separately; this working-tree result is not attributed to the earlier native-CI commit. |
|
||||
| Current macOS direct `bun test --timeout 60000 test/gstack2-*.test.ts` | **Exit 0: 218 pass / 0 fail**, 2,229 assertions across 20 files. | Current routing, runtime, privacy, installer, upgrade, parity, browser-provider setup, and adversarial-harness surface is green. The `test:gstack2` wrapper's generated-cleanliness precheck remains a commit-time gate, so this row records the direct underlying suite rather than relabeling that wrapper. |
|
||||
| `bun test --timeout 30000 test/gstack2-skills.test.ts test/gstack2-skills-routing.test.ts` after regeneration | **Exit 0: 3 pass / 0 fail**, 81 assertions. | The pinned corpus/parity test and both 25-scenario structured-routing tests are green. This remains structural/fixture evidence, not specialist live execution. |
|
||||
| `bun run scripts/gstack2/run-parity.ts`, 2026-07-17 rerun | **Exit 0: 4,681 checks passed**; 55 modules, 16 sections, 25 scenarios, 16 regressions, 78 assets. | Current source/render/provenance/contract/asset/fixture parity is green. It is deterministic parity, not live-host behavior. |
|
||||
| Earlier regenerated structural parity checkpoint | **Exit 0: 2,403 checks passed** with the then-current 55/16/25/16/45 inventory. | Historical candidate checkpoint before later thin-prelude and asset coverage; superseded by the current 4,681-check rerun. |
|
||||
@@ -30,7 +30,8 @@ pass from deterministic, offline, or filesystem-only evidence.
|
||||
| `gstack context smoke --url https://www.context.dev --json` | **PASS:** the official `/web/scrape/markdown` endpoint returned `ok: true` and credit metadata. | The verified key entered through protected stdin/environment worked. The temporary secrets file was mode `0600`, the isolated home was removed, and the safe output did not contain the key. Committed redacted artifact: [`evals/context-dev/live-smoke-2026-07-17.json`](../../evals/context-dev/live-smoke-2026-07-17.json). |
|
||||
| Standard installer matrix | **PASS: 510/510 checks**, 18 install cases, two removal cases, `skills` CLI 1.5.19. | Project/global installs pass for seven hosts, including Kimi Code CLI through the standard `.agents/skills` path; selected-skill and opt-in compatibility-alias cases, copies, and hashes pass. This remains installer/filesystem evidence. Committed artifact: [`evals/installation/install-matrix.json`](../../evals/installation/install-matrix.json). |
|
||||
| Standard Agent Skills install + actual runtime-absent Codex invocation | **PASS.** Root `--list` returned exactly six. The selected source was `time-attack/gstack/skills --skill qa`; despite `skills` 1.5.19's pre-filter display counting hidden aliases, exactly one skill (`qa`) installed byte-identically. Codex exited 0 with `gstack` absent, reported `NEEDS_SETUP` and one approval prompt, changed no files, and created no runtime/browser/external-service activity. | Closes the actual-host runtime-absent judgment gate for Codex without overstating seven-host UI coverage. Artifact: [`standard-codex-runtime-absent-2026-07-17.json`](../../evals/installation/standard-codex-runtime-absent-2026-07-17.json). |
|
||||
| `bun test test/gstack2-runtime-install.test.ts` | **Exit 0: 25 pass / 0 fail**, 341 assertions. | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, rollback/recovery, stable launchers, wrapper neutrality, and state-preserving uninstall pass. |
|
||||
| `bun test --timeout 30000 test/gstack2-runtime-install.test.ts` | **Exit 0: 41 pass / 0 fail.** | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, atomic browser-choice rollback/recovery, provider-aware stable launchers, wrapper neutrality, and state-preserving uninstall pass. |
|
||||
| `bun test --timeout 30000 test/gstack2-runtime-setup-ux.test.ts` | **Exit 0: 29 pass / 0 fail.** | Explicit managed/installed/later browser selection fails closed before network, local options do not mutate, signed previews remain provider-identical across same- and cross-release capability additions, developer-source provider switches replace the exact retained set, visible extension Chromium remains managed-only, and launcher/doctor paths honor the persisted selection. This is deterministic local evidence, not native Windows or every installed Chrome-family build. |
|
||||
| Deterministic clean macOS arm64 managed runtime bundle audit | **110 components, 1,829 files, 450,044,315 bytes, 50 capability launchers.** | This is a platform-specific bundle measurement, not a universal byte count; platform-native package payloads differ. Setup includes the Sharp/ngrok closure and excludes the development-only Claude Agent SDK. The Hugging Face sidecar is outside the bundle and its package is development-only, so production setup installs neither its inference runtime nor model weights; the L4 capability reports unavailable. The standard skill installer remains Markdown-only. Committed artifact: [`evals/runtime-bundle/darwin-arm64.json`](../../evals/runtime-bundle/darwin-arm64.json); reproduce with `bun run scripts/gstack2/audit-runtime-bundle.ts --output evals/runtime-bundle/darwin-arm64.json`. |
|
||||
| Earlier declared Linux Dev Container plus `bun run test:gstack2` inside it | **Exit 0: 136 pass / 0 fail, 1,127 assertions across 15 files.** Log: `/tmp/gstack2-devcontainer-gate-candidate-final4.log`. | Historical container checkpoint retained rather than overwritten; the current 150-test container result is recorded in the native-CI row below. |
|
||||
| `scripts/gstack2/runtime-install-smoke.sh` in the clean Linux arm64 container | **Pass:** production-only frozen dependencies installed with the development Agent SDK and Hugging Face/ONNX runtime absent; the managed Anthropic SDK, Sharp, and ngrok imports passed; prebuilt capabilities rebuilt; setup/doctor/version/design/PDF passed; a local-browser journey and Sharp full-page screenshot passed; uninstall preserved state. | Proves a source copy with spaces can build and complete the managed runtime lifecycle without Git history, an executable local-model stack, or Darwin-only iOS artifacts. It is not native Windows evidence. |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
+15
-15
@@ -83,7 +83,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1416,7 +1416,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1591,7 +1591,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1820,7 +1820,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2162,7 +2162,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2288,7 +2288,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2466,7 +2466,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2565,7 +2565,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2631,7 +2631,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2710,7 +2710,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2798,7 +2798,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2873,7 +2873,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2934,7 +2934,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4136,7 +4136,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -5775,7 +5775,7 @@
|
||||
"owner_tree": "qa",
|
||||
"consumer_tree": "ship",
|
||||
"target": "skills/ship/references/legacy/canary.md",
|
||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
||||
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||
"disposition": "SHARED_MODULE"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"rendered_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"differs_from_baseline": true,
|
||||
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/design/references/legacy/design-consultation.md",
|
||||
"rendered_legacy_body_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"rendered_legacy_body_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
|
||||
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
|
||||
"obligations_sha256": "c36974d06c0f66d0a0f2ea3d27282ed785c7cc956cce40a8c8db35492680c972",
|
||||
"obligations_sha256": "fa0722c312610cfe74c70b1e29f447622b360cb894f3c697b3ca959c3a068ddb",
|
||||
"heading_count": 12,
|
||||
"question_count": 2,
|
||||
"obligation_count": 18
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"rendered_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"differs_from_baseline": true,
|
||||
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/design/references/legacy/design-review.md",
|
||||
"rendered_legacy_body_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"rendered_legacy_body_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
|
||||
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
|
||||
"obligations_sha256": "e4fe261ecc3956834dfac15cac60fc4b8057ad0c3b37ede06ee4667d61852655",
|
||||
"obligations_sha256": "3e3b3b8a8e43c0120d60f5517cc03aa64bb31bf50adbe418ba89a7ae63557a55",
|
||||
"heading_count": 62,
|
||||
"question_count": 24,
|
||||
"obligation_count": 63
|
||||
|
||||
@@ -58,18 +58,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"rendered_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"differs_from_baseline": true,
|
||||
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/plan/references/legacy/office-hours.md",
|
||||
"rendered_legacy_body_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"rendered_legacy_body_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
|
||||
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
|
||||
"obligations_sha256": "c6f9f220f431b50b7721ac9baa3ae3d7be5de88b489c80a45350028c96a9db84",
|
||||
"obligations_sha256": "a8abcb960325c146cb555a71821d702b593bb7dc4f12646d4ecaea11a9cf6b68",
|
||||
"heading_count": 34,
|
||||
"question_count": 13,
|
||||
"obligation_count": 43
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"rendered_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"differs_from_baseline": true,
|
||||
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/qa/references/legacy/qa.md",
|
||||
"rendered_legacy_body_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"rendered_legacy_body_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
|
||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||
"obligations_sha256": "c9a3696553c10b1338568d728f86f47c9a7a06b2defa37bd557c662d4a9086d8",
|
||||
"obligations_sha256": "8c9e044c4224e4c35838d6ae9241405a5df42c2372c2ce272e0a11f37ab68c86",
|
||||
"heading_count": 58,
|
||||
"question_count": 6,
|
||||
"obligation_count": 65
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"rendered_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"differs_from_baseline": true,
|
||||
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/qa/references/legacy/qa-only.md",
|
||||
"rendered_legacy_body_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"rendered_legacy_body_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
|
||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||
"obligations_sha256": "9ea973707574aa3750e38a2920e65072f804fe7cddf789f1710e08b0a44d92d3",
|
||||
"obligations_sha256": "f16cce08f386155cae52758fb3f7235e1c8090ca7d84117caa4761b76927d9d5",
|
||||
"heading_count": 34,
|
||||
"question_count": 6,
|
||||
"obligation_count": 40
|
||||
|
||||
+3
-5
@@ -627,13 +627,11 @@ async function upgradeCommand({ args, home, stdout, installOptions = {} }) {
|
||||
if (parsed.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE");
|
||||
if (parsed.flags.has("--rollback")) {
|
||||
if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE");
|
||||
let rollbackBrowserChoice = null;
|
||||
const pointer = await rollbackUpgrade(home, {
|
||||
healthCheck: async (fallbackPath) => {
|
||||
rollbackBrowserChoice = await resolvedBrowserChoiceForRuntimePath(fallbackPath);
|
||||
},
|
||||
prepareActivation: async (fallbackPath) => ({
|
||||
browserChoice: await resolvedBrowserChoiceForRuntimePath(fallbackPath),
|
||||
}),
|
||||
});
|
||||
if (rollbackBrowserChoice) await configSetBrowserChoice(home, rollbackBrowserChoice);
|
||||
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+23
-8
@@ -143,7 +143,11 @@ export async function runDoctor(options = {}) {
|
||||
add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details);
|
||||
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
|
||||
const launchers = manifest?.capabilities ?? {};
|
||||
for (const capability of OPTIONAL_RUNTIME_CAPABILITIES) {
|
||||
const capabilitiesToInspect = [
|
||||
...OPTIONAL_RUNTIME_CAPABILITIES,
|
||||
...(selected.has("browser-visible") ? ["browser-visible"] : []),
|
||||
];
|
||||
for (const capability of capabilitiesToInspect) {
|
||||
if (!selected.has(capability)) {
|
||||
add(`capability:${capability}`, "warn", "not selected");
|
||||
continue;
|
||||
@@ -158,8 +162,11 @@ export async function runDoctor(options = {}) {
|
||||
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
|
||||
continue;
|
||||
}
|
||||
if (capability === "browser") {
|
||||
const browser = runtimeConfig?.browser?.provider === "installed"
|
||||
if (capability === "browser" || capability === "browser-visible") {
|
||||
const visible = capability === "browser-visible";
|
||||
const browser = visible && runtimeConfig?.browser?.provider !== "managed"
|
||||
? { ok: false, message: "visible GStack Browser requires the managed Chromium provider" }
|
||||
: runtimeConfig?.browser?.provider === "installed"
|
||||
? await inspectInstalledChromium(
|
||||
activeRoot,
|
||||
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
|
||||
@@ -167,7 +174,11 @@ export async function runDoctor(options = {}) {
|
||||
options,
|
||||
)
|
||||
: runtimeConfig?.browser?.provider === "managed"
|
||||
? await inspectManagedChromium(activeRoot, options.nodeCommand ?? process.env.GSTACK_NODE ?? "node")
|
||||
? await inspectManagedChromium(
|
||||
activeRoot,
|
||||
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
|
||||
{ visible },
|
||||
)
|
||||
: { ok: false, message: "browser capability is installed, but no browser provider was explicitly selected" };
|
||||
add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details);
|
||||
continue;
|
||||
@@ -245,7 +256,7 @@ async function inspectPython(env) {
|
||||
return { ok: false, message: "Python 3 is absent; only specialist flows that explicitly request it are unavailable" };
|
||||
}
|
||||
|
||||
async function inspectManagedChromium(activeRoot, nodeCommand) {
|
||||
async function inspectManagedChromium(activeRoot, nodeCommand, options = {}) {
|
||||
const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers");
|
||||
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
|
||||
const [browserStat, moduleStat] = await Promise.all([
|
||||
@@ -260,11 +271,15 @@ async function inspectManagedChromium(activeRoot, nodeCommand) {
|
||||
const result = await captureCommand(nodeCommand, [
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch({ headless: true }); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`,
|
||||
`const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch(${options.visible ? '{ headless: true, channel: "chromium" }' : "{ headless: true }"}); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`,
|
||||
], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } });
|
||||
const version = result.stdout.trim();
|
||||
if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" };
|
||||
return { ok: true, message: `managed headless Chromium ${version} launches and exits cleanly`, details: { browserRoot, version } };
|
||||
return {
|
||||
ok: true,
|
||||
message: `managed ${options.visible ? "visible-capable" : "headless"} Chromium ${version} launches and exits cleanly`,
|
||||
details: { browserRoot, version },
|
||||
};
|
||||
} catch (error) {
|
||||
return { ok: false, message: `managed Chromium is not runnable: ${error.message}` };
|
||||
}
|
||||
@@ -311,7 +326,7 @@ async function inspectXcrun() {
|
||||
}
|
||||
|
||||
function capabilityLaunchersReady(capability, launchers) {
|
||||
if (capability === "browser") return typeof launchers.browse === "string";
|
||||
if (capability === "browser" || capability === "browser-visible") return typeof launchers.browse === "string";
|
||||
if (capability === "design") return typeof launchers["gstack-design"] === "string";
|
||||
if (capability === "pdf") return typeof launchers["make-pdf"] === "string";
|
||||
if (capability === "diagram") return true;
|
||||
|
||||
+14
-7
@@ -1641,6 +1641,12 @@ const bundle = await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")
|
||||
const browserBacked = relative.startsWith("browse/") || relative.startsWith("make-pdf/");
|
||||
const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : [];
|
||||
const runtimeComponents = Array.isArray(bundle?.runtimeComponents) ? bundle.runtimeComponents : [];
|
||||
const visibleRequested = relative.startsWith("browse/") && (
|
||||
args.includes("connect") ||
|
||||
args.includes("handoff") ||
|
||||
args.includes("--headed") ||
|
||||
(args[0] === "pair-agent" && !args.includes("--headless"))
|
||||
);
|
||||
const slotProvider = bundle?.browserChoice?.provider ?? (
|
||||
runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible")
|
||||
? "managed"
|
||||
@@ -1656,16 +1662,16 @@ if (browserBacked) {
|
||||
if (slotProvider && browserChoice.provider !== slotProvider) {
|
||||
throw new Error("The selected browser provider does not match the active runtime slot; run the signed browser capability bootstrap for the selected provider");
|
||||
}
|
||||
if (visibleRequested && !selectedCapabilities.includes("browser-visible")) {
|
||||
if (browserChoice.provider === "installed") {
|
||||
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
|
||||
}
|
||||
throw new Error("The active runtime slot does not include visible Chromium; preview and approve the browser-visible capability first");
|
||||
}
|
||||
if (browserChoice.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible")) {
|
||||
throw new Error("Visible GStack Browser requires a managed Chromium runtime slot");
|
||||
}
|
||||
const visibleRequested = relative.startsWith("browse/") && (
|
||||
args.includes("connect") ||
|
||||
args.includes("handoff") ||
|
||||
args.includes("--headed") ||
|
||||
(args[0] === "pair-agent" && !args.includes("--headless"))
|
||||
);
|
||||
if (visibleRequested) {
|
||||
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
|
||||
}
|
||||
@@ -1835,7 +1841,8 @@ function processIsAlive(pid) {
|
||||
}
|
||||
|
||||
function validTransactionPath(value) {
|
||||
if (value === "runtime-install.json" || (typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value;
|
||||
if (value === "config.json" || value === "runtime-install.json" ||
|
||||
(typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value;
|
||||
throw new Error("Invalid managed runtime transaction path");
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ function validateTransactionPath(value) {
|
||||
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
|
||||
}
|
||||
const normalized = value.replaceAll("\\", "/");
|
||||
if (normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized;
|
||||
if (normalized === "config.json" || normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized;
|
||||
throw managedHomeError(`Invalid runtime transaction path: ${value}`, "RUNTIME_TRANSACTION_INVALID");
|
||||
}
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
+67
-1
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { assertPathInside, resolveRuntimePaths } from "./paths.js";
|
||||
import { atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
||||
import { atomicWriteFile, atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
||||
import {
|
||||
assertManagedHome,
|
||||
ensureManagedHome,
|
||||
ensureManagedRuntimeDirectory,
|
||||
recoverRuntimeTransactionUnlocked,
|
||||
RUNTIME_TRANSACTION_FILE,
|
||||
withRuntimeLifecycleLock,
|
||||
} from "./managed-home.js";
|
||||
import { configSetBrowserChoice } from "./config.js";
|
||||
import { errorWithCode as upgradeError } from "./errors.js";
|
||||
import { currentIsoTimestamp as isoNow } from "./time.js";
|
||||
|
||||
@@ -214,6 +216,11 @@ export async function rollbackUpgrade(home, options = {}) {
|
||||
}
|
||||
await assertTreeContainsNoLinks(fallbackPath);
|
||||
if (options.healthCheck) await options.healthCheck(fallbackPath);
|
||||
const activation = options.prepareActivation
|
||||
? await options.prepareActivation(fallbackPath)
|
||||
: null;
|
||||
const syncBrowserChoice = activation != null &&
|
||||
Object.prototype.hasOwnProperty.call(activation, "browserChoice");
|
||||
const rolledBack = {
|
||||
schemaVersion: 2,
|
||||
status: "active",
|
||||
@@ -222,11 +229,70 @@ export async function rollbackUpgrade(home, options = {}) {
|
||||
rolledBackFrom: pointer.current ?? null,
|
||||
rolledBackAt: isoNow(options.now),
|
||||
};
|
||||
if (!syncBrowserChoice) {
|
||||
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
|
||||
return rolledBack;
|
||||
}
|
||||
|
||||
const configSnapshot = await snapshotRollbackConfig(paths.config);
|
||||
const journalPath = path.join(resolved, RUNTIME_TRANSACTION_FILE);
|
||||
await atomicWriteJson(journalPath, {
|
||||
schemaVersion: 1,
|
||||
kind: "gstack-runtime-install-transaction",
|
||||
status: "prepared",
|
||||
home: resolved,
|
||||
version: fallbackVersion,
|
||||
previousPointerExists: true,
|
||||
previousPointer: pointer,
|
||||
files: [configSnapshot == null
|
||||
? { path: "config.json", existed: false }
|
||||
: {
|
||||
path: "config.json",
|
||||
existed: true,
|
||||
mode: configSnapshot.mode,
|
||||
dataBase64: configSnapshot.data.toString("base64"),
|
||||
}],
|
||||
preparedAt: isoNow(options.now),
|
||||
}, { mode: 0o600 });
|
||||
try {
|
||||
await configSetBrowserChoice(resolved, activation.browserChoice);
|
||||
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
|
||||
await fs.rm(journalPath, { force: true });
|
||||
} catch (cause) {
|
||||
const rollbackErrors = [];
|
||||
try {
|
||||
if (configSnapshot == null) await fs.rm(paths.config, { force: true });
|
||||
else await atomicWriteFile(paths.config, configSnapshot.data, { mode: configSnapshot.mode });
|
||||
} catch (error) {
|
||||
rollbackErrors.push(error);
|
||||
}
|
||||
try {
|
||||
await atomicWriteJson(paths.versionPointer, pointer, { mode: 0o600 });
|
||||
} catch (error) {
|
||||
rollbackErrors.push(error);
|
||||
}
|
||||
if (rollbackErrors.length === 0) await fs.rm(journalPath, { force: true });
|
||||
const error = upgradeError("Rollback activation failed and the previous runtime was restored", "ROLLBACK_ACTIVATION_FAILED", cause);
|
||||
if (rollbackErrors.length === 1) error.rollbackError = rollbackErrors[0];
|
||||
else if (rollbackErrors.length > 1) error.rollbackError = new AggregateError(rollbackErrors, "Runtime rollback restoration was incomplete");
|
||||
throw error;
|
||||
}
|
||||
return rolledBack;
|
||||
}, options);
|
||||
}
|
||||
|
||||
async function snapshotRollbackConfig(configPath) {
|
||||
const stat = await fs.lstat(configPath).catch((error) => {
|
||||
if (error?.code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!stat) return null;
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) {
|
||||
throw upgradeError("Refusing unsafe browser configuration during rollback", "RUNTIME_TRANSACTION_INVALID");
|
||||
}
|
||||
return { data: await fs.readFile(configPath), mode: stat.mode & 0o777 };
|
||||
}
|
||||
|
||||
export async function activeVersion(home, options = {}) {
|
||||
const recovered = await recoverPendingUpgrade(home, options);
|
||||
return recovered.pointer;
|
||||
|
||||
@@ -38,7 +38,7 @@ CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort
|
||||
|
||||
if [ -z "$CHROMIUM_DIR" ]; then
|
||||
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
||||
echo "Run: bunx playwright install chromium"
|
||||
echo "Run: bunx playwright-core install chromium"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -311,7 +311,8 @@ function portLegacyText(value: string, source: string): string {
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version')
|
||||
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
|
||||
.replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"')
|
||||
.replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.')
|
||||
.replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
|
||||
.replaceAll('Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
|
||||
.replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, '');
|
||||
|
||||
body = body
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Generate --module design-consultation visibility=primary depth=deep mutation=design-artifacts web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-consultation -->
|
||||
@@ -74,7 +74,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-html visibility=primary depth=standard mutation=design-artifacts web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-html -->
|
||||
@@ -167,7 +167,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-review visibility=primary depth=deep mutation=fix-safe web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-review -->
|
||||
@@ -81,7 +81,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$plan --mode Discovery --module office-hours visibility=primary depth=deep mutation=design-doc-only web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=office-hours -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module benchmark visibility=primary depth=standard mutation=report-only web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=benchmark -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module browse visibility=internal depth=standard mutation=source-defined web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=browse -->
|
||||
@@ -37,7 +37,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module devex-review visibility=primary depth=deep mutation=report-only web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=devex-review -->
|
||||
@@ -71,7 +71,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module open-gstack-browser visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=open-gstack-browser -->
|
||||
@@ -43,7 +43,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module pair-agent visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=pair-agent -->
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module qa-only visibility=primary depth=deep mutation=report-only web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=qa-only -->
|
||||
@@ -52,7 +52,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Fix --module qa visibility=primary depth=deep mutation=fix-safe web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=qa -->
|
||||
@@ -123,7 +123,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module setup-browser-cookies visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=setup-browser-cookies -->
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$ship --mode Land --module land-and-deploy visibility=primary depth=deep mutation=merge-deploy web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=land-and-deploy -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -161,6 +161,23 @@ describe("GStack 2 managed runtime installer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("managed headless launchers require the separately approved visible-browser slot", async () => {
|
||||
await withFixture(async ({ source, home }) => {
|
||||
await installFixture(source, home, "managed-visible-refusal", {
|
||||
entries: BROWSER_ENTRIES,
|
||||
capabilities: BROWSER_CAPABILITIES,
|
||||
browserChoice: { provider: "managed", executablePath: null },
|
||||
});
|
||||
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
|
||||
await expect(runInstalledLauncher(home, "browse", ["--headed"], { capture: true }))
|
||||
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
|
||||
await expect(runInstalledLauncher(home, "browse", ["connect"], { capture: true }))
|
||||
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
|
||||
await expect(runInstalledLauncher(home, "browse", ["handoff"], { capture: true }))
|
||||
.rejects.toMatchObject({ stderr: expect.stringContaining("does not include visible Chromium") });
|
||||
});
|
||||
});
|
||||
|
||||
test("design-only launchers do not require an unrelated browser selection", async () => {
|
||||
await withFixture(async ({ source, home }) => {
|
||||
const design = path.join(source, "design", "dist", "design");
|
||||
@@ -248,6 +265,45 @@ describe("GStack 2 managed runtime installer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("rollback switches the runtime pointer and recorded browser choice in one recoverable transaction", async () => {
|
||||
await withFixture(async ({ source, home }) => {
|
||||
const executable = await fs.realpath(process.execPath);
|
||||
const fallback = await installFixture(source, home, "installed-fallback-valid");
|
||||
const fallbackManifestPath = path.join(fallback.path, ".gstack-bundle.json");
|
||||
const fallbackManifest = await readJson(fallbackManifestPath);
|
||||
await fs.writeFile(fallbackManifestPath, JSON.stringify({
|
||||
...fallbackManifest,
|
||||
selectedCapabilities: ["browser"],
|
||||
runtimeComponents: ["browser-code", "core"],
|
||||
browserChoice: { provider: "installed", executablePath: executable },
|
||||
}));
|
||||
|
||||
const current = await installFixture(source, home, "managed-current-valid");
|
||||
const currentManifestPath = path.join(current.path, ".gstack-bundle.json");
|
||||
const currentManifest = await readJson(currentManifestPath);
|
||||
await fs.writeFile(currentManifestPath, JSON.stringify({
|
||||
...currentManifest,
|
||||
selectedCapabilities: ["browser"],
|
||||
runtimeComponents: ["browser-headless", "core"],
|
||||
browserChoice: { provider: "managed", executablePath: null },
|
||||
}));
|
||||
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
|
||||
|
||||
const output = captureStream();
|
||||
expect(await runtimeMain(["upgrade", "--rollback"], {
|
||||
cwd: source,
|
||||
env: { ...process.env, GSTACK_HOME: home },
|
||||
stdout: output.stream,
|
||||
stderr: output.stream,
|
||||
})).toBe(0);
|
||||
expect(await readJson(path.join(home, "versions", "current.json")))
|
||||
.toMatchObject({ current: "installed-fallback-valid", lastKnownGood: "managed-current-valid" });
|
||||
expect((await readJson(path.join(home, "config.json"))).browser)
|
||||
.toEqual({ provider: "installed", executablePath: executable });
|
||||
expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("an installed-browser setup persists the choice only after activation and launches through it", async () => {
|
||||
await withFixture(async ({ root, source, home }) => {
|
||||
await fs.writeFile(path.join(source, "cap", "tool"), `#!/usr/bin/env node
|
||||
@@ -958,14 +1014,17 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
|
||||
test("a launcher repairs a crash journal before resolving any runtime", async () => {
|
||||
await withFixture(async ({ source, home }) => {
|
||||
await installFixture(source, home, "1.0.0");
|
||||
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
|
||||
const pointer = await readJson(path.join(home, "versions", "current.json"));
|
||||
const manifest = await fs.readFile(path.join(home, "runtime-install.json"));
|
||||
const config = await fs.readFile(path.join(home, "config.json"));
|
||||
// Keep the launcher for this host executable so it can enter the shared
|
||||
// recovery path. The transaction restores the inactive host variant.
|
||||
const recoverableLauncher = process.platform === "win32" ? "gstack" : "gstack.cmd";
|
||||
const launcherPath = path.join(home, "bin", recoverableLauncher);
|
||||
const launcherBefore = await fs.readFile(launcherPath);
|
||||
await fs.writeFile(path.join(home, "runtime-install.json"), '{"activeVersion":"crashed"}\n');
|
||||
await configSetBrowserChoice(home, { provider: "installed", executablePath: process.execPath });
|
||||
await fs.writeFile(launcherPath, "candidate launcher\n");
|
||||
await fs.writeFile(path.join(home, "versions", "current.json"), `${JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
@@ -982,6 +1041,7 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
|
||||
previousPointerExists: true,
|
||||
previousPointer: pointer,
|
||||
files: [
|
||||
{ path: "config.json", existed: true, mode: 0o644, dataBase64: config.toString("base64") },
|
||||
{ path: "runtime-install.json", existed: true, mode: 0o600, dataBase64: manifest.toString("base64") },
|
||||
{ path: `bin/${recoverableLauncher}`, existed: true, mode: 0o644, dataBase64: launcherBefore.toString("base64") },
|
||||
],
|
||||
@@ -997,6 +1057,7 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
|
||||
const launched = await runInstalledLauncher(home, "gstack", ["doctor"], { capture: true });
|
||||
expect(launched.stdout).toContain("gstack fixture doctor");
|
||||
expect(await readJson(path.join(home, "versions", "current.json"))).toEqual(pointer);
|
||||
expect(await fs.readFile(path.join(home, "config.json"))).toEqual(config);
|
||||
expect(await fs.readFile(path.join(home, "runtime-install.json"))).toEqual(manifest);
|
||||
expect(await fs.readFile(launcherPath)).toEqual(launcherBefore);
|
||||
expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false);
|
||||
|
||||
@@ -249,6 +249,25 @@ describe("one config authority", () => {
|
||||
expect(setup.stdout).toContain("optional runtime: unchanged");
|
||||
});
|
||||
|
||||
test("generic config writes cannot create an incoherent browser selection", async () => {
|
||||
const base = await root();
|
||||
const home = path.join(base, "state");
|
||||
const project = path.join(base, "project");
|
||||
await fs.mkdir(project);
|
||||
const run = (args: string[]) => spawnSync(process.execPath, [gstackBin, ...args], {
|
||||
cwd: project,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, GSTACK_HOME: home },
|
||||
});
|
||||
|
||||
for (const key of ["browser", "browser.provider", "browser.executablePath"]) {
|
||||
const result = run(["config", "set", key, "installed"]);
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("gstack config browser");
|
||||
}
|
||||
expect(await fs.stat(path.join(home, "config.json")).catch(() => null)).toBeNull();
|
||||
});
|
||||
|
||||
test("legacy YAML is read-only migration input and JSON takes authority on write", async () => {
|
||||
const home = path.join(await root(), "legacy");
|
||||
await fs.mkdir(home);
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { PassThrough, Readable } from "node:stream";
|
||||
import { runDoctor } from "../runtime/doctor.js";
|
||||
import { runInstallerCli, runtimeSlotVersion, runtimeSurfaceForCapabilities } from "../runtime/install.js";
|
||||
@@ -49,6 +50,36 @@ function officialManifestFixture(target: string, customize?: (component: string,
|
||||
};
|
||||
}
|
||||
|
||||
async function createActiveRuntimeFixture(home: string, options: {
|
||||
bundleVersion: string;
|
||||
selectedCapabilities: string[];
|
||||
runtimeComponents: string[];
|
||||
browserChoice: { provider: "managed" | "installed"; executablePath: string | null };
|
||||
}) {
|
||||
const root = path.join(home, "versions", "active-slot");
|
||||
const payload = Buffer.from("verified active runtime payload\n");
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await fs.writeFile(path.join(root, "payload.txt"), payload);
|
||||
await fs.writeFile(path.join(root, ".gstack-bundle.json"), JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
version: options.bundleVersion,
|
||||
selectedCapabilities: options.selectedCapabilities,
|
||||
runtimeComponents: options.runtimeComponents,
|
||||
browserChoice: options.browserChoice,
|
||||
files: [{
|
||||
path: "payload.txt",
|
||||
size: payload.byteLength,
|
||||
sha256: createHash("sha256").update(payload).digest("hex"),
|
||||
}],
|
||||
}));
|
||||
await fs.writeFile(path.join(home, "versions", "current.json"), JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
status: "active",
|
||||
current: "active-slot",
|
||||
lastKnownGood: null,
|
||||
}));
|
||||
}
|
||||
|
||||
describe("GStack runtime setup UX", () => {
|
||||
test("capability selection keeps the core and excludes unselected heavyweight surfaces", () => {
|
||||
const surface = runtimeSurfaceForCapabilities(["browser"]);
|
||||
@@ -277,6 +308,32 @@ describe("GStack runtime setup UX", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("explicit install-later needs no browser selection and does not prompt or mutate", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-browser-later-"));
|
||||
try {
|
||||
const home = path.join(root, "home");
|
||||
const output = capture();
|
||||
const input = Readable.from([]) as Readable & { isTTY: boolean };
|
||||
input.isTTY = false;
|
||||
expect(await runInstallerCli([
|
||||
"--source", path.resolve(import.meta.dir, ".."),
|
||||
"--home", home,
|
||||
"--capabilities", "browser",
|
||||
"--install-later",
|
||||
"--json",
|
||||
], { stdin: input, stdout: output.stream, stderr: output.stream })).toBe(0);
|
||||
expect(JSON.parse(output.value())).toMatchObject({
|
||||
ok: true,
|
||||
action: "install-later",
|
||||
mutated: false,
|
||||
preview: null,
|
||||
});
|
||||
await expect(fs.stat(home)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("browser bootstrap options are local-only and browser preview requires an explicit choice", async () => {
|
||||
const output = capture();
|
||||
let fetches = 0;
|
||||
@@ -332,6 +389,76 @@ describe("GStack runtime setup UX", () => {
|
||||
expect(fetches).toBe(1);
|
||||
});
|
||||
|
||||
test("official previews retain an active installed-browser choice across same- and cross-release additions", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-retain-browser-"));
|
||||
const executable = await fs.realpath(process.execPath);
|
||||
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
|
||||
try {
|
||||
for (const [name, bundleVersion, expectsReuse] of [
|
||||
["same", `${BOOTSTRAP_RUNTIME_VERSION}-caps-installed`, true],
|
||||
["cross", "1.9.0-caps-installed", false],
|
||||
] as const) {
|
||||
const home = path.join(root, name);
|
||||
await createActiveRuntimeFixture(home, {
|
||||
bundleVersion,
|
||||
selectedCapabilities: ["browser"],
|
||||
runtimeComponents: ["browser-code", "core"],
|
||||
browserChoice: { provider: "installed", executablePath: executable },
|
||||
});
|
||||
const output = capture();
|
||||
expect(await bootstrapMain([
|
||||
"preview", "--capability", "design", "--home", home, "--json",
|
||||
], {
|
||||
stdout: output.stream,
|
||||
stderr: output.stream,
|
||||
libc: process.platform === "linux" ? "glibc" : undefined,
|
||||
fetch: async (url: string) => ({ ok: true, url, json: async () => officialManifestFixture(target) }),
|
||||
})).toBe(0);
|
||||
const result = JSON.parse(output.value());
|
||||
expect(result.capabilities).toEqual(["browser", "design"]);
|
||||
expect(result.browser).toEqual({ provider: "installed", executablePath: executable });
|
||||
expect(result.components).toEqual(["browser-code", "core", "design"]);
|
||||
expect(result.components).not.toContain("browser-headless");
|
||||
expect(result.reusedComponents.length > 0).toBe(expectsReuse);
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("switching a reusable managed visible slot to installed drops visible payload from the exact plan", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-provider-switch-"));
|
||||
const home = path.join(root, "home");
|
||||
const executable = await fs.realpath(process.execPath);
|
||||
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
|
||||
try {
|
||||
await createActiveRuntimeFixture(home, {
|
||||
bundleVersion: `${BOOTSTRAP_RUNTIME_VERSION}-caps-managed-visible`,
|
||||
selectedCapabilities: ["browser-visible"],
|
||||
runtimeComponents: ["browser-code", "browser-visible", "core"],
|
||||
browserChoice: { provider: "managed", executablePath: null },
|
||||
});
|
||||
const output = capture();
|
||||
expect(await bootstrapMain([
|
||||
"preview", "--capability", "browser", "--browser", "installed",
|
||||
"--browser-path", executable, "--home", home, "--json",
|
||||
], {
|
||||
stdout: output.stream,
|
||||
stderr: output.stream,
|
||||
libc: process.platform === "linux" ? "glibc" : undefined,
|
||||
fetch: async (url: string) => ({ ok: true, url, json: async () => officialManifestFixture(target) }),
|
||||
})).toBe(0);
|
||||
const result = JSON.parse(output.value());
|
||||
expect(result.capabilities).toEqual(["browser"]);
|
||||
expect(result.browser.provider).toBe("installed");
|
||||
expect(result.components).toEqual(["browser-code", "core"]);
|
||||
expect(result.components).not.toContain("browser-visible");
|
||||
expect(result.components).not.toContain("browser-headless");
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("visible GStack Browser refuses installed Chrome before any network request", async () => {
|
||||
const output = capture();
|
||||
let fetches = 0;
|
||||
@@ -468,6 +595,43 @@ describe("GStack runtime setup UX", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor reports and launches an internal managed visible-browser slot", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-visible-browser-"));
|
||||
const home = path.join(root, "home");
|
||||
try {
|
||||
await setupRuntime({ home, cwd: root });
|
||||
const paths = resolveRuntimePaths({ home });
|
||||
const active = path.join(paths.versions, "fixture");
|
||||
const browserRoot = path.join(active, ".gstack-runtime-browsers");
|
||||
const managedBun = path.join(active, ".gstack-runtime-tools", process.platform === "win32" ? "bun.exe" : "bun");
|
||||
const playwright = path.join(active, "node_modules", "playwright");
|
||||
await fs.mkdir(path.join(browserRoot, "chromium-fixture"), { recursive: true });
|
||||
await fs.mkdir(path.dirname(managedBun), { recursive: true });
|
||||
await fs.mkdir(playwright, { recursive: true });
|
||||
await fs.copyFile(process.execPath, managedBun);
|
||||
if (process.platform !== "win32") await fs.chmod(managedBun, 0o755);
|
||||
await fs.writeFile(path.join(playwright, "index.mjs"),
|
||||
`export const chromium = { launch: async ({ headless, channel }) => { if (headless !== true || channel !== "chromium") throw new Error("expected full Chromium channel"); return { version: () => "fixture-visible", close: async () => {} }; } };\n`);
|
||||
await fs.writeFile(path.join(active, ".gstack-bundle.json"), JSON.stringify({
|
||||
compatibility: { skillApi: "2.0" },
|
||||
selectedCapabilities: ["browser-visible"],
|
||||
capabilities: { browse: "browse/dist/browse" },
|
||||
tools: { bun: { path: path.relative(active, managedBun).split(path.sep).join("/"), version: "1.3.14" } },
|
||||
}));
|
||||
await fs.writeFile(paths.versionPointer, JSON.stringify({
|
||||
schemaVersion: 2, status: "active", current: "fixture", lastKnownGood: "fixture",
|
||||
}));
|
||||
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
|
||||
const report = await runDoctor({ home, cwd: root, nodeCommand: process.execPath });
|
||||
expect(report.checks.find((check) => check.id === "capability:browser-visible")).toMatchObject({
|
||||
status: "pass",
|
||||
details: { browserRoot, version: "fixture-visible" },
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("doctor launches the explicitly selected installed browser through the same Playwright adapter", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-installed-browser-"));
|
||||
const home = path.join(root, "home");
|
||||
@@ -660,9 +824,50 @@ describe("GStack runtime setup UX", () => {
|
||||
expect(args).toContain("--yes");
|
||||
expect(args).toContain("browser,diagram,pdf");
|
||||
expect(args).not.toContain("--prepared");
|
||||
expect(args).toContain("--replace-capabilities");
|
||||
expect(output.value()).toContain("Developer-only source install");
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("developer source fallback can switch a retained managed visible slot to installed", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-source-switch-"));
|
||||
const source = path.join(root, "source");
|
||||
const runtime = path.join(source, "runtime");
|
||||
const home = path.join(root, "home");
|
||||
const log = path.join(root, "args.json");
|
||||
const executable = await fs.realpath(process.execPath);
|
||||
const output = capture();
|
||||
try {
|
||||
await fs.mkdir(runtime, { recursive: true });
|
||||
await fs.writeFile(path.join(runtime, "install.js"),
|
||||
`import fs from "node:fs"; fs.writeFileSync(process.env.BOOTSTRAP_TEST_LOG, JSON.stringify(process.argv.slice(2)));\n`);
|
||||
await createActiveRuntimeFixture(home, {
|
||||
bundleVersion: `${BOOTSTRAP_RUNTIME_VERSION}-caps-managed-visible`,
|
||||
selectedCapabilities: ["browser-visible"],
|
||||
runtimeComponents: ["browser-code", "browser-visible", "core"],
|
||||
browserChoice: { provider: "managed", executablePath: null },
|
||||
});
|
||||
const previous = process.env.BOOTSTRAP_TEST_LOG;
|
||||
process.env.BOOTSTRAP_TEST_LOG = log;
|
||||
try {
|
||||
expect(await bootstrapMain([
|
||||
"install", "--source", source, "--capability", "browser", "--browser", "installed",
|
||||
"--browser-path", executable, "--home", home, "--yes",
|
||||
], { stdout: output.stream, stderr: output.stream })).toBe(0);
|
||||
} finally {
|
||||
if (previous == null) delete process.env.BOOTSTRAP_TEST_LOG;
|
||||
else process.env.BOOTSTRAP_TEST_LOG = previous;
|
||||
}
|
||||
const args = JSON.parse(await fs.readFile(log, "utf8"));
|
||||
expect(args).toContain("browser");
|
||||
expect(args).toContain("installed");
|
||||
expect(args).toContain(executable);
|
||||
expect(args).toContain("--replace-capabilities");
|
||||
expect(args).not.toContain("browser-visible");
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user