feat: add /gsd update slash command for in-session self-update (#964)
The update banner already referenced `/gsd:update` but the command didn't exist. This adds `/gsd update` as a proper subcommand that checks the npm registry and runs `npm install -g gsd-pi@latest` when a newer version is available. - Register `update` in subcommand completions and help text - Add `handleUpdate()` that reuses `compareSemver` from update-check - Fix banner text from `/gsd:update` to `/gsd update` (space, not colon) - Add tests for completion registration and help description
This commit is contained in:
parent
99c3375f18
commit
add957ed31
3 changed files with 121 additions and 2 deletions
|
|
@ -77,7 +77,7 @@ function projectRoot(): string {
|
|||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("gsd", {
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel",
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
||||
getArgumentCompletions: (prefix: string) => {
|
||||
const subcommands = [
|
||||
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
||||
|
|
@ -113,6 +113,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
||||
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
||||
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
||||
{ cmd: "update", desc: "Update GSD to the latest version" },
|
||||
];
|
||||
const parts = prefix.trim().split(/\s+/);
|
||||
|
||||
|
|
@ -575,6 +576,11 @@ Examples:
|
|||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "update") {
|
||||
await handleUpdate(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "") {
|
||||
// Bare /gsd defaults to step mode
|
||||
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
||||
|
|
@ -630,6 +636,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|||
" /gsd migrate Upgrade .gsd/ structures to new format",
|
||||
" /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
|
||||
" /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
|
||||
" /gsd update Update GSD to the latest version via npm",
|
||||
];
|
||||
ctx.ui.notify(lines.join("\n"), "info");
|
||||
}
|
||||
|
|
@ -2091,3 +2098,48 @@ Examples:
|
|||
ctx.ui.notify("Failed to dispatch hook. Auto-mode may have been cancelled.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Self-update handler ────────────────────────────────────────────────────
|
||||
|
||||
async function handleUpdate(ctx: ExtensionCommandContext): Promise<void> {
|
||||
const { execSync } = await import("node:child_process");
|
||||
const { compareSemver } = await import("../../../update-check.js");
|
||||
|
||||
const NPM_PACKAGE = "gsd-pi";
|
||||
const current = process.env.GSD_VERSION || "0.0.0";
|
||||
|
||||
ctx.ui.notify(`Current version: v${current}\nChecking npm registry...`, "info");
|
||||
|
||||
let latest: string;
|
||||
try {
|
||||
latest = execSync(`npm view ${NPM_PACKAGE} version`, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
ctx.ui.notify("Failed to reach npm registry. Check your network connection.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (compareSemver(latest, current) <= 0) {
|
||||
ctx.ui.notify(`Already up to date (v${current}).`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
|
||||
|
||||
try {
|
||||
execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`Updated to v${latest}. Restart your GSD session to use the new version.`,
|
||||
"info",
|
||||
);
|
||||
} catch {
|
||||
ctx.ui.notify(
|
||||
`Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
src/resources/extensions/gsd/tests/update-command.test.ts
Normal file
67
src/resources/extensions/gsd/tests/update-command.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { registerGSDCommand } from "../commands.ts";
|
||||
|
||||
function createMockPi() {
|
||||
const commands = new Map<string, any>();
|
||||
return {
|
||||
registerCommand(name: string, options: any) {
|
||||
commands.set(name, options);
|
||||
},
|
||||
registerTool() {},
|
||||
registerShortcut() {},
|
||||
on() {},
|
||||
sendMessage() {},
|
||||
commands,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockCtx() {
|
||||
const notifications: { message: string; level: string }[] = [];
|
||||
return {
|
||||
notifications,
|
||||
ui: {
|
||||
notify(message: string, level: string) {
|
||||
notifications.push({ message, level });
|
||||
},
|
||||
custom: async () => {},
|
||||
},
|
||||
shutdown: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
test("/gsd update appears in subcommand completions", () => {
|
||||
const pi = createMockPi();
|
||||
registerGSDCommand(pi as any);
|
||||
|
||||
const gsd = pi.commands.get("gsd");
|
||||
assert.ok(gsd, "registerGSDCommand should register /gsd");
|
||||
|
||||
const completions = gsd.getArgumentCompletions("update");
|
||||
const updateEntry = completions.find((c: any) => c.value === "update");
|
||||
assert.ok(updateEntry, "update should appear in completions");
|
||||
assert.equal(updateEntry.label, "update");
|
||||
});
|
||||
|
||||
test("/gsd update appears in help description", () => {
|
||||
const pi = createMockPi();
|
||||
registerGSDCommand(pi as any);
|
||||
|
||||
const gsd = pi.commands.get("gsd");
|
||||
assert.ok(gsd?.description?.includes("update"), "description should mention update");
|
||||
});
|
||||
|
||||
test("/gsd update is listed in completions with correct description", () => {
|
||||
const pi = createMockPi();
|
||||
registerGSDCommand(pi as any);
|
||||
|
||||
const gsd = pi.commands.get("gsd");
|
||||
const completions = gsd.getArgumentCompletions("");
|
||||
const updateEntry = completions.find((c: any) => c.value === "update");
|
||||
assert.ok(updateEntry, "update should appear in full completion list");
|
||||
assert.ok(
|
||||
updateEntry.description.toLowerCase().includes("update"),
|
||||
"completion description should mention updating",
|
||||
);
|
||||
});
|
||||
|
|
@ -50,7 +50,7 @@ export function writeUpdateCache(cache: UpdateCheckCache, cachePath: string = CA
|
|||
function printUpdateBanner(current: string, latest: string): void {
|
||||
process.stderr.write(
|
||||
` ${chalk.yellow('Update available:')} ${chalk.dim(`v${current}`)} → ${chalk.bold(`v${latest}`)}\n` +
|
||||
` ${chalk.dim('Run')} npm update -g gsd-pi ${chalk.dim('or')} /gsd:update ${chalk.dim('to upgrade')}\n\n`,
|
||||
` ${chalk.dim('Run')} npm update -g gsd-pi ${chalk.dim('or')} /gsd update ${chalk.dim('to upgrade')}\n\n`,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue