mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
fix: harden browser provider activation
This commit is contained in:
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-consultation/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=64af56ecdbd132cb7c28344e8e4ecb2e5dacf811 baseline_render_sha256=62b8141e0b3edb26dcfd175c25c7021d4713b64add121137ace0a123e6e6ea8a ported_render_sha256=13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Generate --module design-consultation visibility=primary depth=deep mutation=design-artifacts web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-consultation -->
|
||||
@@ -74,7 +74,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-html/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=3cdec9a14d62d2e046ed924c972efc30a7d43aca baseline_render_sha256=d16ec32f4c07da49d32efc309e621b514854ce355db8347647b9f9fc215ff66d ported_render_sha256=775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-html visibility=primary depth=standard mutation=design-artifacts web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-html -->
|
||||
@@ -167,7 +167,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=design-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=bdcda48e29b489a1cc49faa333922412251d4b41 baseline_render_sha256=ff6d5d4858ed45db1e9581080739c0b4c5029bca44ecabe0c385637ece68e0cb ported_render_sha256=9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$design --mode Implement --module design-review visibility=primary depth=deep mutation=fix-safe web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=design-review -->
|
||||
@@ -81,7 +81,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=office-hours/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=8568fe73cca76a80805fab3092cacd10db7e1d7f baseline_render_sha256=5af4dc503ee149ac5052617ec4d5ad1947c9fbf28c663f40457b0eb07f5fcea3 ported_render_sha256=ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$plan --mode Discovery --module office-hours visibility=primary depth=deep mutation=design-doc-only web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=office-hours -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=benchmark/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=038f16f5fba4ae4e9eae922e3276bba8ef88149e baseline_render_sha256=c1a8019b9b430790f0917df8d58e7a645f01f2784398343d64e5505b535c1ea7 ported_render_sha256=5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module benchmark visibility=primary depth=standard mutation=report-only web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=benchmark -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=browse/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=9a159e4c9820172c229e2174d4a62a8f9668ab93 baseline_render_sha256=26c248b90f91a99d1e31e51afaec46385941ab2071bfab7bfdf6d044151ab3ac ported_render_sha256=fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module browse visibility=internal depth=standard mutation=source-defined web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=browse -->
|
||||
@@ -37,7 +37,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=devex-review/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=081d4f35bbdec0c6b3da8ae71615ec4d41a84551 baseline_render_sha256=d070b5d50c0b8a59efc7be04881734419f815f01b065ee9d15cf151dba9afb18 ported_render_sha256=6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module devex-review visibility=primary depth=deep mutation=report-only web=optional -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=devex-review -->
|
||||
@@ -71,7 +71,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=e6e8271ecd89761627e6e67745750b22e64596d0e51e4a2350dccd8e2ce8ebd6 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=open-gstack-browser/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=ef91a527890a3ac3622cc7dc84bad1ff7b64443b baseline_render_sha256=f68b483619f37175687c64510c4de5c718ad3aa2d644134f6539d28df3a9ad7c ported_render_sha256=f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module open-gstack-browser visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=open-gstack-browser -->
|
||||
@@ -43,7 +43,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=256fd576911cc286ddd2510daec8f4c68501cc5534f46edc044c1908574ac64a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=pair-agent/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75ed42d590f99c46cd0883c37bb1f2f9f499211c baseline_render_sha256=6bb659c03b5df7c36f446fad30aaec4ab6d5e0d25fb8392702573e66923b02fb ported_render_sha256=e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module pair-agent visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=pair-agent -->
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa-only/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=75c4123cc5c406ffdd36c71a094335c137135b1e baseline_render_sha256=7f8c42379e748156bf131a5bae121ab9a3f307e57619e261f5db7865eead3029 ported_render_sha256=601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module qa-only visibility=primary depth=deep mutation=report-only web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=qa-only -->
|
||||
@@ -52,7 +52,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=qa/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=11997f7b878282c34b6bfd3d4b7a8131f9ad4da8 baseline_render_sha256=07e2c6a841c6701d186b3b6536cfdb87af49566029971502065f636576ba071c ported_render_sha256=63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Fix --module qa visibility=primary depth=deep mutation=fix-safe web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=qa -->
|
||||
@@ -123,7 +123,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=7d539b2113f8cc9bf0b8b2f6e1da3dde7028176a6f71de8f47de0a98c45663e8 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=setup-browser-cookies/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=f812d9f56f27c32fb5f102083bbe418344c1a652 baseline_render_sha256=22b03503fa8ba63de98866d64ab0563291f4b41e1577c8022d09add3e0bdb59c ported_render_sha256=9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module setup-browser-cookies visibility=internal depth=standard mutation=configuration web=local-browser -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=setup-browser-cookies -->
|
||||
@@ -61,7 +61,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=canary/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=d1eb2950aba2fa2b09d90f13143492c60d46793c baseline_render_sha256=8dd0ff918566e1c5536f1bfcc546eebbdbee5e43d0a05b14a8efb898d3574dbe ported_render_sha256=b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$qa --mode Report --module canary visibility=primary depth=deep mutation=report-only web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=canary -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
|
||||
<!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_PROVENANCE source=land-and-deploy/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=98976ad020d541d251cc7e34802a13458ddc88e2 baseline_render_sha256=be77d9332d68281785eb2daf1d094f53bad537dfa282a4abb8638aca398cd2b9 ported_render_sha256=405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2 disposition=BUG_FIX -->
|
||||
<!-- GSTACK2_ROUTING replacement=$ship --mode Land --module land-and-deploy visibility=primary depth=deep mutation=merge-deploy web=production -->
|
||||
|
||||
<!-- GSTACK2_LEGACY_BODY_START source=land-and-deploy -->
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
```
|
||||
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.
|
||||
1. Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.
|
||||
2. Read `references/RUNTIME.md` and follow its explicit capability bootstrap. Never assume a standard-installed skill directory contains `./setup`.
|
||||
3. The approved managed runtime includes its own pinned Bun at `$GSTACK_BIN/bun`; never download or install another Bun from a skill workflow.
|
||||
|
||||
|
||||
@@ -123,13 +123,36 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
if (parsed.source) {
|
||||
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
if (parsed.action === "preview") {
|
||||
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
|
||||
return 0;
|
||||
}
|
||||
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
|
||||
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
|
||||
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false, browserChoice });
|
||||
return await installFromSource(parsed.source, parsed, {
|
||||
...options,
|
||||
...io,
|
||||
prepared: false,
|
||||
replaceCapabilities: true,
|
||||
browserChoice,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_ = options.fetch ?? globalThis.fetch;
|
||||
@@ -146,7 +169,23 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
|
||||
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
const reusable = active?.releaseMatches ? active : null;
|
||||
if (!browserChoice && active?.browserChoice) {
|
||||
browserChoice = await resolveBrowserChoice(active.browserChoice, {
|
||||
platform,
|
||||
env: options.env,
|
||||
homeDir: options.homeDir,
|
||||
});
|
||||
}
|
||||
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
|
||||
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
|
||||
throw bootstrapError(
|
||||
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
|
||||
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
|
||||
);
|
||||
}
|
||||
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
|
||||
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
|
||||
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
@@ -300,6 +339,24 @@ function selectedComponents(capabilities, browserChoice) {
|
||||
return applyBrowserProviderToComponents([...selected], browserChoice);
|
||||
}
|
||||
|
||||
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
|
||||
const selected = new Set([
|
||||
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
|
||||
...requested,
|
||||
]);
|
||||
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
|
||||
const pending = [...selected];
|
||||
while (pending.length) {
|
||||
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
|
||||
if (!selected.has(dependency)) {
|
||||
selected.add(dependency);
|
||||
pending.push(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...selected].sort();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
@@ -341,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
|
||||
const stat = await fs.lstat(root);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
|
||||
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) ||
|
||||
const releaseMatches = bundle?.version === version ||
|
||||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
|
||||
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
|
||||
!Array.isArray(bundle.files)) return null;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
|
||||
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
|
||||
? [...new Set(bundle.selectedCapabilities)]
|
||||
: [];
|
||||
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
|
||||
let browserChoice = null;
|
||||
if (browserChoiceRequired(selectedCapabilities)) {
|
||||
const explicit = bundle.browserChoice;
|
||||
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
|
||||
if (explicit.provider === "installed") {
|
||||
if (selectedCapabilities.includes("browser-visible") ||
|
||||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
|
||||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
|
||||
} else {
|
||||
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
|
||||
browserChoice = { provider: "managed", executablePath: null };
|
||||
}
|
||||
}
|
||||
await assertNoLinks(root);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -360,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
async function seedReusableRuntime(reusable, destination, claimedFiles) {
|
||||
@@ -496,6 +574,7 @@ async function installFromSource(source, parsed, options) {
|
||||
if (parsed.home) args.push("--home", path.resolve(parsed.home));
|
||||
if (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user