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
|
RUN npm i -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
# Playwright system deps (Chromium) — needed for browse E2E tests
|
# 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
|
# 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
|
# 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
|
# but the dep is implicit and could change — install explicitly so upgrades
|
||||||
# can't silently regress rendering.
|
# 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
|
# Install Playwright Chromium to a shared location accessible by all users
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
|
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
|
&& chmod -R a+rX /opt/playwright-browsers
|
||||||
|
|
||||||
# Verify everything works
|
# Verify everything works
|
||||||
RUN bun --version && node --version && claude --version && jq --version && gh --version \
|
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" \
|
&& fc-match "Liberation Sans" | grep -qi "Liberation" \
|
||||||
|| (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1)
|
|| (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
|
fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true
|
||||||
|
|
||||||
- name: Install Playwright Chromium
|
- name: Install Playwright Chromium
|
||||||
run: bunx playwright install chromium
|
run: bunx playwright-core install chromium
|
||||||
|
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ export class BrowserManager {
|
|||||||
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
||||||
// Extensions only work in headed mode, so we use an off-screen window.
|
// Extensions only work in headed mode, so we use an off-screen window.
|
||||||
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
||||||
|
if (extensionsDir) assertHeadedBrowserProvider();
|
||||||
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
|
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
|
||||||
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
|
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
|
||||||
let useHeadless = true;
|
let useHeadless = true;
|
||||||
@@ -1587,6 +1588,7 @@ export class BrowserManager {
|
|||||||
* If step 2 fails → return error, headless browser untouched
|
* If step 2 fails → return error, headless browser untouched
|
||||||
*/
|
*/
|
||||||
async handoff(message: string): Promise<string> {
|
async handoff(message: string): Promise<string> {
|
||||||
|
assertHeadedBrowserProvider();
|
||||||
if (this.connectionMode === 'headed' || this.isHeaded) {
|
if (this.connectionMode === 'headed' || this.isHeaded) {
|
||||||
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
|
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -118,7 +118,7 @@ interface ServerState {
|
|||||||
serverPath: string;
|
serverPath: string;
|
||||||
binaryVersion?: string;
|
binaryVersion?: string;
|
||||||
mode?: 'launched' | 'headed';
|
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;
|
configHash?: string;
|
||||||
/** Xvfb child PID for cleanup on disconnect. */
|
/** Xvfb child PID for cleanup on disconnect. */
|
||||||
xvfbPid?: number;
|
xvfbPid?: number;
|
||||||
@@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
|
|||||||
// hint. No silent restart — that would drop tab state, cookies, and
|
// hint. No silent restart — that would drop tab state, cookies, and
|
||||||
// logged-in sessions without warning.
|
// logged-in sessions without warning.
|
||||||
if (desiredHash && state.configHash && state.configHash !== desiredHash) {
|
if (desiredHash && state.configHash && state.configHash !== desiredHash) {
|
||||||
console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`);
|
console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`);
|
||||||
console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`);
|
console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
// Same path: existing daemon is plain (no flags) but caller passes
|
// Same path: existing daemon is plain (no flags) but caller passes
|
||||||
@@ -782,7 +782,7 @@ export interface GlobalFlags {
|
|||||||
proxyUrl: string | null;
|
proxyUrl: string | null;
|
||||||
/** Whether --headed was passed. */
|
/** Whether --headed was passed. */
|
||||||
headed: boolean;
|
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;
|
configHash: string;
|
||||||
/** Redacted form of proxyUrl, safe for logs. */
|
/** Redacted form of proxyUrl, safe for logs. */
|
||||||
redactedProxyUrl: string;
|
redactedProxyUrl: string;
|
||||||
@@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G
|
|||||||
args: out,
|
args: out,
|
||||||
proxyUrl: canonicalProxyUrl,
|
proxyUrl: canonicalProxyUrl,
|
||||||
headed,
|
headed,
|
||||||
configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }),
|
configHash: computeConfigHash({
|
||||||
|
proxyUrl: canonicalProxyUrl,
|
||||||
|
headed,
|
||||||
|
browserProvider: env.GSTACK_BROWSER_PROVIDER,
|
||||||
|
browserExecutable: env.GSTACK_CHROMIUM_PATH,
|
||||||
|
}),
|
||||||
redactedProxyUrl: redactProxyUrl(canonicalProxyUrl),
|
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
|
* detection (D2). The hash is deterministic across CLI invocations on the
|
||||||
* same machine and survives daemon restarts via the state file.
|
* same machine and survives daemon restarts via the state file.
|
||||||
*
|
*
|
||||||
@@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
|
|||||||
export function computeConfigHash(opts: {
|
export function computeConfigHash(opts: {
|
||||||
proxyUrl: string | null | undefined;
|
proxyUrl: string | null | undefined;
|
||||||
headed: boolean;
|
headed: boolean;
|
||||||
|
browserProvider?: string | null;
|
||||||
|
browserExecutable?: string | null;
|
||||||
}): string {
|
}): string {
|
||||||
const proxyKey = canonicalizeProxyUrl(opts.proxyUrl);
|
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);
|
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -355,11 +355,18 @@ export async function handleWriteCommand(
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
|
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
|
||||||
const isOption = 'locator' in resolved
|
// Do not start a second auto-wait after the click has already timed out.
|
||||||
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
|
// Missing selectors used to spend 5s in click(), then block again in
|
||||||
: await target.locator(resolved.selector).evaluate(
|
// evaluate() until the outer client killed the command. count() is an
|
||||||
el => el.tagName === 'OPTION'
|
// immediate query and keeps the helpful option guidance only when one
|
||||||
).catch(() => false);
|
// 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) {
|
if (isOption) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
|
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
|
||||||
|
|||||||
@@ -478,6 +478,20 @@ describe('Interaction', () => {
|
|||||||
}
|
}
|
||||||
}, 15000);
|
}, 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 () => {
|
test('hover works', async () => {
|
||||||
const result = await handleWriteCommand('hover', ['h1'], bm);
|
const result = await handleWriteCommand('hover', ['h1'], bm);
|
||||||
expect(result).toContain('Hovered');
|
expect(result).toContain('Hovered');
|
||||||
|
|||||||
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
|
|||||||
}
|
}
|
||||||
}, 15000);
|
}, 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 () => {
|
test('refuses when existing plain daemon meets a --proxy invocation', async () => {
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
|
||||||
const stateFile = path.join(tmpDir, 'browse.json');
|
const stateFile = path.join(tmpDir, 'browse.json');
|
||||||
|
|||||||
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
|
|||||||
);
|
);
|
||||||
expect(a.configHash).not.toBe(b.configHash);
|
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",
|
"source_path": "office-hours/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"target": "skills/plan/references/legacy/office-hours.md",
|
"target": "skills/plan/references/legacy/office-hours.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1416,7 +1416,7 @@
|
|||||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"target": "skills/design/references/legacy/design-consultation.md",
|
"target": "skills/design/references/legacy/design-consultation.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1591,7 +1591,7 @@
|
|||||||
"source_path": "design-html/SKILL.md.tmpl",
|
"source_path": "design-html/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||||
"target": "skills/design/references/legacy/design-html.md",
|
"target": "skills/design/references/legacy/design-html.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1820,7 +1820,7 @@
|
|||||||
"source_path": "design-review/SKILL.md.tmpl",
|
"source_path": "design-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"target": "skills/design/references/legacy/design-review.md",
|
"target": "skills/design/references/legacy/design-review.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2162,7 +2162,7 @@
|
|||||||
"source_path": "qa/SKILL.md.tmpl",
|
"source_path": "qa/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"target": "skills/qa/references/legacy/qa.md",
|
"target": "skills/qa/references/legacy/qa.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2288,7 +2288,7 @@
|
|||||||
"source_path": "qa-only/SKILL.md.tmpl",
|
"source_path": "qa-only/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"target": "skills/qa/references/legacy/qa-only.md",
|
"target": "skills/qa/references/legacy/qa-only.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2466,7 +2466,7 @@
|
|||||||
"source_path": "devex-review/SKILL.md.tmpl",
|
"source_path": "devex-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||||
"target": "skills/qa/references/legacy/devex-review.md",
|
"target": "skills/qa/references/legacy/devex-review.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2565,7 +2565,7 @@
|
|||||||
"source_path": "benchmark/SKILL.md.tmpl",
|
"source_path": "benchmark/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||||
"target": "skills/qa/references/legacy/benchmark.md",
|
"target": "skills/qa/references/legacy/benchmark.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2631,7 +2631,7 @@
|
|||||||
"source_path": "canary/SKILL.md.tmpl",
|
"source_path": "canary/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||||
"target": "skills/qa/references/legacy/canary.md",
|
"target": "skills/qa/references/legacy/canary.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2710,7 +2710,7 @@
|
|||||||
"source_path": "browse/SKILL.md.tmpl",
|
"source_path": "browse/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||||
"target": "skills/qa/references/legacy/browse.md",
|
"target": "skills/qa/references/legacy/browse.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2798,7 +2798,7 @@
|
|||||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2873,7 +2873,7 @@
|
|||||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2934,7 +2934,7 @@
|
|||||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -4136,7 +4136,7 @@
|
|||||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -5775,7 +5775,7 @@
|
|||||||
"owner_tree": "qa",
|
"owner_tree": "qa",
|
||||||
"consumer_tree": "ship",
|
"consumer_tree": "ship",
|
||||||
"target": "skills/ship/references/legacy/canary.md",
|
"target": "skills/ship/references/legacy/canary.md",
|
||||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||||
"disposition": "SHARED_MODULE"
|
"disposition": "SHARED_MODULE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ manifest contains only official GitHub Release URLs and the fixed workflow
|
|||||||
certificate identity.
|
certificate identity.
|
||||||
|
|
||||||
Browser-capable archives include the Playwright-managed Chromium directory at
|
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
|
only—never `--with-deps` or `sudo`—copies physical files into the immutable
|
||||||
slot, and launch-smokes that exact Chromium on every native release runner.
|
slot, and launch-smokes that exact Chromium on every native release runner.
|
||||||
The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active
|
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
|
three retained Claude Haiku live samples are classified `REGRESSION`; they
|
||||||
are preserved as noisy supplemental evidence, never cherry-picked as a
|
are preserved as noisy supplemental evidence, never cherry-picked as a
|
||||||
primary gate or represented as green.
|
primary gate or represented as green.
|
||||||
- [x] The current macOS GStack 2 suite is green: 151 pass / 0 fail and 1,194
|
- [x] The current macOS GStack 2 suite is green: 218 pass / 0 fail and 2,229
|
||||||
assertions across 16 files.
|
assertions across 20 files.
|
||||||
- [x] Optional host-neutral runtime implemented with canonical paths,
|
- [x] Optional host-neutral runtime implemented with canonical paths,
|
||||||
repo/worktree state identity, locks, atomic writes, effect claims,
|
repo/worktree state identity, locks, atomic writes, effect claims,
|
||||||
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
|
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
|
||||||
- [x] Managed runtime installer coverage is green at 25 pass / 0 fail and 341
|
- [x] Managed runtime installer coverage is green at 34 pass / 0 fail. The
|
||||||
assertions. The deterministic clean macOS arm64 managed-bundle audit records
|
deterministic clean macOS arm64 managed-bundle audit records
|
||||||
110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers.
|
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;
|
This is a platform-specific bundle measurement, not a universal byte count;
|
||||||
platform-native package payloads differ. Setup installs frozen
|
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
|
production-only install with the development SDK absent, completed a local
|
||||||
browser journey and Sharp full-page screenshot, and uninstalled while
|
browser journey and Sharp full-page screenshot, and uninstalled while
|
||||||
preserving state.
|
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
|
- [x] The current candidate additionally captures a runtime-owned Bun 1.3.14
|
||||||
executable under `.gstack-runtime-tools`, records its path/version in the
|
executable under `.gstack-runtime-tools`, records its path/version in the
|
||||||
bundle manifest, vendors the tagged license/source notices, and routes 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 |
|
| 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. |
|
| 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 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. |
|
| `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. |
|
| 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). |
|
| `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 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). |
|
| 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`. |
|
| 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. |
|
| 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. |
|
| `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",
|
"source_path": "benchmark/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||||
"target": "skills/qa/references/legacy/benchmark.md",
|
"target": "skills/qa/references/legacy/benchmark.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679
|
679
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "browse/SKILL.md.tmpl",
|
"source_path": "browse/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||||
"target": "skills/qa/references/legacy/browse.md",
|
"target": "skills/qa/references/legacy/browse.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "canary/SKILL.md.tmpl",
|
"source_path": "canary/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||||
"target": "skills/qa/references/legacy/canary.md",
|
"target": "skills/qa/references/legacy/canary.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"target": "skills/design/references/legacy/design-consultation.md",
|
"target": "skills/design/references/legacy/design-consultation.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "design-html/SKILL.md.tmpl",
|
"source_path": "design-html/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||||
"target": "skills/design/references/legacy/design-html.md",
|
"target": "skills/design/references/legacy/design-html.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679
|
679
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "design-review/SKILL.md.tmpl",
|
"source_path": "design-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"target": "skills/design/references/legacy/design-review.md",
|
"target": "skills/design/references/legacy/design-review.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "devex-review/SKILL.md.tmpl",
|
"source_path": "devex-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||||
"target": "skills/qa/references/legacy/devex-review.md",
|
"target": "skills/qa/references/legacy/devex-review.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "office-hours/SKILL.md.tmpl",
|
"source_path": "office-hours/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"target": "skills/plan/references/legacy/office-hours.md",
|
"target": "skills/plan/references/legacy/office-hours.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679
|
679
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679
|
679
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "qa-only/SKILL.md.tmpl",
|
"source_path": "qa-only/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"target": "skills/qa/references/legacy/qa-only.md",
|
"target": "skills/qa/references/legacy/qa-only.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "qa/SKILL.md.tmpl",
|
"source_path": "qa/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"target": "skills/qa/references/legacy/qa.md",
|
"target": "skills/qa/references/legacy/qa.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679,
|
679,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
679
|
679
|
||||||
|
|||||||
+15
-15
@@ -83,7 +83,7 @@
|
|||||||
"source_path": "office-hours/SKILL.md.tmpl",
|
"source_path": "office-hours/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"target": "skills/plan/references/legacy/office-hours.md",
|
"target": "skills/plan/references/legacy/office-hours.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1416,7 +1416,7 @@
|
|||||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"target": "skills/design/references/legacy/design-consultation.md",
|
"target": "skills/design/references/legacy/design-consultation.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1591,7 +1591,7 @@
|
|||||||
"source_path": "design-html/SKILL.md.tmpl",
|
"source_path": "design-html/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||||
"target": "skills/design/references/legacy/design-html.md",
|
"target": "skills/design/references/legacy/design-html.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -1820,7 +1820,7 @@
|
|||||||
"source_path": "design-review/SKILL.md.tmpl",
|
"source_path": "design-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"target": "skills/design/references/legacy/design-review.md",
|
"target": "skills/design/references/legacy/design-review.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2162,7 +2162,7 @@
|
|||||||
"source_path": "qa/SKILL.md.tmpl",
|
"source_path": "qa/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"target": "skills/qa/references/legacy/qa.md",
|
"target": "skills/qa/references/legacy/qa.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2288,7 +2288,7 @@
|
|||||||
"source_path": "qa-only/SKILL.md.tmpl",
|
"source_path": "qa-only/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"target": "skills/qa/references/legacy/qa-only.md",
|
"target": "skills/qa/references/legacy/qa-only.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2466,7 +2466,7 @@
|
|||||||
"source_path": "devex-review/SKILL.md.tmpl",
|
"source_path": "devex-review/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||||
"target": "skills/qa/references/legacy/devex-review.md",
|
"target": "skills/qa/references/legacy/devex-review.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2565,7 +2565,7 @@
|
|||||||
"source_path": "benchmark/SKILL.md.tmpl",
|
"source_path": "benchmark/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||||
"target": "skills/qa/references/legacy/benchmark.md",
|
"target": "skills/qa/references/legacy/benchmark.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2631,7 +2631,7 @@
|
|||||||
"source_path": "canary/SKILL.md.tmpl",
|
"source_path": "canary/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||||
"target": "skills/qa/references/legacy/canary.md",
|
"target": "skills/qa/references/legacy/canary.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2710,7 +2710,7 @@
|
|||||||
"source_path": "browse/SKILL.md.tmpl",
|
"source_path": "browse/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||||
"target": "skills/qa/references/legacy/browse.md",
|
"target": "skills/qa/references/legacy/browse.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2798,7 +2798,7 @@
|
|||||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||||
"normalized_render_sha256": "e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6",
|
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2873,7 +2873,7 @@
|
|||||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||||
"normalized_render_sha256": "7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8",
|
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -2934,7 +2934,7 @@
|
|||||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||||
"normalized_render_sha256": "256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a",
|
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -4136,7 +4136,7 @@
|
|||||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||||
"disposition": "BUG_FIX",
|
"disposition": "BUG_FIX",
|
||||||
"overlays": [
|
"overlays": [
|
||||||
@@ -5775,7 +5775,7 @@
|
|||||||
"owner_tree": "qa",
|
"owner_tree": "qa",
|
||||||
"consumer_tree": "ship",
|
"consumer_tree": "ship",
|
||||||
"target": "skills/ship/references/legacy/canary.md",
|
"target": "skills/ship/references/legacy/canary.md",
|
||||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||||
"disposition": "SHARED_MODULE"
|
"disposition": "SHARED_MODULE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -57,18 +57,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mechanical_port": {
|
"mechanical_port": {
|
||||||
"rendered_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"rendered_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"differs_from_baseline": true,
|
"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."
|
"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": {
|
"candidate": {
|
||||||
"target_path": "skills/design/references/legacy/design-consultation.md",
|
"target_path": "skills/design/references/legacy/design-consultation.md",
|
||||||
"rendered_legacy_body_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"rendered_legacy_body_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"semantic_signature": {
|
"semantic_signature": {
|
||||||
"normalized_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
"normalized_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||||
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
|
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
|
||||||
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
|
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
|
||||||
"obligations_sha256": "c36974d06c0f66d0a0f2ea3d27282ed785c7cc956cce40a8c8db35492680c972",
|
"obligations_sha256": "fa0722c312610cfe74c70b1e29f447622b360cb894f3c697b3ca959c3a068ddb",
|
||||||
"heading_count": 12,
|
"heading_count": 12,
|
||||||
"question_count": 2,
|
"question_count": 2,
|
||||||
"obligation_count": 18
|
"obligation_count": 18
|
||||||
|
|||||||
@@ -57,18 +57,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mechanical_port": {
|
"mechanical_port": {
|
||||||
"rendered_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"rendered_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"differs_from_baseline": true,
|
"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."
|
"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": {
|
"candidate": {
|
||||||
"target_path": "skills/design/references/legacy/design-review.md",
|
"target_path": "skills/design/references/legacy/design-review.md",
|
||||||
"rendered_legacy_body_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"rendered_legacy_body_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"semantic_signature": {
|
"semantic_signature": {
|
||||||
"normalized_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
"normalized_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||||
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
|
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
|
||||||
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
|
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
|
||||||
"obligations_sha256": "e4fe261ecc3956834dfac15cac60fc4b8057ad0c3b37ede06ee4667d61852655",
|
"obligations_sha256": "3e3b3b8a8e43c0120d60f5517cc03aa64bb31bf50adbe418ba89a7ae63557a55",
|
||||||
"heading_count": 62,
|
"heading_count": 62,
|
||||||
"question_count": 24,
|
"question_count": 24,
|
||||||
"obligation_count": 63
|
"obligation_count": 63
|
||||||
|
|||||||
@@ -58,18 +58,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mechanical_port": {
|
"mechanical_port": {
|
||||||
"rendered_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"rendered_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"differs_from_baseline": true,
|
"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."
|
"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": {
|
"candidate": {
|
||||||
"target_path": "skills/plan/references/legacy/office-hours.md",
|
"target_path": "skills/plan/references/legacy/office-hours.md",
|
||||||
"rendered_legacy_body_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"rendered_legacy_body_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"semantic_signature": {
|
"semantic_signature": {
|
||||||
"normalized_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
"normalized_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||||
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
|
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
|
||||||
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
|
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
|
||||||
"obligations_sha256": "c6f9f220f431b50b7721ac9baa3ae3d7be5de88b489c80a45350028c96a9db84",
|
"obligations_sha256": "a8abcb960325c146cb555a71821d702b593bb7dc4f12646d4ecaea11a9cf6b68",
|
||||||
"heading_count": 34,
|
"heading_count": 34,
|
||||||
"question_count": 13,
|
"question_count": 13,
|
||||||
"obligation_count": 43
|
"obligation_count": 43
|
||||||
|
|||||||
@@ -57,18 +57,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mechanical_port": {
|
"mechanical_port": {
|
||||||
"rendered_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"rendered_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"differs_from_baseline": true,
|
"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."
|
"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": {
|
"candidate": {
|
||||||
"target_path": "skills/qa/references/legacy/qa.md",
|
"target_path": "skills/qa/references/legacy/qa.md",
|
||||||
"rendered_legacy_body_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"rendered_legacy_body_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"semantic_signature": {
|
"semantic_signature": {
|
||||||
"normalized_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
"normalized_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||||
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
|
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
|
||||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||||
"obligations_sha256": "c9a3696553c10b1338568d728f86f47c9a7a06b2defa37bd557c662d4a9086d8",
|
"obligations_sha256": "8c9e044c4224e4c35838d6ae9241405a5df42c2372c2ce272e0a11f37ab68c86",
|
||||||
"heading_count": 58,
|
"heading_count": 58,
|
||||||
"question_count": 6,
|
"question_count": 6,
|
||||||
"obligation_count": 65
|
"obligation_count": 65
|
||||||
|
|||||||
@@ -57,18 +57,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mechanical_port": {
|
"mechanical_port": {
|
||||||
"rendered_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"rendered_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"differs_from_baseline": true,
|
"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."
|
"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": {
|
"candidate": {
|
||||||
"target_path": "skills/qa/references/legacy/qa-only.md",
|
"target_path": "skills/qa/references/legacy/qa-only.md",
|
||||||
"rendered_legacy_body_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"rendered_legacy_body_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"semantic_signature": {
|
"semantic_signature": {
|
||||||
"normalized_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
"normalized_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||||
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
|
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
|
||||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||||
"obligations_sha256": "9ea973707574aa3750e38a2920e65072f804fe7cddf789f1710e08b0a44d92d3",
|
"obligations_sha256": "f16cce08f386155cae52758fb3f7235e1c8090ca7d84117caa4761b76927d9d5",
|
||||||
"heading_count": 34,
|
"heading_count": 34,
|
||||||
"question_count": 6,
|
"question_count": 6,
|
||||||
"obligation_count": 40
|
"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.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE");
|
||||||
if (parsed.flags.has("--rollback")) {
|
if (parsed.flags.has("--rollback")) {
|
||||||
if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE");
|
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, {
|
const pointer = await rollbackUpgrade(home, {
|
||||||
healthCheck: async (fallbackPath) => {
|
prepareActivation: async (fallbackPath) => ({
|
||||||
rollbackBrowserChoice = await resolvedBrowserChoiceForRuntimePath(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`);
|
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`);
|
||||||
return 0;
|
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);
|
add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details);
|
||||||
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
|
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
|
||||||
const launchers = manifest?.capabilities ?? {};
|
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)) {
|
if (!selected.has(capability)) {
|
||||||
add(`capability:${capability}`, "warn", "not selected");
|
add(`capability:${capability}`, "warn", "not selected");
|
||||||
continue;
|
continue;
|
||||||
@@ -158,8 +162,11 @@ export async function runDoctor(options = {}) {
|
|||||||
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
|
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (capability === "browser") {
|
if (capability === "browser" || capability === "browser-visible") {
|
||||||
const browser = runtimeConfig?.browser?.provider === "installed"
|
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(
|
? await inspectInstalledChromium(
|
||||||
activeRoot,
|
activeRoot,
|
||||||
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
|
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
|
||||||
@@ -167,7 +174,11 @@ export async function runDoctor(options = {}) {
|
|||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
: runtimeConfig?.browser?.provider === "managed"
|
: 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" };
|
: { 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);
|
add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details);
|
||||||
continue;
|
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" };
|
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 browserRoot = path.join(activeRoot, ".gstack-runtime-browsers");
|
||||||
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
|
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
|
||||||
const [browserStat, moduleStat] = await Promise.all([
|
const [browserStat, moduleStat] = await Promise.all([
|
||||||
@@ -260,11 +271,15 @@ async function inspectManagedChromium(activeRoot, nodeCommand) {
|
|||||||
const result = await captureCommand(nodeCommand, [
|
const result = await captureCommand(nodeCommand, [
|
||||||
"--input-type=module",
|
"--input-type=module",
|
||||||
"--eval",
|
"--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 } });
|
], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } });
|
||||||
const version = result.stdout.trim();
|
const version = result.stdout.trim();
|
||||||
if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" };
|
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) {
|
} catch (error) {
|
||||||
return { ok: false, message: `managed Chromium is not runnable: ${error.message}` };
|
return { ok: false, message: `managed Chromium is not runnable: ${error.message}` };
|
||||||
}
|
}
|
||||||
@@ -311,7 +326,7 @@ async function inspectXcrun() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function capabilityLaunchersReady(capability, launchers) {
|
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 === "design") return typeof launchers["gstack-design"] === "string";
|
||||||
if (capability === "pdf") return typeof launchers["make-pdf"] === "string";
|
if (capability === "pdf") return typeof launchers["make-pdf"] === "string";
|
||||||
if (capability === "diagram") return true;
|
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 browserBacked = relative.startsWith("browse/") || relative.startsWith("make-pdf/");
|
||||||
const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : [];
|
const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : [];
|
||||||
const runtimeComponents = Array.isArray(bundle?.runtimeComponents) ? bundle.runtimeComponents : [];
|
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 ?? (
|
const slotProvider = bundle?.browserChoice?.provider ?? (
|
||||||
runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible")
|
runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible")
|
||||||
? "managed"
|
? "managed"
|
||||||
@@ -1656,16 +1662,16 @@ if (browserBacked) {
|
|||||||
if (slotProvider && browserChoice.provider !== slotProvider) {
|
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");
|
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 (browserChoice.provider === "installed") {
|
||||||
if (selectedCapabilities.includes("browser-visible")) {
|
if (selectedCapabilities.includes("browser-visible")) {
|
||||||
throw new Error("Visible GStack Browser requires a managed Chromium runtime slot");
|
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) {
|
if (visibleRequested) {
|
||||||
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
|
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) {
|
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");
|
throw new Error("Invalid managed runtime transaction path");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ function validateTransactionPath(value) {
|
|||||||
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
|
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
|
||||||
}
|
}
|
||||||
const normalized = value.replaceAll("\\", "/");
|
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");
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+68
-2
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { assertPathInside, resolveRuntimePaths } from "./paths.js";
|
import { assertPathInside, resolveRuntimePaths } from "./paths.js";
|
||||||
import { atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
import { atomicWriteFile, atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
||||||
import {
|
import {
|
||||||
assertManagedHome,
|
assertManagedHome,
|
||||||
ensureManagedHome,
|
ensureManagedHome,
|
||||||
ensureManagedRuntimeDirectory,
|
ensureManagedRuntimeDirectory,
|
||||||
recoverRuntimeTransactionUnlocked,
|
recoverRuntimeTransactionUnlocked,
|
||||||
|
RUNTIME_TRANSACTION_FILE,
|
||||||
withRuntimeLifecycleLock,
|
withRuntimeLifecycleLock,
|
||||||
} from "./managed-home.js";
|
} from "./managed-home.js";
|
||||||
|
import { configSetBrowserChoice } from "./config.js";
|
||||||
import { errorWithCode as upgradeError } from "./errors.js";
|
import { errorWithCode as upgradeError } from "./errors.js";
|
||||||
import { currentIsoTimestamp as isoNow } from "./time.js";
|
import { currentIsoTimestamp as isoNow } from "./time.js";
|
||||||
|
|
||||||
@@ -214,6 +216,11 @@ export async function rollbackUpgrade(home, options = {}) {
|
|||||||
}
|
}
|
||||||
await assertTreeContainsNoLinks(fallbackPath);
|
await assertTreeContainsNoLinks(fallbackPath);
|
||||||
if (options.healthCheck) await options.healthCheck(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 = {
|
const rolledBack = {
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
status: "active",
|
status: "active",
|
||||||
@@ -222,11 +229,70 @@ export async function rollbackUpgrade(home, options = {}) {
|
|||||||
rolledBackFrom: pointer.current ?? null,
|
rolledBackFrom: pointer.current ?? null,
|
||||||
rolledBackAt: isoNow(options.now),
|
rolledBackAt: isoNow(options.now),
|
||||||
};
|
};
|
||||||
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
|
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;
|
return rolledBack;
|
||||||
}, options);
|
}, 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 = {}) {
|
export async function activeVersion(home, options = {}) {
|
||||||
const recovered = await recoverPendingUpgrade(home, options);
|
const recovered = await recoverPendingUpgrade(home, options);
|
||||||
return recovered.pointer;
|
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
|
if [ -z "$CHROMIUM_DIR" ]; then
|
||||||
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
||||||
echo "Run: bunx playwright install chromium"
|
echo "Run: bunx playwright-core install chromium"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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-next-version', '$GSTACK_BIN/gstack-next-version')
|
||||||
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
|
.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('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, '');
|
.replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, '');
|
||||||
|
|
||||||
body = body
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
<!-- 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_ROUTING replacement=$design --mode Generate --module design-consultation visibility=primary depth=deep mutation=design-artifacts web=optional -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=design-consultation -->
|
<!-- GSTACK2_LEGACY_BODY_START source=design-consultation -->
|
||||||
@@ -74,7 +74,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=design-html -->
|
||||||
@@ -167,7 +167,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=design-review -->
|
||||||
@@ -81,7 +81,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=office-hours -->
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module benchmark visibility=primary depth=standard mutation=report-only web=local-browser -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=benchmark -->
|
<!-- GSTACK2_LEGACY_BODY_START source=benchmark -->
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module browse visibility=internal depth=standard mutation=source-defined web=local-browser -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=browse -->
|
<!-- GSTACK2_LEGACY_BODY_START source=browse -->
|
||||||
@@ -37,7 +37,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module devex-review visibility=primary depth=deep mutation=report-only web=optional -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=devex-review -->
|
<!-- GSTACK2_LEGACY_BODY_START source=devex-review -->
|
||||||
@@ -71,7 +71,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=open-gstack-browser -->
|
||||||
@@ -43,7 +43,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module pair-agent visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=pair-agent -->
|
<!-- GSTACK2_LEGACY_BODY_START source=pair-agent -->
|
||||||
@@ -61,7 +61,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=qa-only -->
|
||||||
@@ -52,7 +52,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_ROUTING replacement=$qa --mode Fix --module qa visibility=primary depth=deep mutation=fix-safe web=local-browser -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=qa -->
|
<!-- GSTACK2_LEGACY_BODY_START source=qa -->
|
||||||
@@ -123,7 +123,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=setup-browser-cookies -->
|
||||||
@@ -61,7 +61,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
<!-- 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_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||||
|
|
||||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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. -->
|
<!-- 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_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 -->
|
<!-- GSTACK2_LEGACY_BODY_START source=land-and-deploy -->
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `NEEDS_SETUP`:
|
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`.
|
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.
|
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");
|
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||||
}
|
}
|
||||||
if (parsed.source) {
|
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") {
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
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");
|
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;
|
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|||||||
});
|
});
|
||||||
validateManifest(manifest, target);
|
validateManifest(manifest, target);
|
||||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
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);
|
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`);
|
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||||
else printComponentPlan(io.stdout, plan);
|
else printComponentPlan(io.stdout, plan);
|
||||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
|||||||
return applyBrowserProviderToComponents([...selected], 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) {
|
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||||
const components = selectedComponents(capabilities, browserChoice);
|
const components = selectedComponents(capabilities, browserChoice);
|
||||||
const retained = new Set(reusable?.components ?? []);
|
const retained = new Set(reusable?.components ?? []);
|
||||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
const stat = await fs.lstat(root);
|
const stat = await fs.lstat(root);
|
||||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
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;
|
!Array.isArray(bundle.files)) return null;
|
||||||
const components = [...new Set(bundle.runtimeComponents)];
|
const components = [...new Set(bundle.runtimeComponents)];
|
||||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
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);
|
await assertNoLinks(root);
|
||||||
const files = [];
|
const files = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
|||||||
await sha256File(file) !== entry.sha256) return null;
|
await sha256File(file) !== entry.sha256) return null;
|
||||||
files.push(relative);
|
files.push(relative);
|
||||||
}
|
}
|
||||||
return { root, components, files };
|
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
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 (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||||
if (options.version) args.push("--version", options.version);
|
if (options.version) args.push("--version", options.version);
|
||||||
if (options.prepared) args.push("--prepared");
|
if (options.prepared) args.push("--prepared");
|
||||||
|
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||||
await run(options.nodeCommand ?? process.execPath, args);
|
await run(options.nodeCommand ?? process.execPath, args);
|
||||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||||
return 0;
|
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 () => {
|
test("design-only launchers do not require an unrelated browser selection", async () => {
|
||||||
await withFixture(async ({ source, home }) => {
|
await withFixture(async ({ source, home }) => {
|
||||||
const design = path.join(source, "design", "dist", "design");
|
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 () => {
|
test("an installed-browser setup persists the choice only after activation and launches through it", async () => {
|
||||||
await withFixture(async ({ root, source, home }) => {
|
await withFixture(async ({ root, source, home }) => {
|
||||||
await fs.writeFile(path.join(source, "cap", "tool"), `#!/usr/bin/env node
|
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 () => {
|
test("a launcher repairs a crash journal before resolving any runtime", async () => {
|
||||||
await withFixture(async ({ source, home }) => {
|
await withFixture(async ({ source, home }) => {
|
||||||
await installFixture(source, home, "1.0.0");
|
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 pointer = await readJson(path.join(home, "versions", "current.json"));
|
||||||
const manifest = await fs.readFile(path.join(home, "runtime-install.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
|
// Keep the launcher for this host executable so it can enter the shared
|
||||||
// recovery path. The transaction restores the inactive host variant.
|
// recovery path. The transaction restores the inactive host variant.
|
||||||
const recoverableLauncher = process.platform === "win32" ? "gstack" : "gstack.cmd";
|
const recoverableLauncher = process.platform === "win32" ? "gstack" : "gstack.cmd";
|
||||||
const launcherPath = path.join(home, "bin", recoverableLauncher);
|
const launcherPath = path.join(home, "bin", recoverableLauncher);
|
||||||
const launcherBefore = await fs.readFile(launcherPath);
|
const launcherBefore = await fs.readFile(launcherPath);
|
||||||
await fs.writeFile(path.join(home, "runtime-install.json"), '{"activeVersion":"crashed"}\n');
|
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(launcherPath, "candidate launcher\n");
|
||||||
await fs.writeFile(path.join(home, "versions", "current.json"), `${JSON.stringify({
|
await fs.writeFile(path.join(home, "versions", "current.json"), `${JSON.stringify({
|
||||||
schemaVersion: 2,
|
schemaVersion: 2,
|
||||||
@@ -982,6 +1041,7 @@ process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
|
|||||||
previousPointerExists: true,
|
previousPointerExists: true,
|
||||||
previousPointer: pointer,
|
previousPointer: pointer,
|
||||||
files: [
|
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: "runtime-install.json", existed: true, mode: 0o600, dataBase64: manifest.toString("base64") },
|
||||||
{ path: `bin/${recoverableLauncher}`, existed: true, mode: 0o644, dataBase64: launcherBefore.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 });
|
const launched = await runInstalledLauncher(home, "gstack", ["doctor"], { capture: true });
|
||||||
expect(launched.stdout).toContain("gstack fixture doctor");
|
expect(launched.stdout).toContain("gstack fixture doctor");
|
||||||
expect(await readJson(path.join(home, "versions", "current.json"))).toEqual(pointer);
|
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(path.join(home, "runtime-install.json"))).toEqual(manifest);
|
||||||
expect(await fs.readFile(launcherPath)).toEqual(launcherBefore);
|
expect(await fs.readFile(launcherPath)).toEqual(launcherBefore);
|
||||||
expect(await exists(path.join(home, ".gstack-runtime-transaction.json"))).toBe(false);
|
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");
|
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 () => {
|
test("legacy YAML is read-only migration input and JSON takes authority on write", async () => {
|
||||||
const home = path.join(await root(), "legacy");
|
const home = path.join(await root(), "legacy");
|
||||||
await fs.mkdir(home);
|
await fs.mkdir(home);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import { PassThrough, Readable } from "node:stream";
|
import { PassThrough, Readable } from "node:stream";
|
||||||
import { runDoctor } from "../runtime/doctor.js";
|
import { runDoctor } from "../runtime/doctor.js";
|
||||||
import { runInstallerCli, runtimeSlotVersion, runtimeSurfaceForCapabilities } from "../runtime/install.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", () => {
|
describe("GStack runtime setup UX", () => {
|
||||||
test("capability selection keeps the core and excludes unselected heavyweight surfaces", () => {
|
test("capability selection keeps the core and excludes unselected heavyweight surfaces", () => {
|
||||||
const surface = runtimeSurfaceForCapabilities(["browser"]);
|
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 () => {
|
test("browser bootstrap options are local-only and browser preview requires an explicit choice", async () => {
|
||||||
const output = capture();
|
const output = capture();
|
||||||
let fetches = 0;
|
let fetches = 0;
|
||||||
@@ -332,6 +389,76 @@ describe("GStack runtime setup UX", () => {
|
|||||||
expect(fetches).toBe(1);
|
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 () => {
|
test("visible GStack Browser refuses installed Chrome before any network request", async () => {
|
||||||
const output = capture();
|
const output = capture();
|
||||||
let fetches = 0;
|
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 () => {
|
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 root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-installed-browser-"));
|
||||||
const home = path.join(root, "home");
|
const home = path.join(root, "home");
|
||||||
@@ -660,9 +824,50 @@ describe("GStack runtime setup UX", () => {
|
|||||||
expect(args).toContain("--yes");
|
expect(args).toContain("--yes");
|
||||||
expect(args).toContain("browser,diagram,pdf");
|
expect(args).toContain("browser,diagram,pdf");
|
||||||
expect(args).not.toContain("--prepared");
|
expect(args).not.toContain("--prepared");
|
||||||
|
expect(args).toContain("--replace-capabilities");
|
||||||
expect(output.value()).toContain("Developer-only source install");
|
expect(output.value()).toContain("Developer-only source install");
|
||||||
} finally {
|
} finally {
|
||||||
await fs.rm(root, { recursive: true, force: true });
|
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