@clipboard-health/groundcrew 3.2.1 → 3.4.0

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
@@ -522,9 +522,9 @@ Cross-team projects work — the orchestrator caches the in-progress state ID pe
522
522
  </details>
523
523
 
524
524
  <details>
525
- <summary>Claude launches in bypass-permissions mode by default</summary>
525
+ <summary>Claude launches in auto mode by default</summary>
526
526
 
527
- Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode bypassPermissions` to avoid workspace-trust and tool-permission prompts blocking automation. Override `models.definitions.claude.cmd` for a stricter mode.
527
+ Groundcrew creates isolated per-ticket worktrees for unattended runs, so the shipped `claude` command is `claude --permission-mode auto` to let Claude proceed without stopping for clarifying questions while keeping its built-in safety prompts intact. Override `models.definitions.claude.cmd` for `bypassPermissions` if you need to suppress tool-permission prompts entirely, or for a stricter mode.
528
528
 
529
529
  </details>
530
530
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyKA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgOA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCvD"}
package/dist/cli.js CHANGED
@@ -8,7 +8,11 @@ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
8
  import { sandboxCli } from "./commands/sandbox/index.js";
9
9
  import { setupReposCli } from "./commands/setupRepos.js";
10
10
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
11
- import { errorMessage, readTicketArgument, writeError, writeOutput } from "./lib/util.js";
11
+ import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
12
+ import { computeUpgradeNudge, defaultUpgradeCheckCachePath, fetchLatestVersion, } from "./lib/upgrade.js";
13
+ import { errorMessage, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
14
+ const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
15
+ const NUDGE_FETCH_TIMEOUT_MS = 1000;
12
16
  const requireFromCli = createRequire(import.meta.url);
13
17
  function setupUsage() {
14
18
  return "Usage: crew setup repos [--dry-run] [<repo>...]";
@@ -51,6 +55,30 @@ async function runCli(argv) {
51
55
  }
52
56
  await setupWorkspaceCli(ticket, { dryRun });
53
57
  }
58
+ async function upgradeCliInvoke(argv) {
59
+ const metadata = packageMetadata();
60
+ await upgradeCli(argv, async () => await createDefaultUpgradeCliOptions({
61
+ currentVersion: metadata.version,
62
+ packageName: metadata.name,
63
+ cliMetaUrl: import.meta.url,
64
+ }));
65
+ }
66
+ async function maybeRunUpgradeNudge(metadata) {
67
+ const message = await computeUpgradeNudge({
68
+ currentVersion: metadata.version,
69
+ packageName: metadata.name,
70
+ cachePath: defaultUpgradeCheckCachePath(),
71
+ ttlMs: NUDGE_TTL_MS,
72
+ fetchTimeoutMs: NUDGE_FETCH_TIMEOUT_MS,
73
+ registry: readEnvironmentVariable("npm_config_registry"),
74
+ noUpgradeCheck: readEnvironmentVariable("GROUNDCREW_NO_UPGRADE_CHECK") === "1",
75
+ now: Date.now,
76
+ fetcher: fetchLatestVersion,
77
+ });
78
+ if (message !== undefined) {
79
+ writeError(message);
80
+ }
81
+ }
54
82
  async function doctorCli(argv) {
55
83
  let ticket;
56
84
  const remainingArgs = [];
@@ -119,6 +147,11 @@ const SUBCOMMANDS = {
119
147
  usage: "repos [--dry-run] [<repo>...]",
120
148
  invoke: setupCli,
121
149
  },
150
+ upgrade: {
151
+ summary: "Install the latest version of crew (or pin to a specific version)",
152
+ usage: "[<version>] [--check]",
153
+ invoke: upgradeCliInvoke,
154
+ },
122
155
  };
123
156
  function printHelp() {
124
157
  const width = Math.max(...Object.keys(SUBCOMMANDS).map((key) => key.length));
@@ -134,10 +167,13 @@ function printHelp() {
134
167
  }
135
168
  writeOutput("\nSee README.md for full configuration and behavior.");
136
169
  }
170
+ function packageMetadata() {
171
+ // oxlint-disable-next-line typescript-eslint/no-unsafe-assignment -- package.json is shipped with this package and is the metadata source of truth.
172
+ const metadata = requireFromCli("../package.json");
173
+ return metadata;
174
+ }
137
175
  function packageVersion() {
138
- // oxlint-disable-next-line typescript-eslint/no-unsafe-assignment -- package.json is shipped with this package and is the version source of truth.
139
- const packageMetadata = requireFromCli("../package.json");
140
- return packageMetadata.version;
176
+ return packageMetadata().version;
141
177
  }
142
178
  export async function run(argv) {
143
179
  const [subcommand, ...rest] = argv;
@@ -159,6 +195,14 @@ export async function run(argv) {
159
195
  process.exitCode = 1;
160
196
  return;
161
197
  }
198
+ if (subcommand !== "upgrade") {
199
+ try {
200
+ await maybeRunUpgradeNudge(packageMetadata());
201
+ }
202
+ catch {
203
+ // Passive nudge is never load-bearing; never block the user's command.
204
+ }
205
+ }
162
206
  try {
163
207
  await command.invoke(rest);
164
208
  }
@@ -0,0 +1,34 @@
1
+ import { type InstallKind, type NpmRunResult } from "../lib/npmGlobal.ts";
2
+ import { type VersionFetcher } from "../lib/upgrade.ts";
3
+ export interface UpgradeCliOptions {
4
+ currentVersion: string;
5
+ packageName: string;
6
+ resolveInstall: () => Promise<UpgradeInstallDetails>;
7
+ fetcher: VersionFetcher;
8
+ runInstall: (options: {
9
+ packageName: string;
10
+ version: string;
11
+ npmBin: string;
12
+ }) => Promise<NpmRunResult>;
13
+ registry?: string | undefined;
14
+ fetchTimeoutMs: number;
15
+ /** Path of the upgrade-availability cache that the nudge reads. We prime
16
+ * it from `--check` and the default install path so the next non-upgrade
17
+ * subcommand can render the nudge without paying the network cost. */
18
+ cachePath: string;
19
+ now: () => number;
20
+ }
21
+ export interface UpgradeInstallDetails {
22
+ installKind: InstallKind;
23
+ installPath: string;
24
+ npmBin: string | undefined;
25
+ }
26
+ export type UpgradeCliOptionsInput = UpgradeCliOptions | (() => Promise<UpgradeCliOptions>);
27
+ export declare function upgradeCli(argv: string[], optionsInput: UpgradeCliOptionsInput): Promise<void>;
28
+ export interface CreateUpgradeOptionsArgs {
29
+ currentVersion: string;
30
+ packageName: string;
31
+ cliMetaUrl: string;
32
+ }
33
+ export declare function createDefaultUpgradeCliOptions(args: CreateUpgradeOptionsArgs): Promise<UpgradeCliOptions>;
34
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACrD,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,CAAC,OAAO,EAAE;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB;;0EAEsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAQD,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,CAAC,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AA0D5F,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EAAE,EACd,YAAY,EAAE,sBAAsB,GACnC,OAAO,CAAC,IAAI,CAAC,CAyCf;AAyFD,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,iBAAiB,CAAC,CA2B5B"}
@@ -0,0 +1,196 @@
1
+ import { runCommand } from "../lib/commandRunner.js";
2
+ import { which } from "../lib/host.js";
3
+ import { classifyInstall, createDefaultNpmSpawner, detectInstallPath, detectIsSymlink, detectNpmRootGlobal, runNpmInstallGlobal, } from "../lib/npmGlobal.js";
4
+ import { compareVersions, defaultUpgradeCheckCachePath, fetchAndPrimeUpgradeCheckCache, fetchLatestVersion, parseVersion, } from "../lib/upgrade.js";
5
+ import { errorMessage, readEnvironmentVariable, writeError, writeOutput } from "../lib/util.js";
6
+ const EXPLICIT_FETCH_TIMEOUT_MS = 5000;
7
+ function parseArgs(argv) {
8
+ let check = false;
9
+ let pinnedVersion;
10
+ for (const arg of argv) {
11
+ if (arg === "--help" || arg === "-h") {
12
+ return { kind: "help" };
13
+ }
14
+ if (arg === "--check") {
15
+ check = true;
16
+ continue;
17
+ }
18
+ if (arg.startsWith("-")) {
19
+ return { kind: "error", message: `crew upgrade: unknown argument: ${arg}` };
20
+ }
21
+ if (pinnedVersion !== undefined) {
22
+ return { kind: "error", message: "crew upgrade: too many positional arguments" };
23
+ }
24
+ pinnedVersion = arg;
25
+ }
26
+ if (check && pinnedVersion !== undefined) {
27
+ return { kind: "error", message: "crew upgrade: --check does not accept a version argument" };
28
+ }
29
+ if (check) {
30
+ return { kind: "check" };
31
+ }
32
+ return { kind: "install", pinnedVersion };
33
+ }
34
+ function printHelp() {
35
+ writeOutput("Usage: crew upgrade [<version>] [--check]");
36
+ writeOutput("");
37
+ writeOutput("Install the latest version of crew.");
38
+ writeOutput("");
39
+ writeOutput("Arguments:");
40
+ writeOutput(" <version> Install an exact version (upgrade or downgrade)");
41
+ writeOutput("");
42
+ writeOutput("Options:");
43
+ writeOutput(" --check Report availability without installing");
44
+ writeOutput(" -h, --help Show this help");
45
+ }
46
+ function refusalMessage(kind, installPath, packageName) {
47
+ return `crew is not installed globally (${kind} at ${installPath}). Run 'npm install -g ${packageName}' to use 'crew upgrade'.`;
48
+ }
49
+ async function resolveOptions(options) {
50
+ if (typeof options === "function") {
51
+ return await options();
52
+ }
53
+ return options;
54
+ }
55
+ export async function upgradeCli(argv, optionsInput) {
56
+ const parsed = parseArgs(argv);
57
+ if (parsed.kind === "error") {
58
+ writeError(parsed.message);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ if (parsed.kind === "help") {
63
+ printHelp();
64
+ return;
65
+ }
66
+ const options = await resolveOptions(optionsInput);
67
+ if (parsed.kind === "check") {
68
+ await runCheck(options);
69
+ return;
70
+ }
71
+ let targetVersion;
72
+ if (parsed.pinnedVersion === undefined) {
73
+ const fetched = await fetchOrFail(options);
74
+ if (fetched === undefined) {
75
+ return;
76
+ }
77
+ if (compareVersions(options.currentVersion, fetched) >= 0) {
78
+ writeOutput(`crew is up to date (${fetched})`);
79
+ return;
80
+ }
81
+ targetVersion = fetched;
82
+ }
83
+ else {
84
+ const resolved = resolvePinnedVersion(options, parsed.pinnedVersion);
85
+ if (resolved === undefined) {
86
+ return;
87
+ }
88
+ targetVersion = resolved;
89
+ }
90
+ const npmBin = await resolveGlobalNpmBin(options);
91
+ if (npmBin === undefined) {
92
+ return;
93
+ }
94
+ await runInstallAndReport(options, npmBin, targetVersion);
95
+ }
96
+ async function resolveGlobalNpmBin(options) {
97
+ const install = await options.resolveInstall();
98
+ if (install.installKind !== "global") {
99
+ writeError(refusalMessage(install.installKind, install.installPath, options.packageName));
100
+ process.exitCode = 1;
101
+ return undefined;
102
+ }
103
+ if (install.npmBin === undefined) {
104
+ writeError("crew upgrade: npm is required on PATH but was not found.");
105
+ process.exitCode = 1;
106
+ return undefined;
107
+ }
108
+ return install.npmBin;
109
+ }
110
+ async function runCheck(options) {
111
+ const latest = await fetchOrFail(options);
112
+ if (latest === undefined) {
113
+ return;
114
+ }
115
+ if (compareVersions(options.currentVersion, latest) >= 0) {
116
+ writeOutput(`crew is up to date (${latest})`);
117
+ return;
118
+ }
119
+ writeOutput(`${latest} available (you are on ${options.currentVersion}); run \`crew upgrade\``);
120
+ }
121
+ async function fetchOrFail(options) {
122
+ try {
123
+ return await fetchAndPrimeUpgradeCheckCache({
124
+ packageName: options.packageName,
125
+ cachePath: options.cachePath,
126
+ fetchTimeoutMs: options.fetchTimeoutMs,
127
+ registry: options.registry,
128
+ now: options.now,
129
+ fetcher: options.fetcher,
130
+ });
131
+ }
132
+ catch (error) {
133
+ writeError(`crew upgrade: could not reach npm registry: ${errorMessage(error)}`);
134
+ process.exitCode = 1;
135
+ return undefined;
136
+ }
137
+ }
138
+ function resolvePinnedVersion(options, pinnedVersion) {
139
+ try {
140
+ parseVersion(pinnedVersion);
141
+ }
142
+ catch (error) {
143
+ writeError(`crew upgrade: ${errorMessage(error)}`);
144
+ process.exitCode = 1;
145
+ return undefined;
146
+ }
147
+ if (options.currentVersion === pinnedVersion) {
148
+ writeOutput(`crew is already on ${pinnedVersion}`);
149
+ return undefined;
150
+ }
151
+ const cmp = compareVersions(options.currentVersion, pinnedVersion);
152
+ if (cmp > 0) {
153
+ writeOutput(`downgrading ${options.currentVersion} → ${pinnedVersion}`);
154
+ }
155
+ return pinnedVersion;
156
+ }
157
+ async function runInstallAndReport(options, npmBin, version) {
158
+ const result = await options.runInstall({
159
+ packageName: options.packageName,
160
+ version,
161
+ npmBin,
162
+ });
163
+ if (result.exitCode === 0) {
164
+ return;
165
+ }
166
+ if (result.sawEacces) {
167
+ writeError("crew upgrade: install failed with EACCES (permission denied). Your global npm prefix may require elevated permissions — see https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally");
168
+ }
169
+ process.exitCode = result.exitCode;
170
+ }
171
+ export async function createDefaultUpgradeCliOptions(args) {
172
+ return {
173
+ currentVersion: args.currentVersion,
174
+ packageName: args.packageName,
175
+ resolveInstall: async () => {
176
+ const installPath = detectInstallPath(args.cliMetaUrl);
177
+ const npmBin = await which("npm");
178
+ const npmRootGlobal = npmBin === undefined ? undefined : detectNpmRootGlobal(npmBin, runCommand);
179
+ const installKind = classifyInstall({
180
+ installPath,
181
+ npmRootGlobal,
182
+ isSymlink: detectIsSymlink,
183
+ });
184
+ return { installKind, installPath, npmBin };
185
+ },
186
+ fetcher: fetchLatestVersion,
187
+ runInstall: async (options) => await runNpmInstallGlobal({
188
+ ...options,
189
+ spawner: createDefaultNpmSpawner(process.stderr),
190
+ }),
191
+ fetchTimeoutMs: EXPLICIT_FETCH_TIMEOUT_MS,
192
+ registry: readEnvironmentVariable("npm_config_registry"),
193
+ cachePath: defaultUpgradeCheckCachePath(),
194
+ now: Date.now,
195
+ };
196
+ }
@@ -100,7 +100,7 @@ export interface ModelDefinition {
100
100
  * for execution. The rendered prompt is appended as a single quoted
101
101
  * positional argument. `{{worktree}}` is replaced before launch.
102
102
  *
103
- * Keep this agent-native (e.g., `claude --permission-mode bypassPermissions`).
103
+ * Keep this agent-native (e.g., `claude --permission-mode auto`).
104
104
  * Groundcrew adds the Safehouse wrapper.
105
105
  */
106
106
  cmd: string;
@@ -42,7 +42,7 @@ const DEFAULT_ORCHESTRATOR = {
42
42
  };
43
43
  const DEFAULT_MODEL_DEFINITIONS = {
44
44
  claude: {
45
- cmd: "claude --permission-mode bypassPermissions",
45
+ cmd: "claude --permission-mode auto",
46
46
  color: "#C15F3C",
47
47
  usage: { codexbar: { provider: "claude" } },
48
48
  },
@@ -0,0 +1,29 @@
1
+ export type InstallKind = "global" | "linked" | "npx" | "project" | "unknown";
2
+ export interface ClassifyInstallOptions {
3
+ installPath: string;
4
+ npmRootGlobal: string | undefined;
5
+ isSymlink: (path: string) => boolean;
6
+ }
7
+ export declare function classifyInstall(options: ClassifyInstallOptions): InstallKind;
8
+ export interface NpmSpawnerResult {
9
+ exitCode: number;
10
+ stderrText: string;
11
+ }
12
+ export type NpmSpawner = (command: string, args: readonly string[]) => Promise<NpmSpawnerResult>;
13
+ export interface NpmRunResult {
14
+ exitCode: number;
15
+ sawEacces: boolean;
16
+ }
17
+ export interface RunNpmInstallOptions {
18
+ packageName: string;
19
+ version: string;
20
+ npmBin: string;
21
+ spawner: NpmSpawner;
22
+ }
23
+ export declare function runNpmInstallGlobal(options: RunNpmInstallOptions): Promise<NpmRunResult>;
24
+ export declare function detectInstallPath(cliMetaUrl: string): string;
25
+ export type NpmRootRunner = (command: string, args: readonly string[]) => string;
26
+ export declare function detectNpmRootGlobal(npmBin: string, runner: NpmRootRunner): string | undefined;
27
+ export declare function detectIsSymlink(path: string): boolean;
28
+ export declare function createDefaultNpmSpawner(passthroughStderr: NodeJS.WritableStream): NpmSpawner;
29
+ //# sourceMappingURL=npmGlobal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npmGlobal.d.ts","sourceRoot":"","sources":["../../src/lib/npmGlobal.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,WAAW,CAY5E;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAEjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAO9F;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAEjF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAM7F;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED,wBAAgB,uBAAuB,CAAC,iBAAiB,EAAE,MAAM,CAAC,cAAc,GAAG,UAAU,CAmB5F"}
@@ -0,0 +1,62 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lstatSync } from "node:fs";
3
+ import { dirname, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ export function classifyInstall(options) {
6
+ const { installPath, npmRootGlobal, isSymlink } = options;
7
+ if (npmRootGlobal !== undefined && installPath.startsWith(`${npmRootGlobal}${sep}`)) {
8
+ return isSymlink(installPath) ? "linked" : "global";
9
+ }
10
+ if (installPath.includes(`${sep}_npx${sep}`)) {
11
+ return "npx";
12
+ }
13
+ if (installPath.includes(`${sep}node_modules${sep}`)) {
14
+ return "project";
15
+ }
16
+ return "unknown";
17
+ }
18
+ export async function runNpmInstallGlobal(options) {
19
+ const args = ["install", "-g", `${options.packageName}@${options.version}`];
20
+ const result = await options.spawner(options.npmBin, args);
21
+ return {
22
+ exitCode: result.exitCode,
23
+ sawEacces: result.stderrText.includes("EACCES"),
24
+ };
25
+ }
26
+ export function detectInstallPath(cliMetaUrl) {
27
+ return dirname(dirname(fileURLToPath(cliMetaUrl)));
28
+ }
29
+ export function detectNpmRootGlobal(npmBin, runner) {
30
+ try {
31
+ return runner(npmBin, ["root", "-g"]);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ export function detectIsSymlink(path) {
38
+ try {
39
+ return lstatSync(path).isSymbolicLink();
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function createDefaultNpmSpawner(passthroughStderr) {
46
+ return async (command, args) => await new Promise((resolve, reject) => {
47
+ const child = spawn(command, [...args], { stdio: ["inherit", "inherit", "pipe"] });
48
+ const { stderr } = child;
49
+ const chunks = [];
50
+ stderr.on("data", (chunk) => {
51
+ chunks.push(chunk);
52
+ passthroughStderr.write(chunk);
53
+ });
54
+ child.on("error", reject);
55
+ child.on("close", (code) => {
56
+ resolve({
57
+ exitCode: code ?? 1,
58
+ stderrText: Buffer.concat(chunks).toString("utf8"),
59
+ });
60
+ });
61
+ });
62
+ }
@@ -0,0 +1,66 @@
1
+ interface Version {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ }
6
+ export declare function parseVersion(version: string): Version;
7
+ export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
8
+ export declare function normalizeRegistry(registry: string | undefined): string;
9
+ export interface FetchOptions {
10
+ timeoutMs: number;
11
+ registry?: string | undefined;
12
+ }
13
+ export declare function fetchLatestVersion(packageName: string, options: FetchOptions): Promise<string>;
14
+ export interface UpgradeCheckCacheEntry {
15
+ latest: string;
16
+ fetchedAt: number;
17
+ registry: string;
18
+ }
19
+ export interface PrimeUpgradeCheckCacheOptions {
20
+ path: string;
21
+ latest: string;
22
+ registry?: string | undefined;
23
+ now: () => number;
24
+ }
25
+ export type UpgradeCheckCacheResult = {
26
+ kind: "missing";
27
+ } | {
28
+ kind: "fresh";
29
+ entry: UpgradeCheckCacheEntry;
30
+ } | {
31
+ kind: "stale";
32
+ entry: UpgradeCheckCacheEntry;
33
+ };
34
+ export declare function defaultUpgradeCheckCachePath(): string;
35
+ export declare function readUpgradeCheckCache(path: string, options: {
36
+ now: () => number;
37
+ ttlMs: number;
38
+ registry?: string | undefined;
39
+ }): UpgradeCheckCacheResult;
40
+ export declare function writeUpgradeCheckCache(path: string, entry: UpgradeCheckCacheEntry): void;
41
+ export declare function primeUpgradeCheckCache(options: PrimeUpgradeCheckCacheOptions): void;
42
+ export declare function composeNudgeMessage(current: string, latest: string): string | undefined;
43
+ export type VersionFetcher = (packageName: string, options: FetchOptions) => Promise<string>;
44
+ export interface FetchAndPrimeUpgradeCheckCacheOptions {
45
+ packageName: string;
46
+ cachePath: string;
47
+ fetchTimeoutMs: number;
48
+ registry?: string | undefined;
49
+ now: () => number;
50
+ fetcher: VersionFetcher;
51
+ }
52
+ export declare function fetchAndPrimeUpgradeCheckCache(options: FetchAndPrimeUpgradeCheckCacheOptions): Promise<string>;
53
+ export interface ComputeUpgradeNudgeOptions {
54
+ currentVersion: string;
55
+ packageName: string;
56
+ cachePath: string;
57
+ ttlMs: number;
58
+ fetchTimeoutMs: number;
59
+ registry?: string | undefined;
60
+ noUpgradeCheck: boolean;
61
+ now: () => number;
62
+ fetcher: VersionFetcher;
63
+ }
64
+ export declare function computeUpgradeNudge(options: ComputeUpgradeNudgeOptions): Promise<string | undefined>;
65
+ export {};
66
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade.ts"],"names":[],"mappings":"AAMA,UAAU,OAAO;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYrD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAShE;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtE;AAMD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,sBAAsB,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,sBAAsB,CAAA;CAAE,CAAC;AAErD,wBAAgB,4BAA4B,IAAI,MAAM,CAKrD;AA0BD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC3E,uBAAuB,CAsBzB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAGxF;AAUD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAMnF;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKvF;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7F,MAAM,WAAW,qCAAqC;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,qCAAqC,GAC7C,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,cAAc,EAAE,OAAO,CAAC;IACxB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqB7B"}
@@ -0,0 +1,178 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { errorMessage, readEnvironmentVariable } from "./util.js";
5
+ const NUMERIC_IDENTIFIER_PATTERN = String.raw `0|[1-9]\d*`;
6
+ const VERSION_RE = new RegExp(String.raw `^(${NUMERIC_IDENTIFIER_PATTERN})\.(${NUMERIC_IDENTIFIER_PATTERN})\.(${NUMERIC_IDENTIFIER_PATTERN})$`);
7
+ export function parseVersion(version) {
8
+ const match = VERSION_RE.exec(version);
9
+ if (!match) {
10
+ throw new Error(`invalid version: ${JSON.stringify(version)}`);
11
+ }
12
+ return {
13
+ // oxlint-disable typescript/no-non-null-assertion -- VERSION_RE guarantees groups 1–3 on match; group 4 is optional.
14
+ major: Number.parseInt(match[1], 10),
15
+ minor: Number.parseInt(match[2], 10),
16
+ patch: Number.parseInt(match[3], 10),
17
+ // oxlint-enable typescript/no-non-null-assertion
18
+ };
19
+ }
20
+ export function compareVersions(a, b) {
21
+ const left = parseVersion(a);
22
+ const right = parseVersion(b);
23
+ for (const key of ["major", "minor", "patch"]) {
24
+ if (left[key] !== right[key]) {
25
+ return left[key] > right[key] ? 1 : -1;
26
+ }
27
+ }
28
+ return 0;
29
+ }
30
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
31
+ export function normalizeRegistry(registry) {
32
+ return (registry ?? DEFAULT_REGISTRY).replace(/\/$/, "");
33
+ }
34
+ function encodePackageNameForRegistry(packageName) {
35
+ return encodeURIComponent(packageName).replace(/^%40/, "@");
36
+ }
37
+ export async function fetchLatestVersion(packageName, options) {
38
+ const registry = normalizeRegistry(options.registry);
39
+ const url = `${registry}/${encodePackageNameForRegistry(packageName)}/latest`;
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => {
42
+ controller.abort();
43
+ }, options.timeoutMs);
44
+ try {
45
+ let response;
46
+ try {
47
+ response = await fetch(url, { signal: controller.signal });
48
+ }
49
+ catch (error) {
50
+ throw new Error(`registry request failed: ${errorMessage(error)}`, { cause: error });
51
+ }
52
+ if (!response.ok) {
53
+ throw new Error(`registry returned ${response.status} for ${url}`);
54
+ }
55
+ const body = await response.json();
56
+ if (typeof body !== "object" || body === null || !("version" in body)) {
57
+ throw new TypeError(`registry response missing 'version' field`);
58
+ }
59
+ const { version } = body;
60
+ if (typeof version !== "string") {
61
+ throw new TypeError(`registry response 'version' field is not a string`);
62
+ }
63
+ parseVersion(version);
64
+ return version;
65
+ }
66
+ finally {
67
+ clearTimeout(timer);
68
+ }
69
+ }
70
+ export function defaultUpgradeCheckCachePath() {
71
+ const override = readEnvironmentVariable("XDG_CACHE_HOME");
72
+ const base = override === undefined || override.length === 0 ? join(homedir(), ".cache") : override;
73
+ return join(base, "groundcrew", "upgrade-check.json");
74
+ }
75
+ function parseCacheEntry(value) {
76
+ if (typeof value !== "object" || value === null) {
77
+ return undefined;
78
+ }
79
+ const candidate = value;
80
+ if (typeof candidate.latest !== "string" ||
81
+ typeof candidate.fetchedAt !== "number" ||
82
+ typeof candidate.registry !== "string") {
83
+ return undefined;
84
+ }
85
+ try {
86
+ parseVersion(candidate.latest);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ return { latest: candidate.latest, fetchedAt: candidate.fetchedAt, registry: candidate.registry };
92
+ }
93
+ export function readUpgradeCheckCache(path, options) {
94
+ let raw;
95
+ try {
96
+ raw = readFileSync(path, "utf8");
97
+ }
98
+ catch {
99
+ return { kind: "missing" };
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(raw);
104
+ }
105
+ catch {
106
+ return { kind: "missing" };
107
+ }
108
+ const entry = parseCacheEntry(parsed);
109
+ if (!entry) {
110
+ return { kind: "missing" };
111
+ }
112
+ if (entry.registry !== normalizeRegistry(options.registry)) {
113
+ return { kind: "missing" };
114
+ }
115
+ const ageMs = options.now() - entry.fetchedAt;
116
+ return ageMs >= options.ttlMs ? { kind: "stale", entry } : { kind: "fresh", entry };
117
+ }
118
+ export function writeUpgradeCheckCache(path, entry) {
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ writeFileSync(path, JSON.stringify(entry));
121
+ }
122
+ function writeUpgradeCheckCacheBestEffort(path, entry) {
123
+ try {
124
+ writeUpgradeCheckCache(path, entry);
125
+ }
126
+ catch {
127
+ // Upgrade-check cache writes are best-effort; callers should keep using current data.
128
+ }
129
+ }
130
+ export function primeUpgradeCheckCache(options) {
131
+ writeUpgradeCheckCacheBestEffort(options.path, {
132
+ latest: options.latest,
133
+ fetchedAt: options.now(),
134
+ registry: normalizeRegistry(options.registry),
135
+ });
136
+ }
137
+ export function composeNudgeMessage(current, latest) {
138
+ if (compareVersions(current, latest) >= 0) {
139
+ return undefined;
140
+ }
141
+ return `[crew] ${latest} available — run \`crew upgrade\` (you have ${current})`;
142
+ }
143
+ export async function fetchAndPrimeUpgradeCheckCache(options) {
144
+ const latest = await options.fetcher(options.packageName, {
145
+ timeoutMs: options.fetchTimeoutMs,
146
+ registry: options.registry,
147
+ });
148
+ primeUpgradeCheckCache({
149
+ path: options.cachePath,
150
+ latest,
151
+ registry: options.registry,
152
+ now: options.now,
153
+ });
154
+ return latest;
155
+ }
156
+ export async function computeUpgradeNudge(options) {
157
+ if (options.noUpgradeCheck) {
158
+ return undefined;
159
+ }
160
+ const cacheResult = readUpgradeCheckCache(options.cachePath, {
161
+ now: options.now,
162
+ ttlMs: options.ttlMs,
163
+ registry: options.registry,
164
+ });
165
+ if (cacheResult.kind === "fresh") {
166
+ return composeNudgeMessage(options.currentVersion, cacheResult.entry.latest);
167
+ }
168
+ try {
169
+ const latest = await fetchAndPrimeUpgradeCheckCache(options);
170
+ return composeNudgeMessage(options.currentVersion, latest);
171
+ }
172
+ catch {
173
+ if (cacheResult.kind === "stale") {
174
+ return composeNudgeMessage(options.currentVersion, cacheResult.entry.latest);
175
+ }
176
+ return undefined;
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "3.2.1",
3
+ "version": "3.4.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",