@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 +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -4
- package/dist/commands/upgrade.d.ts +34 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +196 -0
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/npmGlobal.d.ts +29 -0
- package/dist/lib/npmGlobal.d.ts.map +1 -0
- package/dist/lib/npmGlobal.js +62 -0
- package/dist/lib/upgrade.d.ts +66 -0
- package/dist/lib/upgrade.d.ts.map +1 -0
- package/dist/lib/upgrade.js +178 -0
- package/package.json +1 -1
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
|
|
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
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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
|
|
103
|
+
* Keep this agent-native (e.g., `claude --permission-mode auto`).
|
|
104
104
|
* Groundcrew adds the Safehouse wrapper.
|
|
105
105
|
*/
|
|
106
106
|
cmd: string;
|
package/dist/lib/config.js
CHANGED
|
@@ -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":"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"}
|
|
@@ -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