@groundtrack/hooks 0.1.17 → 0.1.18

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.
package/README.md CHANGED
@@ -88,7 +88,12 @@ live activation needs attention. No synthetic engineering event is required.
88
88
  ## Doctor compatibility
89
89
 
90
90
  `groundtrack doctor` is an alias for `status`; `groundtrack doctor --fix` uses the
91
- same setup and repair flow. Existing single-client commands continue to work:
91
+ same setup and repair flow. Doctor checks for a newer published hooks package;
92
+ when one is available, upgrade before repairing with an older bundled policy.
93
+ If the registry is unavailable, Doctor still checks local configuration and
94
+ preserves managed instructions newer than its own template. When invoked through
95
+ `npx`, Doctor checks the globally installed hook runtime rather than the
96
+ temporary executable provided by `npx`. Existing single-client commands continue to work:
92
97
 
93
98
  ```sh
94
99
  groundtrack doctor --fix --client codex
package/dist/doctor.js CHANGED
@@ -228,9 +228,10 @@ function upsertCodexHook(content, matcher) {
228
228
  function managedContentIsCurrent(content, desired) {
229
229
  if (!content)
230
230
  return false;
231
- const start = content.indexOf(MANAGED_START);
232
- const end = content.indexOf(MANAGED_END, start);
233
- return start >= 0 && end >= 0 && content.slice(start, end + MANAGED_END.length) === desired;
231
+ const managed = /<!-- groundtrack:start version=(\d+) -->[\s\S]*?<!-- groundtrack:end -->/.exec(content);
232
+ const desiredVersion = /<!-- groundtrack:start version=(\d+) -->/.exec(desired)?.[1];
233
+ return ((managed != null && desiredVersion != null && Number(managed[1]) > Number(desiredVersion)) ||
234
+ managed?.[0] === desired);
234
235
  }
235
236
  export function claudeInstructionsAreCurrent(content) {
236
237
  if (!content)
@@ -271,8 +272,12 @@ function removeLegacyAgentInstructions(content) {
271
272
  }
272
273
  export function upsertManagedContent(content, desired, migrateLegacyAgentInstructions = false) {
273
274
  const existing = content ?? "";
274
- const managed = /<!-- groundtrack:start version=\d+ -->[\s\S]*?<!-- groundtrack:end -->/;
275
- if (managed.test(existing)) {
275
+ const managed = /<!-- groundtrack:start version=(\d+) -->[\s\S]*?<!-- groundtrack:end -->/;
276
+ const match = managed.exec(existing);
277
+ if (match) {
278
+ const desiredVersion = /<!-- groundtrack:start version=(\d+) -->/.exec(desired)?.[1];
279
+ if (desiredVersion && Number(match[1]) > Number(desiredVersion))
280
+ return existing;
276
281
  return `${existing.replace(managed, desired).trimEnd()}\n`;
277
282
  }
278
283
  const base = (migrateLegacyAgentInstructions ? removeLegacyAgentInstructions(existing) : existing).trimEnd();
@@ -371,7 +376,8 @@ export function runDoctor(options) {
371
376
  const matcher = matcherForAliases(aliases);
372
377
  const checks = [];
373
378
  const changedFiles = [];
374
- if (options.latestVersion && compareVersions(options.latestVersion, version) > 0) {
379
+ const upgradeNeeded = options.latestVersion && compareVersions(options.latestVersion, version) > 0;
380
+ if (upgradeNeeded) {
375
381
  checks.push({
376
382
  id: "cli-version",
377
383
  label: "Groundtrack package",
@@ -387,6 +393,8 @@ export function runDoctor(options) {
387
393
  detail: options.latestVersion ? `${version} (latest)` : `${version} (offline bundled state)`,
388
394
  });
389
395
  }
396
+ if (upgradeNeeded && options.fix)
397
+ return { version, clients, aliases, checks, changedFiles, healthy: false };
390
398
  let runtimeVersion = options.installedHookVersion;
391
399
  if (runtimeVersion === undefined)
392
400
  runtimeVersion = installedHookVersion();
@@ -2,6 +2,7 @@ import { type Agent } from "../agents.js";
2
2
  export declare const USAGE: string;
3
3
  export type Arguments = {
4
4
  command: "setup" | "status" | "version" | "help";
5
+ doctor: boolean;
5
6
  agents: Agent[];
6
7
  allDetected: boolean;
7
8
  yes: boolean;
package/dist/setup/cli.js CHANGED
@@ -36,6 +36,7 @@ export function parseArguments(input) {
36
36
  const argv = [...input];
37
37
  const result = {
38
38
  command: "status",
39
+ doctor: false,
39
40
  agents: [],
40
41
  allDetected: false,
41
42
  yes: false,
@@ -51,6 +52,7 @@ export function parseArguments(input) {
51
52
  command = "version";
52
53
  if (!["setup", "status", "doctor", "version"].includes(command))
53
54
  throw new Error(`Unknown command: ${command}. Run groundtrack --help.`);
55
+ result.doctor = command === "doctor";
54
56
  result.command = command === "doctor" ? "status" : command;
55
57
  const take = (index, flag) => {
56
58
  const value = argv[index + 1];
@@ -105,6 +107,23 @@ export function parseArguments(input) {
105
107
  return result;
106
108
  }
107
109
  function runtimeVersion(context) {
110
+ if (context.env.npm_command === "exec") {
111
+ const result = spawnSync("npm", ["ls", "--global", "@groundtrack/hooks", "--depth=0", "--json"], {
112
+ env: context.env,
113
+ encoding: "utf8",
114
+ timeout: 5_000,
115
+ windowsHide: true,
116
+ shell: context.platform === "win32",
117
+ });
118
+ try {
119
+ const value = JSON.parse(result.stdout);
120
+ const version = value.dependencies?.["@groundtrack/hooks"]?.version;
121
+ return typeof version === "string" ? version : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
108
127
  const binary = executable("groundtrack-hook", context);
109
128
  if (!binary)
110
129
  return null;
@@ -129,6 +148,26 @@ function runtimeVersion(context) {
129
148
  return null;
130
149
  }
131
150
  }
151
+ function publishedVersion(context) {
152
+ if (context.env.GROUNDTRACK_DOCTOR_LATEST_VERSION !== undefined)
153
+ return context.env.GROUNDTRACK_DOCTOR_LATEST_VERSION || null;
154
+ const result = spawnSync("npm", ["view", "@groundtrack/hooks", "version", "--json"], {
155
+ env: context.env,
156
+ encoding: "utf8",
157
+ timeout: 5_000,
158
+ windowsHide: true,
159
+ shell: context.platform === "win32",
160
+ });
161
+ if (result.status !== 0)
162
+ return null;
163
+ try {
164
+ const version = JSON.parse(result.stdout);
165
+ return typeof version === "string" ? version : null;
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
132
171
  function run(binary, args, context, inherit = false) {
133
172
  return new Promise((resolve) => {
134
173
  const child = spawn(binary, args, {
@@ -143,7 +182,7 @@ function run(binary, args, context, inherit = false) {
143
182
  child.once("close", (code) => resolve(code === 0));
144
183
  });
145
184
  }
146
- function printPlan(plan, context, runtimeReady) {
185
+ function printPlan(plan, context, runtimeReady, installedVersion) {
147
186
  process.stdout.write(`\nGroundtrack · ${basename(context.repoDir)}\nProject: ${context.repoDir}\n\n`);
148
187
  for (const agent of planSummary(plan, context)) {
149
188
  process.stdout.write(` ${agent.status === "blocked" ? "!" : agent.status === "configured" ? "✓" : "+"} ${agent.name}: ${agent.status === "configured" ? "already configured" : agent.status === "blocked" ? agent.issue : "ready to set up"}\n`);
@@ -153,7 +192,9 @@ function printPlan(plan, context, runtimeReady) {
153
192
  if (plan.shared.length)
154
193
  process.stdout.write(" + Update shared AGENTS.md instructions\n");
155
194
  if (!runtimeReady)
156
- process.stdout.write(" + Install the shared Groundtrack hook runtime once\n");
195
+ process.stdout.write(installedVersion
196
+ ? ` + Upgrade the shared Groundtrack hook runtime from ${installedVersion} to ${packageVersion()}\n`
197
+ : " + Install the shared Groundtrack hook runtime once\n");
157
198
  process.stdout.write("\nScope: this project. Existing agent connections are kept.\n");
158
199
  }
159
200
  export async function runCli(argv) {
@@ -168,6 +209,33 @@ export async function runCli(argv) {
168
209
  }
169
210
  const context = environment({ repoDir: args.repoDir, homeDir: args.homeDir });
170
211
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !args.json);
212
+ const latestVersion = args.doctor ? publishedVersion(context) : null;
213
+ const upgradeNeeded = latestVersion != null && compareVersions(latestVersion, packageVersion()) > 0;
214
+ if (upgradeNeeded) {
215
+ const command = "npm install --global @groundtrack/hooks@latest";
216
+ const retry = args.command === "setup" ? "groundtrack doctor --fix" : "groundtrack doctor";
217
+ if (!args.json)
218
+ process.stdout.write(`Groundtrack Doctor ${packageVersion()} is outdated; ${latestVersion} is available. Upgrade before checking or repairing this project.\n`);
219
+ if (args.command === "setup" && interactive && !args.yes) {
220
+ const approved = await prompts.confirm({
221
+ message: `Upgrade Groundtrack globally to ${latestVersion} now?`,
222
+ initialValue: true,
223
+ });
224
+ if (!prompts.isCancel(approved) && approved) {
225
+ const installed = await run("npm", ["install", "--global", "@groundtrack/hooks@latest"], context, true);
226
+ if (installed) {
227
+ process.stdout.write(`Upgrade complete. Rerun ${retry} to check this project.\n`);
228
+ process.exitCode = 1;
229
+ return;
230
+ }
231
+ }
232
+ }
233
+ process.stdout.write(args.json
234
+ ? `${JSON.stringify({ installedVersion: packageVersion(), latestVersion, upgradeCommand: command, applied: false })}\n`
235
+ : `Run \`${command}\`, then rerun \`${retry}\`. No project files changed.\n`);
236
+ process.exitCode = 1;
237
+ return;
238
+ }
171
239
  if (args.connect && !interactive)
172
240
  throw new Error("--connect needs an interactive terminal. Run setup there, or use the per-agent sign-in instructions after setup.");
173
241
  const discovered = discoverAgents(context, args.url);
@@ -231,7 +299,7 @@ export async function runCli(argv) {
231
299
  if (args.json)
232
300
  process.stdout.write(`${JSON.stringify({ agents: planSummary(plan, context), detected: discovered, runtimeReady, sharedIssue: plan.sharedIssue, sharedFiles: plan.shared.map((change) => change.path), dryRun: args.dryRun }, null, 2)}\n`);
233
301
  else {
234
- printPlan(plan, context, runtimeReady);
302
+ printPlan(plan, context, runtimeReady, installedVersion);
235
303
  const other = discovered.filter((agent) => agent.detected && !targets.includes(agent.id));
236
304
  if (other.length)
237
305
  process.stdout.write(`\nAlso detected: ${other.map((agent) => agent.name).join(", ")}. Add them with groundtrack setup.\n`);
@@ -250,7 +318,7 @@ export async function runCli(argv) {
250
318
  if (!args.yes && !interactive)
251
319
  throw new Error("Setup needs confirmation. Use --yes with your selected --agent values, or run groundtrack setup in an interactive terminal. No files changed.");
252
320
  if (!args.json)
253
- printPlan(plan, context, runtimeReady);
321
+ printPlan(plan, context, runtimeReady, installedVersion);
254
322
  const ready = plan.agents.filter((agent) => !agent.issue);
255
323
  if (plan.sharedIssue || !ready.length) {
256
324
  if (args.json)
@@ -269,6 +337,7 @@ export async function runCli(argv) {
269
337
  return;
270
338
  }
271
339
  }
340
+ let runtimeUpdated = false;
272
341
  if (!runtimeReady && ready.some((agent) => requiresHookRuntime(agent.agent))) {
273
342
  if (context.env.GROUNDTRACK_DOCTOR_SKIP_INSTALL === "1")
274
343
  throw new Error("The Groundtrack hook runtime is missing or outdated. Install @groundtrack/hooks before applying this setup.");
@@ -279,6 +348,7 @@ export async function runCli(argv) {
279
348
  const version = runtimeVersion(context);
280
349
  if (!installed || !version || compareVersions(version, packageVersion()) < 0)
281
350
  throw new Error("The hook runtime could not be installed or is not on PATH. Run npm install --global @groundtrack/hooks, ensure groundtrack-hook is on PATH, then retry. Agent configuration was not changed.");
351
+ runtimeUpdated = true;
282
352
  }
283
353
  const preferences = picked ? targets : [...new Set([...(saved ?? []), ...targets])];
284
354
  const changes = [
@@ -296,7 +366,7 @@ export async function runCli(argv) {
296
366
  process.stdout.write(`${JSON.stringify({ agents: planSummary(after, context), localReady, applied: true, changedFiles: changes.map((change) => change.path), backupDir }, null, 2)}\n`);
297
367
  }
298
368
  else {
299
- process.stdout.write(`\n${ready.length} ${ready.length === 1 ? "agent configured" : "agents configured"}. ${changes.length ? `${changes.length} files updated.` : "Everything is already current."}\n`);
369
+ process.stdout.write(`\n${ready.length} ${ready.length === 1 ? "agent configured" : "agents configured"}. ${changes.length ? `${changes.length} files updated.` : runtimeUpdated ? "Hook runtime upgraded. No project files changed." : "Everything is already current."}\n`);
300
370
  let connections = [];
301
371
  if (interactive) {
302
372
  const spinner = prompts.spinner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundtrack/hooks",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Lifecycle hook utilities for Groundtrack MCP clients.",
5
5
  "keywords": [
6
6
  "claude-code",