implement six-skill gstack 2 runtime

This commit is contained in:
Sinabina
2026-07-17 11:08:14 -07:00
parent ce37bd36a9
commit b6572ebbb7
455 changed files with 108945 additions and 2622 deletions
+86
View File
@@ -0,0 +1,86 @@
import os from "node:os";
import path from "node:path";
/**
* Resolve the one and only gstack state root.
*
* This deliberately does not consult host-specific variables such as
* CLAUDE_PLUGIN_DATA. GSTACK_HOME wins; otherwise state lives in ~/.gstack.
* Values are handled as paths, never evaluated by a shell.
*/
export function resolveGstackHome(options = {}) {
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? os.homedir();
const cwd = options.cwd ?? process.cwd();
let configured = env.GSTACK_HOME;
if (configured != null && configured.includes("\0")) {
throw new TypeError("GSTACK_HOME must not contain a NUL byte");
}
if (configured == null || configured === "") {
if (!homeDir) throw new Error("Unable to resolve a home directory for ~/.gstack");
configured = path.join(homeDir, ".gstack");
} else if (configured === "~" || configured.startsWith(`~${path.sep}`)) {
if (!homeDir) throw new Error("Unable to expand ~ in GSTACK_HOME");
configured = path.join(homeDir, configured.slice(2));
}
return path.normalize(path.resolve(cwd, configured));
}
export function resolveRuntimePaths(options = {}) {
const home = options.home ?? resolveGstackHome(options);
return Object.freeze({
home,
config: path.join(home, "config.json"),
secrets: path.join(home, "secrets.json"),
migrations: path.join(home, "migration.json"),
projects: path.join(home, "projects"),
locks: path.join(home, "locks"),
tmp: path.join(home, "tmp"),
plans: path.join(home, "plans"),
versions: path.join(home, "versions"),
versionPointer: path.join(home, "versions", "current.json"),
});
}
/** POSIX-shell literal used only by the legacy gstack-paths adapter. */
export function shellQuote(value) {
return `'${String(value).replaceAll("'", `'\\''`)}'`;
}
export function projectPaths(home, projectId) {
assertSafeId(projectId, "project id");
const root = path.join(home, "projects", projectId);
return Object.freeze({
root,
state: path.join(root, "state.json"),
timeline: path.join(root, "timeline.jsonl"),
decisions: path.join(root, "decisions.jsonl"),
evidence: path.join(root, "evidence"),
artifacts: path.join(root, "artifacts"),
reviews: path.join(root, "reviews"),
checkpoints: path.join(root, "checkpoints"),
lock: path.join(root, ".state.lock"),
});
}
export function assertSafeId(value, label = "id") {
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9_-]{0,127}$/i.test(value)) {
throw new TypeError(`Invalid ${label}`);
}
return value;
}
/** Return candidate only when it is strictly inside root. */
export function assertPathInside(root, candidate) {
const base = path.resolve(root);
const target = path.resolve(candidate);
const relative = path.relative(base, target);
if (relative === "" || relative === ".") return target;
if (relative.startsWith(".." + path.sep) || relative === ".." || path.isAbsolute(relative)) {
throw new Error(`Path escapes gstack home: ${target}`);
}
return target;
}