@specforge/cli 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=update-check.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/update-check.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BpC;;GAEG;AACH,QAAA,MAAM,WAAW,EAAE,MAAoB,CAAC;AAqKxC;;GAEG;AACH,QAAA,MAAM,OAAO,SAAkB,CAAC;AAShC;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,IAAI,CAa1D;AAED;;GAEG;AACH,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC;;GAEG;AACH,QAAA,MAAM,WAAW,EAAE,MAAoB,CAAC;AAqKxC;;GAEG;AACH,QAAA,MAAM,OAAO,SAAkB,CAAC;AAShC;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,IAAI,CAkB1D;AAED;;GAEG;AACH,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,CAAC"}
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { resolveConfig } from "./config/loader.js";
6
6
  import { colors } from "./ui/index.js";
7
7
  import { printBanner } from "./ui/banner.js";
8
8
  import { CHANNEL } from "../channel.js";
9
+ import { notifyUpdate } from "./update-check.js";
9
10
  const BANNER_SKIP_COMMANDS = /* @__PURE__ */ new Set(["init", "login", "serve"]);
10
11
  const require2 = createRequire(import.meta.url);
11
12
  const pkg = require2("../../package.json");
@@ -127,6 +128,7 @@ registerServeCommand(program);
127
128
  registerCommands(program);
128
129
  configureHelp(program);
129
130
  function runCLI(argv = process.argv) {
131
+ notifyUpdate({ current: CLI_VERSION, isJson: argv.includes("--json") });
130
132
  try {
131
133
  program.parse(argv);
132
134
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["/**\n * SpecForge MCP CLI Entry Point\n *\n * Main CLI application using Commander.js.\n * Sets up command routing, middleware, and global options.\n */\n\nimport { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { authGuardHook } from './middleware/auth-guard.js';\nimport { withErrorHandler, debugLog, handleError } from './middleware/error-handler.js';\nimport { resolveConfig } from './config/loader.js';\nimport { colors } from './ui/index.js';\nimport { printBanner } from './ui/banner.js';\nimport { CHANNEL } from '../channel.js';\n\nconst BANNER_SKIP_COMMANDS = new Set(['init', 'login', 'serve']);\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../../package.json');\nimport {\n registerLoginCommand,\n registerInitCommand,\n registerConfigureCommand,\n registerDoctorCommand,\n registerStatusCommand,\n registerSwitchCommand,\n registerFeedbackCommand,\n registerMembersCommand,\n registerInvitationsCommand,\n} from './commands/index.js';\nimport { registerDebugCommands } from './commands/debug/index.js';\nimport { registerScaffoldCommand } from './commands/scaffold/index.js';\n\n/**\n * CLI version - read from package.json\n */\nconst CLI_VERSION: string = pkg.version;\n\n/**\n * CLI name for display — varies per build channel (`specforge` vs.\n * `specforge-canary`). The `SpecForge` brand text in banners stays fixed.\n */\nconst CLI_NAME = CHANNEL.name;\n\n/**\n * Create and configure the CLI program\n */\nfunction createProgram(): Command {\n const program = new Command();\n\n // Basic program info\n program\n .name(CLI_NAME)\n .description('SpecForge MCP CLI - Configure and interact with SpecForge')\n .version(CLI_VERSION, '-v, --version', 'Display version number');\n\n // Global options\n program\n .option('--debug', 'Enable debug output')\n .option('--no-color', 'Disable colored output')\n .option('--json', 'Output in JSON format');\n\n // Pre-action hook for auth guard\n program.hook('preAction', (thisCommand, actionCommand) => {\n const commandName = actionCommand.name();\n\n // Set debug mode from flag\n if (thisCommand.opts().debug) {\n process.env.SPECFORGE_DEBUG = 'true';\n }\n\n // Disable colors if requested\n if (thisCommand.opts().noColor) {\n // chalk.level = 0 is handled in colors module\n process.env.NO_COLOR = '1';\n }\n\n // Compact brand header for human-facing commands\n const jsonOutput = !!thisCommand.opts().json;\n const programState = thisCommand as Command & { _bannerPrinted?: boolean };\n if (\n !programState._bannerPrinted &&\n process.stdout.isTTY &&\n !jsonOutput &&\n !BANNER_SKIP_COMMANDS.has(commandName)\n ) {\n printBanner('compact');\n programState._bannerPrinted = true;\n }\n\n debugLog(`Running command: ${commandName}`);\n debugLog('Resolved config:', resolveConfig());\n\n // Run auth guard\n authGuardHook({ name: () => commandName });\n });\n\n return program;\n}\n\n/**\n * Register the default serve command (MCP server mode)\n *\n * This is the hidden default when no subcommand is provided.\n * Maintains backward compatibility with direct MCP server usage.\n */\nfunction registerServeCommand(program: Command): void {\n program\n .command('serve', { isDefault: true, hidden: true })\n .description('Start MCP server (default)')\n .action(withErrorHandler(async () => {\n debugLog('Starting MCP server');\n\n // Dynamic import to avoid loading server code for CLI commands\n const { loadConfig, validateConfig } = await import('../config/index.js');\n const { createServer, startServer } = await import('../server.js');\n\n // Load and validate config\n const config = await loadConfig();\n validateConfig(config);\n // Honor the resolved MCP output format (env > project > global > default).\n config.mcpOutputFormat = resolveConfig().mcpOutputFormat;\n\n // Create and start server\n const server = await createServer(config);\n await startServer(server);\n }));\n}\n\n/**\n * Register all CLI commands\n */\nfunction registerCommands(program: Command): void {\n registerLoginCommand(program);\n registerInitCommand(program);\n registerConfigureCommand(program);\n registerDoctorCommand(program);\n registerStatusCommand(program);\n registerSwitchCommand(program);\n registerDebugCommands(program);\n registerScaffoldCommand(program);\n registerFeedbackCommand(program);\n registerMembersCommand(program);\n registerInvitationsCommand(program);\n}\n\n/**\n * Configure help output styling\n */\nfunction configureHelp(program: Command): void {\n program.configureHelp({\n sortSubcommands: false,\n subcommandTerm: (cmd) => cmd.name(),\n });\n\n // Custom help with organized command groups\n program.addHelpText('after', `\n${colors.muted('Command Groups:')}\n\n ${colors.bold('Authentication')}\n login Authenticate with SpecForge API\n configure Read/write configuration\n\n ${colors.bold('Setup')}\n init Initialize SpecForge in a project\n scaffold Generate skills, agents, and commands\n\n ${colors.bold('Diagnostics')}\n doctor Health check\n status Project status and session info\n\n ${colors.bold('Context')}\n switch Switch active project or specification\n\n ${colors.bold('Collaboration')}\n members Manage project members\n invitations Manage invitations\n\n ${colors.bold('Debug')}\n debug call Direct MCP tool dispatch\n debug tools List available MCP tools\n debug test Connection test\n debug whoami Identity and config info\n\n ${colors.bold('Info')}\n feedback List and submit feedback\n\n${colors.muted('Examples:')}\n $ ${CLI_NAME} login # Authenticate with API key\n $ ${CLI_NAME} init # Initialize in current directory\n $ ${CLI_NAME} status # Show current status\n $ ${CLI_NAME} switch <id> # Switch active project/spec\n\n${colors.muted('Documentation:')}\n ${colors.primary('https://docs.specforge.com/cli')}\n\n${colors.muted('Report issues:')}\n ${colors.primary('https://github.com/specforge/mcp/issues')}\n`);\n}\n\n/**\n * The main CLI program instance\n */\nconst program = createProgram();\n\n// Register commands\nregisterServeCommand(program);\nregisterCommands(program);\n\n// Configure help\nconfigureHelp(program);\n\n/**\n * Run the CLI with given arguments\n *\n * @param argv - Command line arguments (defaults to process.argv)\n */\nexport function runCLI(argv: string[] = process.argv): void {\n try {\n program.parse(argv);\n } catch (error) {\n // Commander handles action-handler errors via `withErrorHandler`, but\n // anything thrown from a `preAction` hook (e.g. `resolveConfig()`\n // surfacing a `LEGACY_CONFIG_DETECTED` ConfigError for a project dir\n // with a legacy `.specforge.json`) reaches here. Use the shared\n // formatter so the user sees the message + hint instead of a silent\n // exit. Keep `debugLog` for stack-trace-under-debug ergonomics.\n debugLog('CLI parse error:', error);\n handleError(error);\n }\n}\n\n/**\n * Export program for testing and extension\n */\nexport { program };\n\n/**\n * Export version for external use\n */\nexport { CLI_VERSION };\n"],"mappings":"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","names":["require","program"]}
1
+ {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["/**\n * SpecForge MCP CLI Entry Point\n *\n * Main CLI application using Commander.js.\n * Sets up command routing, middleware, and global options.\n */\n\nimport { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { authGuardHook } from './middleware/auth-guard.js';\nimport { withErrorHandler, debugLog, handleError } from './middleware/error-handler.js';\nimport { resolveConfig } from './config/loader.js';\nimport { colors } from './ui/index.js';\nimport { printBanner } from './ui/banner.js';\nimport { CHANNEL } from '../channel.js';\nimport { notifyUpdate } from './update-check.js';\n\nconst BANNER_SKIP_COMMANDS = new Set(['init', 'login', 'serve']);\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../../package.json');\nimport {\n registerLoginCommand,\n registerInitCommand,\n registerConfigureCommand,\n registerDoctorCommand,\n registerStatusCommand,\n registerSwitchCommand,\n registerFeedbackCommand,\n registerMembersCommand,\n registerInvitationsCommand,\n} from './commands/index.js';\nimport { registerDebugCommands } from './commands/debug/index.js';\nimport { registerScaffoldCommand } from './commands/scaffold/index.js';\n\n/**\n * CLI version - read from package.json\n */\nconst CLI_VERSION: string = pkg.version;\n\n/**\n * CLI name for display — varies per build channel (`specforge` vs.\n * `specforge-canary`). The `SpecForge` brand text in banners stays fixed.\n */\nconst CLI_NAME = CHANNEL.name;\n\n/**\n * Create and configure the CLI program\n */\nfunction createProgram(): Command {\n const program = new Command();\n\n // Basic program info\n program\n .name(CLI_NAME)\n .description('SpecForge MCP CLI - Configure and interact with SpecForge')\n .version(CLI_VERSION, '-v, --version', 'Display version number');\n\n // Global options\n program\n .option('--debug', 'Enable debug output')\n .option('--no-color', 'Disable colored output')\n .option('--json', 'Output in JSON format');\n\n // Pre-action hook for auth guard\n program.hook('preAction', (thisCommand, actionCommand) => {\n const commandName = actionCommand.name();\n\n // Set debug mode from flag\n if (thisCommand.opts().debug) {\n process.env.SPECFORGE_DEBUG = 'true';\n }\n\n // Disable colors if requested\n if (thisCommand.opts().noColor) {\n // chalk.level = 0 is handled in colors module\n process.env.NO_COLOR = '1';\n }\n\n // Compact brand header for human-facing commands\n const jsonOutput = !!thisCommand.opts().json;\n const programState = thisCommand as Command & { _bannerPrinted?: boolean };\n if (\n !programState._bannerPrinted &&\n process.stdout.isTTY &&\n !jsonOutput &&\n !BANNER_SKIP_COMMANDS.has(commandName)\n ) {\n printBanner('compact');\n programState._bannerPrinted = true;\n }\n\n debugLog(`Running command: ${commandName}`);\n debugLog('Resolved config:', resolveConfig());\n\n // Run auth guard\n authGuardHook({ name: () => commandName });\n });\n\n return program;\n}\n\n/**\n * Register the default serve command (MCP server mode)\n *\n * This is the hidden default when no subcommand is provided.\n * Maintains backward compatibility with direct MCP server usage.\n */\nfunction registerServeCommand(program: Command): void {\n program\n .command('serve', { isDefault: true, hidden: true })\n .description('Start MCP server (default)')\n .action(withErrorHandler(async () => {\n debugLog('Starting MCP server');\n\n // Dynamic import to avoid loading server code for CLI commands\n const { loadConfig, validateConfig } = await import('../config/index.js');\n const { createServer, startServer } = await import('../server.js');\n\n // Load and validate config\n const config = await loadConfig();\n validateConfig(config);\n // Honor the resolved MCP output format (env > project > global > default).\n config.mcpOutputFormat = resolveConfig().mcpOutputFormat;\n\n // Create and start server\n const server = await createServer(config);\n await startServer(server);\n }));\n}\n\n/**\n * Register all CLI commands\n */\nfunction registerCommands(program: Command): void {\n registerLoginCommand(program);\n registerInitCommand(program);\n registerConfigureCommand(program);\n registerDoctorCommand(program);\n registerStatusCommand(program);\n registerSwitchCommand(program);\n registerDebugCommands(program);\n registerScaffoldCommand(program);\n registerFeedbackCommand(program);\n registerMembersCommand(program);\n registerInvitationsCommand(program);\n}\n\n/**\n * Configure help output styling\n */\nfunction configureHelp(program: Command): void {\n program.configureHelp({\n sortSubcommands: false,\n subcommandTerm: (cmd) => cmd.name(),\n });\n\n // Custom help with organized command groups\n program.addHelpText('after', `\n${colors.muted('Command Groups:')}\n\n ${colors.bold('Authentication')}\n login Authenticate with SpecForge API\n configure Read/write configuration\n\n ${colors.bold('Setup')}\n init Initialize SpecForge in a project\n scaffold Generate skills, agents, and commands\n\n ${colors.bold('Diagnostics')}\n doctor Health check\n status Project status and session info\n\n ${colors.bold('Context')}\n switch Switch active project or specification\n\n ${colors.bold('Collaboration')}\n members Manage project members\n invitations Manage invitations\n\n ${colors.bold('Debug')}\n debug call Direct MCP tool dispatch\n debug tools List available MCP tools\n debug test Connection test\n debug whoami Identity and config info\n\n ${colors.bold('Info')}\n feedback List and submit feedback\n\n${colors.muted('Examples:')}\n $ ${CLI_NAME} login # Authenticate with API key\n $ ${CLI_NAME} init # Initialize in current directory\n $ ${CLI_NAME} status # Show current status\n $ ${CLI_NAME} switch <id> # Switch active project/spec\n\n${colors.muted('Documentation:')}\n ${colors.primary('https://docs.specforge.com/cli')}\n\n${colors.muted('Report issues:')}\n ${colors.primary('https://github.com/specforge/mcp/issues')}\n`);\n}\n\n/**\n * The main CLI program instance\n */\nconst program = createProgram();\n\n// Register commands\nregisterServeCommand(program);\nregisterCommands(program);\n\n// Configure help\nconfigureHelp(program);\n\n/**\n * Run the CLI with given arguments\n *\n * @param argv - Command line arguments (defaults to process.argv)\n */\nexport function runCLI(argv: string[] = process.argv): void {\n // Non-blocking update notice: prints a banner from a prior run's cached\n // registry check, and refreshes that cache in the background. Never awaited,\n // so it can't add latency or fail a command. Suppressed for --json / CI /\n // non-TTY / SPECFORGE_NO_UPDATE_CHECK.\n notifyUpdate({ current: CLI_VERSION, isJson: argv.includes('--json') });\n try {\n program.parse(argv);\n } catch (error) {\n // Commander handles action-handler errors via `withErrorHandler`, but\n // anything thrown from a `preAction` hook (e.g. `resolveConfig()`\n // surfacing a `LEGACY_CONFIG_DETECTED` ConfigError for a project dir\n // with a legacy `.specforge.json`) reaches here. Use the shared\n // formatter so the user sees the message + hint instead of a silent\n // exit. Keep `debugLog` for stack-trace-under-debug ergonomics.\n debugLog('CLI parse error:', error);\n handleError(error);\n }\n}\n\n/**\n * Export program for testing and extension\n */\nexport { program };\n\n/**\n * Export version for external use\n */\nexport { CLI_VERSION };\n"],"mappings":"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","names":["require","program"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Compare dot-separated numeric versions. Returns 1 if a>b, -1 if a<b, 0 equal.
3
+ * Pre-release suffixes (e.g. `0.2.5-beta.1`) are stripped — a numeric compare is
4
+ * enough for the CLI's simple version line.
5
+ */
6
+ export declare function compareVersions(a: string, b: string): number;
7
+ /**
8
+ * Best-effort update check. Prints the banner synchronously from cache, then
9
+ * refreshes the cache in the background. Any failure is swallowed — a version
10
+ * check must never break a command.
11
+ */
12
+ export declare function notifyUpdate(opts: {
13
+ current: string;
14
+ isJson: boolean;
15
+ }): void;
16
+ //# sourceMappingURL=update-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../../src/cli/update-check.ts"],"names":[],"mappings":"AAuEA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU5D;AAyBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAsB7E"}
@@ -0,0 +1,95 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { CHANNEL } from "../channel.js";
6
+ const TTL_MS = 12 * 60 * 60 * 1e3;
7
+ const FETCH_TIMEOUT_MS = 3e3;
8
+ function cacheDir() {
9
+ return join(homedir(), CHANNEL.configDir);
10
+ }
11
+ function cachePath() {
12
+ return join(cacheDir(), "update-check.json");
13
+ }
14
+ function readCacheSync() {
15
+ try {
16
+ const parsed = JSON.parse(readFileSync(cachePath(), "utf8"));
17
+ if (typeof parsed.latest === "string" && typeof parsed.checkedAt === "number") {
18
+ return parsed;
19
+ }
20
+ } catch {
21
+ }
22
+ return null;
23
+ }
24
+ async function writeCache(data) {
25
+ try {
26
+ await mkdir(cacheDir(), { recursive: true });
27
+ await writeFile(cachePath(), JSON.stringify(data), "utf8");
28
+ } catch {
29
+ }
30
+ }
31
+ async function fetchLatestVersion(packageName) {
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
34
+ try {
35
+ const url = `https://registry.npmjs.org/${packageName.replace("/", "%2F")}/latest`;
36
+ const res = await fetch(url, { signal: controller.signal });
37
+ if (!res.ok) return null;
38
+ const body = await res.json();
39
+ return typeof body.version === "string" ? body.version : null;
40
+ } catch {
41
+ return null;
42
+ } finally {
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+ function compareVersions(a, b) {
47
+ const pa = a.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
48
+ const pb = b.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
49
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
50
+ const da = pa[i] ?? 0;
51
+ const db = pb[i] ?? 0;
52
+ if (da > db) return 1;
53
+ if (da < db) return -1;
54
+ }
55
+ return 0;
56
+ }
57
+ function isSuppressed(isJson) {
58
+ return isJson || process.env.SPECFORGE_NO_UPDATE_CHECK === "1" || process.env.CI === "true" || process.env.CI === "1" || !process.stderr.isTTY;
59
+ }
60
+ function printBanner(current, latest) {
61
+ const useColor = !process.env.NO_COLOR && process.stderr.isTTY;
62
+ const dim = useColor ? "\x1B[2m" : "";
63
+ const yellow = useColor ? "\x1B[33m" : "";
64
+ const bold = useColor ? "\x1B[1m" : "";
65
+ const reset = useColor ? "\x1B[0m" : "";
66
+ const cmd = `npm i -g ${CHANNEL.packageName}`;
67
+ process.stderr.write(
68
+ `
69
+ ${yellow}${bold}\u2B06 Update available${reset} ${dim}${current}${reset} \u2192 ${bold}${latest}${reset}
70
+ ${dim}Run${reset} ${bold}${cmd}${reset} ${dim}to update.${reset}
71
+
72
+ `
73
+ );
74
+ }
75
+ function notifyUpdate(opts) {
76
+ try {
77
+ if (isSuppressed(opts.isJson)) return;
78
+ const cache = readCacheSync();
79
+ if (cache && compareVersions(cache.latest, opts.current) > 0) {
80
+ printBanner(opts.current, cache.latest);
81
+ }
82
+ if (!cache || Date.now() - cache.checkedAt > TTL_MS) {
83
+ void fetchLatestVersion(CHANNEL.packageName).then((latest) => {
84
+ if (latest) return writeCache({ latest, checkedAt: Date.now() });
85
+ }).catch(() => {
86
+ });
87
+ }
88
+ } catch {
89
+ }
90
+ }
91
+ export {
92
+ compareVersions,
93
+ notifyUpdate
94
+ };
95
+ //# sourceMappingURL=update-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/update-check.ts"],"sourcesContent":["/**\n * Update notifier — tells the user when a newer CLI is on npm.\n *\n * Channel-aware: checks `CHANNEL.packageName` (stable → `@specforge/cli`, canary\n * → `@specforge/canary-cli`). NEVER blocks a command: the banner is driven by a\n * cached result from a PRIOR run (read synchronously), and the current run\n * refreshes that cache in the background (fire-and-forget) when it's older than\n * the TTL. The very first run just seeds the cache; subsequent runs show the\n * banner — the standard update-notifier trade-off.\n *\n * Suppressed for `--json` output, under CI, on non-TTY stderr (pipes/scripts),\n * and via `SPECFORGE_NO_UPDATE_CHECK=1`.\n */\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { readFileSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { CHANNEL } from '../channel.js';\n\nconst TTL_MS = 12 * 60 * 60 * 1000; // re-check the registry at most ~twice a day\nconst FETCH_TIMEOUT_MS = 3000;\n\ninterface UpdateCache {\n latest: string;\n checkedAt: number;\n}\n\nfunction cacheDir(): string {\n return join(homedir(), CHANNEL.configDir);\n}\nfunction cachePath(): string {\n return join(cacheDir(), 'update-check.json');\n}\n\nfunction readCacheSync(): UpdateCache | null {\n try {\n const parsed = JSON.parse(readFileSync(cachePath(), 'utf8')) as UpdateCache;\n if (typeof parsed.latest === 'string' && typeof parsed.checkedAt === 'number') {\n return parsed;\n }\n } catch {\n /* no cache / unreadable — treat as \"unknown\" */\n }\n return null;\n}\n\nasync function writeCache(data: UpdateCache): Promise<void> {\n try {\n await mkdir(cacheDir(), { recursive: true });\n await writeFile(cachePath(), JSON.stringify(data), 'utf8');\n } catch {\n /* best-effort */\n }\n}\n\nasync function fetchLatestVersion(packageName: string): Promise<string | null> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n const url = `https://registry.npmjs.org/${packageName.replace('/', '%2F')}/latest`;\n const res = await fetch(url, { signal: controller.signal });\n if (!res.ok) return null;\n const body = (await res.json()) as { version?: unknown };\n return typeof body.version === 'string' ? body.version : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Compare dot-separated numeric versions. Returns 1 if a>b, -1 if a<b, 0 equal.\n * Pre-release suffixes (e.g. `0.2.5-beta.1`) are stripped — a numeric compare is\n * enough for the CLI's simple version line.\n */\nexport function compareVersions(a: string, b: string): number {\n const pa = a.split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);\n const pb = b.split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);\n for (let i = 0; i < Math.max(pa.length, pb.length); i++) {\n const da = pa[i] ?? 0;\n const db = pb[i] ?? 0;\n if (da > db) return 1;\n if (da < db) return -1;\n }\n return 0;\n}\n\nfunction isSuppressed(isJson: boolean): boolean {\n return (\n isJson ||\n process.env.SPECFORGE_NO_UPDATE_CHECK === '1' ||\n process.env.CI === 'true' ||\n process.env.CI === '1' ||\n !process.stderr.isTTY\n );\n}\n\nfunction printBanner(current: string, latest: string): void {\n const useColor = !process.env.NO_COLOR && process.stderr.isTTY;\n const dim = useColor ? '\\x1b[2m' : '';\n const yellow = useColor ? '\\x1b[33m' : '';\n const bold = useColor ? '\\x1b[1m' : '';\n const reset = useColor ? '\\x1b[0m' : '';\n const cmd = `npm i -g ${CHANNEL.packageName}`;\n process.stderr.write(\n `\\n${yellow}${bold}⬆ Update available${reset} ${dim}${current}${reset} → ${bold}${latest}${reset}\\n` +\n ` ${dim}Run${reset} ${bold}${cmd}${reset} ${dim}to update.${reset}\\n\\n`,\n );\n}\n\n/**\n * Best-effort update check. Prints the banner synchronously from cache, then\n * refreshes the cache in the background. Any failure is swallowed — a version\n * check must never break a command.\n */\nexport function notifyUpdate(opts: { current: string; isJson: boolean }): void {\n try {\n if (isSuppressed(opts.isJson)) return;\n\n const cache = readCacheSync();\n if (cache && compareVersions(cache.latest, opts.current) > 0) {\n printBanner(opts.current, cache.latest);\n }\n\n // Refresh the cache when missing/stale — fire-and-forget so we never wait.\n if (!cache || Date.now() - cache.checkedAt > TTL_MS) {\n void fetchLatestVersion(CHANNEL.packageName)\n .then((latest) => {\n if (latest) return writeCache({ latest, checkedAt: Date.now() });\n })\n .catch(() => {\n /* ignore */\n });\n }\n } catch {\n /* never break the CLI over a version check */\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specforge/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "MCP server for SpecForge - AI agent integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,8 +20,8 @@
20
20
  "zod": "^3.22.0",
21
21
  "@specforge/api-types": "0.4.12",
22
22
  "@specforge/session-types": "0.4.11",
23
- "@specforge/spec-types": "0.4.11",
24
- "@specforge/report-types": "0.4.3"
23
+ "@specforge/report-types": "0.4.3",
24
+ "@specforge/spec-types": "0.4.11"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/inquirer": "^9.0.9",
@@ -61,7 +61,7 @@
61
61
  "@specforge/session-types",
62
62
  "@specforge/report-types"
63
63
  ],
64
- "gitHead": "39a5444038d38674bebc486923ff133e89271cc9",
64
+ "gitHead": "05419e7f1635de01a3b1245fbca87cc04df3be62",
65
65
  "scripts": {
66
66
  "build": "tsup && tsc --emitDeclarationOnly --outDir dist",
67
67
  "typecheck": "tsc --noEmit",