feat(gstack2): add capability readiness process

This commit is contained in:
Sinabina
2026-07-20 16:23:38 -07:00
parent a1b2b05a18
commit 2e1e52eae7
12 changed files with 324 additions and 8 deletions
+19 -6
View File
@@ -25,7 +25,13 @@ import {
runExternalEffect,
updateRunWorkflow,
} from "./state.js";
import { runDoctor, formatDoctor } from "./doctor.js";
import {
CAPABILITY_READINESS_CAPABILITIES,
capabilityReadiness,
formatCapabilityReadiness,
runDoctor,
formatDoctor,
} from "./doctor.js";
import { cleanupRuntime } from "./cleanup.js";
import {
ContextClient,
@@ -157,11 +163,18 @@ async function initCommand({ args, home, cwd, stdout, legacyAlias = false }) {
}
async function doctorCommand({ args, home, cwd, stdout }) {
const parsed = parseFlags(args, new Set(["--json", "--skill-api"]));
const parsed = parseFlags(args, new Set(["--json", "--skill-api", "--capability"]));
if (parsed.positionals.length) throw cliError("Doctor accepts only named options", "USAGE");
const capability = parsed.values.get("--capability");
if (capability && !CAPABILITY_READINESS_CAPABILITIES.includes(capability)) {
throw cliError(`Unknown capability: ${capability}. Choose ${CAPABILITY_READINESS_CAPABILITIES.join(", ")}.`, "USAGE");
}
const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") });
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(report, null, 2)}\n` : formatDoctor(report));
return report.ok ? 0 : 1;
const result = capability ? capabilityReadiness(report, capability) : report;
write(stdout, parsed.flags.has("--json")
? `${JSON.stringify(result, null, 2)}\n`
: capability ? formatCapabilityReadiness(result) : formatDoctor(result));
return result.ok ? 0 : 1;
}
async function configCommand({ args, home, cwd, stdout }) {
@@ -673,7 +686,7 @@ function parseFlags(args, allowed) {
continue;
}
if (!allowed.has(arg)) throw cliError(`Unknown option: ${arg}`, "USAGE");
if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api"].includes(arg)) {
if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api", "--capability"].includes(arg)) {
const value = args[++index];
if (value == null || value.startsWith("--")) throw cliError(`${arg} requires a value`, "USAGE");
values.set(arg, value);
@@ -750,7 +763,7 @@ function usage() {
"Usage:\n" +
" gstack init # initialize state only\n" +
" gstack setup # compatibility alias for init\n" +
" gstack doctor [--skill-api <version>] [--json]\n" +
" gstack doctor [--capability browser|design|diagram|pdf|ios] [--skill-api <version>] [--json]\n" +
" gstack paths [--json|--shell]\n" +
" gstack runtime path <bundle-relative-path>\n" +
" gstack config get [key]\n" +
+97
View File
@@ -17,6 +17,14 @@ import {
managedBunRelativePath,
} from "./install.js";
export const CAPABILITY_READINESS_CAPABILITIES = Object.freeze([
"browser",
"design",
"diagram",
"pdf",
"ios",
]);
export async function runDoctor(options = {}) {
const paths = resolveRuntimePaths(options);
const checks = [];
@@ -183,6 +191,82 @@ export async function runDoctor(options = {}) {
};
}
export function capabilityReadiness(report, capability, options = {}) {
if (!CAPABILITY_READINESS_CAPABILITIES.includes(capability)) {
throw new TypeError(`Unknown capability: ${capability}`);
}
const platform = options.platform ?? process.platform;
const checkedAt = report.checkedAt;
const judgment = {
status: "available",
message: "Pure-judgment skill guidance is available without the optional runtime.",
};
if (capability === "ios" && platform !== "darwin") {
return {
ok: false,
capability,
checkedAt,
judgment,
platform: { status: "unsupported", platform, message: "Physical iOS requires macOS and the existing CoreDevice harness." },
consent: {
preview: { status: "not-applicable", granted: false },
install: { status: "not-applicable", granted: false },
},
readiness: { status: "unsupported", message: "The runtime capability is unsupported on this platform." },
nextAction: "Continue with pure judgment or move the physical-iOS workflow to macOS.",
};
}
const capabilityCheck = report.checks.find((check) => check.id === `capability:${capability}`);
const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime");
let status;
if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready";
else if (capabilityCheck?.status === "pass") status = "degraded";
else if (capabilityCheck?.status === "fail") status = "failed";
else status = "unavailable";
const needsSetup = status === "unavailable" || status === "failed";
const messages = {
ready: "The selected capability passed its runtime readiness checks.",
degraded: "The capability check passed, but the managed runtime reported a degraded condition.",
unavailable: "The optional runtime capability is not installed or cannot currently be inspected.",
failed: "The selected capability is installed but failed readiness checks.",
};
return {
ok: status === "ready" || status === "degraded",
capability,
checkedAt,
judgment,
platform: { status: "supported", platform },
consent: {
preview: {
status: needsSetup ? "required" : "not-required",
granted: false,
message: needsSetup
? "Consent is required before an uncached signed-manifest metadata preview; this command does not grant it."
: "No setup preview is needed for the current readiness state.",
},
install: {
status: needsSetup ? "required-after-preview" : "not-required",
granted: false,
message: needsSetup
? "Install consent is separate and may be requested only after the complete preview; this command does not grant it."
: "No install is needed for the current readiness state.",
},
},
readiness: {
status,
message: messages[status],
evidence: [runtimeCheck, capabilityCheck].filter(Boolean),
},
nextAction: needsSetup
? `Ask for preview consent, then run the packaged bootstrap preview for ${capability}; continue judgment-only work if setup is deferred.`
: status === "degraded"
? "Review the managed-runtime warning before capability-dependent evidence work."
: "Proceed with capability-dependent work.",
};
}
async function inspectRuntimeBun(activeRoot, manifest) {
const relative = managedBunRelativePath();
const declared = manifest?.tools?.bun;
@@ -328,3 +412,16 @@ export function formatDoctor(report) {
for (const check of report.checks) lines.push(`${symbol[check.status]} ${check.id}: ${check.message}`);
return `${lines.join("\n")}\n`;
}
export function formatCapabilityReadiness(report) {
const lines = [
`gstack capability readiness: ${report.capability}`,
`judgment: ${report.judgment.status}`,
`platform: ${report.platform.status}`,
`preview consent: ${report.consent.preview.status}`,
`install consent: ${report.consent.install.status}`,
`readiness: ${report.readiness.status}`,
`next: ${report.nextAction}`,
];
return `${lines.join("\n")}\n`;
}