mirror of https://github.com/garrytan/gstack.git
252 lines
7.4 KiB
JavaScript
252 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createRequire } from "node:module";
|
|
|
|
process.stdout.on("error", (err: NodeJS.ErrnoException) => {
|
|
if (err.code === "EPIPE") process.exit(0);
|
|
throw err;
|
|
});
|
|
import { runWizard } from "./wizard.js";
|
|
import { installGlobal } from "./commands/install.js";
|
|
import { initProject } from "./commands/init.js";
|
|
import { uninstall } from "./commands/uninstall.js";
|
|
import { upgrade } from "./commands/upgrade.js";
|
|
import { doctor } from "./commands/doctor.js";
|
|
import { status } from "./commands/status.js";
|
|
import { list } from "./commands/list.js";
|
|
import { enable, disable } from "./commands/toggle.js";
|
|
import { HOSTS, type HostId, hostById } from "./lib/hosts.js";
|
|
import { createLogger, colors } from "./lib/logger.js";
|
|
|
|
interface ParsedArgs {
|
|
positional: string[];
|
|
flags: Record<string, string | boolean>;
|
|
list: Record<string, string[]>;
|
|
}
|
|
|
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
const positional: string[] = [];
|
|
const flags: Record<string, string | boolean> = {};
|
|
const list: Record<string, string[]> = {};
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
if (arg.startsWith("--")) {
|
|
const eq = arg.indexOf("=");
|
|
let key: string;
|
|
let value: string | undefined;
|
|
if (eq !== -1) {
|
|
key = arg.slice(2, eq);
|
|
value = arg.slice(eq + 1);
|
|
} else {
|
|
key = arg.slice(2);
|
|
const next = argv[i + 1];
|
|
if (next !== undefined && !next.startsWith("-")) {
|
|
value = next;
|
|
i++;
|
|
}
|
|
}
|
|
if (value === undefined) {
|
|
flags[key] = true;
|
|
} else if (key === "host") {
|
|
list.host = list.host ?? [];
|
|
list.host.push(...value.split(",").map((s) => s.trim()).filter(Boolean));
|
|
} else {
|
|
flags[key] = value;
|
|
}
|
|
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
const key = arg.slice(1);
|
|
flags[key] = true;
|
|
} else {
|
|
positional.push(arg);
|
|
}
|
|
}
|
|
|
|
return { positional, flags, list };
|
|
}
|
|
|
|
function parseHosts(args: ParsedArgs): HostId[] {
|
|
const raw = args.list.host ?? [];
|
|
const hosts: HostId[] = [];
|
|
for (const r of raw) {
|
|
if (r === "auto") {
|
|
return ["claude"];
|
|
}
|
|
const meta = hostById(r);
|
|
if (!meta) {
|
|
console.error(colors.red(`Unknown host: ${r}`));
|
|
console.error(`Valid: ${HOSTS.map((h) => h.id).join(", ")}`);
|
|
process.exit(2);
|
|
}
|
|
hosts.push(meta.id);
|
|
}
|
|
return hosts;
|
|
}
|
|
|
|
function bool(args: ParsedArgs, name: string, fallback: boolean): boolean {
|
|
const v = args.flags[name];
|
|
if (v === undefined) return fallback;
|
|
if (typeof v === "boolean") return v;
|
|
if (v === "true" || v === "1" || v === "yes") return true;
|
|
if (v === "false" || v === "0" || v === "no") return false;
|
|
return fallback;
|
|
}
|
|
|
|
function getVersion(): string {
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const pkg = require("../package.json") as { version: string };
|
|
return pkg.version;
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
const HELP = `${colors.bold("gstack")} — installer for Garry Tan's gstack skill pack
|
|
|
|
${colors.bold("Usage:")}
|
|
npx @garrytan/gstack interactive wizard
|
|
npx @garrytan/gstack <command> [opts]
|
|
|
|
${colors.bold("Commands:")}
|
|
install Install gstack globally (~/.claude/skills/gstack)
|
|
init Add gstack to the current project (team mode)
|
|
uninstall Remove gstack (global; add --project for just this repo)
|
|
upgrade Pull latest gstack and rebuild
|
|
doctor Diagnose install issues
|
|
status Show install version, hosts, and settings
|
|
list List available skills
|
|
enable <name> Enable a skill in the current project
|
|
disable <name> Disable a skill in the current project
|
|
|
|
${colors.bold("Common options:")}
|
|
--host <id> Register with host (repeatable, comma-separated).
|
|
Valid: ${HOSTS.map((h) => h.id).join(", ")}
|
|
--prefix Use gstack-* skill names
|
|
--no-prefix Use flat skill names (default)
|
|
--no-claude-md Don't write gstack section to CLAUDE.md
|
|
--yes, -y Skip confirmation prompts
|
|
--reinstall Remove existing install before installing
|
|
--quiet, -q Suppress non-essential output
|
|
--tier <t> init only: "required" or "optional" (default: required)
|
|
--no-commit init only: stage but don't commit changes
|
|
--project uninstall only: remove from current project, not global
|
|
--keep-claude-md uninstall only: leave CLAUDE.md section in place
|
|
|
|
${colors.bold("Examples:")}
|
|
npx @garrytan/gstack install --host claude,codex
|
|
npx @garrytan/gstack init --tier optional
|
|
npx @garrytan/gstack uninstall --project --yes
|
|
npx @garrytan/gstack doctor
|
|
`;
|
|
|
|
async function main(): Promise<void> {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const quiet = bool(args, "quiet", bool(args, "q", false));
|
|
|
|
if (args.flags.version || args.flags.v) {
|
|
console.log(getVersion());
|
|
return;
|
|
}
|
|
if (args.flags.help || args.flags.h) {
|
|
console.log(HELP);
|
|
return;
|
|
}
|
|
|
|
const cmd = args.positional[0];
|
|
|
|
if (!cmd) {
|
|
await runWizard();
|
|
return;
|
|
}
|
|
|
|
const hosts = parseHosts(args);
|
|
const prefix = args.flags.prefix === true ? true : args.flags["no-prefix"] === true ? false : false;
|
|
const writeClaudeMd = !bool(args, "no-claude-md", false);
|
|
const yes = bool(args, "yes", bool(args, "y", false));
|
|
const reinstall = bool(args, "reinstall", false);
|
|
|
|
switch (cmd) {
|
|
case "install":
|
|
await installGlobal({
|
|
hosts: hosts.length > 0 ? hosts : (["claude"] as HostId[]),
|
|
prefix,
|
|
writeClaudeMd,
|
|
quiet,
|
|
reinstall,
|
|
});
|
|
break;
|
|
case "init": {
|
|
const tierFlag = typeof args.flags.tier === "string" ? args.flags.tier : "required";
|
|
if (tierFlag !== "required" && tierFlag !== "optional") {
|
|
console.error(colors.red(`Invalid --tier: ${tierFlag} (expected "required" or "optional")`));
|
|
process.exit(2);
|
|
}
|
|
await initProject({
|
|
tier: tierFlag,
|
|
commit: !bool(args, "no-commit", false),
|
|
quiet,
|
|
writeClaudeMd,
|
|
globalArgs: {
|
|
hosts: hosts.length > 0 ? hosts : (["claude"] as HostId[]),
|
|
prefix,
|
|
writeClaudeMd,
|
|
quiet,
|
|
reinstall,
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
case "uninstall":
|
|
await uninstall({
|
|
project: bool(args, "project", false),
|
|
yes,
|
|
keepClaudeMd: bool(args, "keep-claude-md", false),
|
|
quiet,
|
|
});
|
|
break;
|
|
case "upgrade":
|
|
await upgrade({ quiet });
|
|
break;
|
|
case "doctor":
|
|
await doctor({ quiet });
|
|
break;
|
|
case "status":
|
|
await status({ quiet });
|
|
break;
|
|
case "list":
|
|
await list({ quiet });
|
|
break;
|
|
case "enable": {
|
|
const name = args.positional[1];
|
|
if (!name) {
|
|
console.error(colors.red("Usage: gstack enable <skill>"));
|
|
process.exit(2);
|
|
}
|
|
await enable({ skillName: name, quiet });
|
|
break;
|
|
}
|
|
case "disable": {
|
|
const name = args.positional[1];
|
|
if (!name) {
|
|
console.error(colors.red("Usage: gstack disable <skill>"));
|
|
process.exit(2);
|
|
}
|
|
await disable({ skillName: name, quiet });
|
|
break;
|
|
}
|
|
default: {
|
|
const log = createLogger(false);
|
|
log.error(`Unknown command: ${cmd}`);
|
|
console.log("");
|
|
console.log(HELP);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
const log = createLogger(false);
|
|
log.error(err instanceof Error ? err.message : String(err));
|
|
process.exit(1);
|
|
});
|