@clipboard-health/groundcrew 4.1.0 → 4.2.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 +25 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -42
- package/dist/commands/upgrade.d.ts +0 -11
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +14 -100
- package/package.json +1 -1
- package/dist/commands/setupRepos.d.ts +0 -44
- package/dist/commands/setupRepos.d.ts.map +0 -1
- package/dist/commands/setupRepos.js +0 -212
- package/dist/lib/upgrade.d.ts +0 -66
- package/dist/lib/upgrade.d.ts.map +0 -1
- package/dist/lib/upgrade.js +0 -178
package/README.md
CHANGED
|
@@ -63,7 +63,9 @@ npm install -g @clipboard-health/groundcrew
|
|
|
63
63
|
crew init && $EDITOR crew.config.ts
|
|
64
64
|
|
|
65
65
|
# 4. Clone the repos referenced in your config
|
|
66
|
-
|
|
66
|
+
PROJECT_DIR="$HOME/dev/c"
|
|
67
|
+
mkdir -p "$PROJECT_DIR/OWNER"
|
|
68
|
+
git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
|
|
67
69
|
|
|
68
70
|
# 5. Export your Linear API key
|
|
69
71
|
export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
|
|
@@ -86,14 +88,35 @@ crew status [<TICKET>] # inspect current state
|
|
|
86
88
|
crew run # one-shot orchestration
|
|
87
89
|
crew run --watch # poll forever
|
|
88
90
|
crew start <TICKET> # provision + launch one ticket now
|
|
89
|
-
crew setup repos [<repo>...] [--dry-run] # clone known repos via gh
|
|
90
91
|
crew stop <TICKET> [--reason <text>] # stop workspace, keep worktree
|
|
91
92
|
crew resume <TICKET> # reopen a paused ticket
|
|
92
93
|
crew cleanup <TICKET> # tear down every worktree for a ticket
|
|
94
|
+
crew upgrade [<version>] # reinstall crew globally through npm
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
Deprecated aliases still work but print a warning and will be removed in the next major version: `crew interrupt` → `crew stop`, `crew run --ticket <TICKET>` → `crew start <TICKET>`, `crew doctor --ticket <TICKET>` → `crew status <TICKET>`.
|
|
96
98
|
|
|
99
|
+
## Manual Repository Bootstrap
|
|
100
|
+
|
|
101
|
+
Groundcrew no longer clones repositories for you. For each `workspace.knownRepositories` entry,
|
|
102
|
+
clone the repository into `workspace.projectDir` using the same relative path that appears in the
|
|
103
|
+
config. For an `OWNER/REPO` entry:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
PROJECT_DIR="$HOME/dev/c"
|
|
107
|
+
mkdir -p "$PROJECT_DIR/OWNER"
|
|
108
|
+
git clone git@github.com:OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
HTTPS works the same way if you do not use SSH:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/OWNER/REPO.git "$PROJECT_DIR/OWNER/REPO"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Bare-name entries do not include an owner, so choose the correct remote URL yourself and clone it to
|
|
118
|
+
`$PROJECT_DIR/<name>`. `crew setup repos` now exits non-zero and points back to this section.
|
|
119
|
+
|
|
97
120
|
## Configuration
|
|
98
121
|
|
|
99
122
|
Two keys are required; everything else has a default.
|
|
@@ -508,13 +531,6 @@ The handoff is `<your cmd> "<prompt>"`. `claude`, `codex`, and `cursor-agent` al
|
|
|
508
531
|
|
|
509
532
|
</details>
|
|
510
533
|
|
|
511
|
-
<details>
|
|
512
|
-
<summary><code>crew setup repos</code> only auto-clones <code>owner/repo</code> entries</summary>
|
|
513
|
-
|
|
514
|
-
Bare-name entries in `workspace.knownRepositories` (e.g. `"api"` rather than `"clipboardhealth/api"`) are skipped with a hint to clone manually — the command refuses to guess the owner. After a partial setup, the exit code is non-zero so CI gates notice; rerun is idempotent once you clone the bare ones into `<projectDir>/<name>` yourself.
|
|
515
|
-
|
|
516
|
-
</details>
|
|
517
|
-
|
|
518
534
|
## Development
|
|
519
535
|
|
|
520
536
|
Clone the repo and the `crew` / `crew:op` scripts execute straight from TypeScript source — no build step needed.
|
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":"AAmPA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCvD"}
|
package/dist/cli.js
CHANGED
|
@@ -5,20 +5,21 @@ import { initConfigCli } from "./commands/init.js";
|
|
|
5
5
|
import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
|
|
6
6
|
import { orchestrate } from "./commands/orchestrator.js";
|
|
7
7
|
import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
|
|
8
|
-
import { setupReposCli } from "./commands/setupRepos.js";
|
|
9
8
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
10
9
|
import { statusCli } from "./commands/status.js";
|
|
11
10
|
import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
|
|
12
|
-
import {
|
|
13
|
-
import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
|
|
14
|
-
const NUDGE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
15
|
-
const NUDGE_FETCH_TIMEOUT_MS = 1000;
|
|
11
|
+
import { errorMessage, parseDryRunPositionals, readTicketArgument, writeError, writeOutput, } from "./lib/util.js";
|
|
16
12
|
const REMOVED_SANDBOX_COMMAND_MESSAGE = [
|
|
17
13
|
"`crew sandbox` is no longer supported.",
|
|
18
14
|
"Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
|
|
19
15
|
"Use the manual `sbx` workflow in README.md#docker-sandboxes-sdx-setup, then keep `models.definitions.<model>.sandbox.agent` in crew.config.ts so launches can address the existing sandbox.",
|
|
20
16
|
].join("\n");
|
|
21
17
|
const requireFromCli = createRequire(import.meta.url);
|
|
18
|
+
const SETUP_REPOS_REMOVED_MESSAGE = [
|
|
19
|
+
"crew setup repos was removed.",
|
|
20
|
+
"Clone repositories manually with git clone into workspace.projectDir.",
|
|
21
|
+
"See README.md#manual-repository-bootstrap for the replacement workflow.",
|
|
22
|
+
].join(" ");
|
|
22
23
|
/**
|
|
23
24
|
* Prints a deprecation warning to stderr naming the canonical command and that
|
|
24
25
|
* the old form is removed in the next major, then lets the caller proceed.
|
|
@@ -27,13 +28,12 @@ function warnDeprecated(forms) {
|
|
|
27
28
|
writeError(`crew ${forms.oldForm} is deprecated and will be removed in the next major version; use crew ${forms.newForm} instead.`);
|
|
28
29
|
}
|
|
29
30
|
function setupUsage() {
|
|
30
|
-
return
|
|
31
|
+
return `Usage: crew setup repos\n\n${SETUP_REPOS_REMOVED_MESSAGE}`;
|
|
31
32
|
}
|
|
32
33
|
async function setupCli(argv) {
|
|
33
|
-
const [verb
|
|
34
|
+
const [verb] = argv;
|
|
34
35
|
if (verb === "repos") {
|
|
35
|
-
|
|
36
|
-
return;
|
|
36
|
+
throw new Error(SETUP_REPOS_REMOVED_MESSAGE);
|
|
37
37
|
}
|
|
38
38
|
throw new Error(setupUsage());
|
|
39
39
|
}
|
|
@@ -80,27 +80,10 @@ async function startCli(argv) {
|
|
|
80
80
|
async function upgradeCliInvoke(argv) {
|
|
81
81
|
const metadata = packageMetadata();
|
|
82
82
|
await upgradeCli(argv, async () => await createDefaultUpgradeCliOptions({
|
|
83
|
-
currentVersion: metadata.version,
|
|
84
83
|
packageName: metadata.name,
|
|
85
84
|
cliMetaUrl: import.meta.url,
|
|
86
85
|
}));
|
|
87
86
|
}
|
|
88
|
-
async function maybeRunUpgradeNudge(metadata) {
|
|
89
|
-
const message = await computeUpgradeNudge({
|
|
90
|
-
currentVersion: metadata.version,
|
|
91
|
-
packageName: metadata.name,
|
|
92
|
-
cachePath: defaultUpgradeCheckCachePath(),
|
|
93
|
-
ttlMs: NUDGE_TTL_MS,
|
|
94
|
-
fetchTimeoutMs: NUDGE_FETCH_TIMEOUT_MS,
|
|
95
|
-
registry: readEnvironmentVariable("npm_config_registry"),
|
|
96
|
-
noUpgradeCheck: readEnvironmentVariable("GROUNDCREW_NO_UPGRADE_CHECK") === "1",
|
|
97
|
-
now: Date.now,
|
|
98
|
-
fetcher: fetchLatestVersion,
|
|
99
|
-
});
|
|
100
|
-
if (message !== undefined) {
|
|
101
|
-
writeError(message);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
87
|
function doctorTicketAlias(argv) {
|
|
105
88
|
if (argv[0] !== "--ticket") {
|
|
106
89
|
return undefined;
|
|
@@ -175,28 +158,27 @@ const SUBCOMMANDS = {
|
|
|
175
158
|
invoke: resumeWorkspaceCli,
|
|
176
159
|
},
|
|
177
160
|
setup: {
|
|
178
|
-
summary: "
|
|
179
|
-
usage: "repos
|
|
161
|
+
summary: "Removed repository bootstrap command",
|
|
162
|
+
usage: "repos",
|
|
163
|
+
hidden: true,
|
|
180
164
|
invoke: setupCli,
|
|
181
165
|
},
|
|
182
166
|
upgrade: {
|
|
183
167
|
summary: "Install the latest version of crew (or pin to a specific version)",
|
|
184
|
-
usage: "[<version>]
|
|
168
|
+
usage: "[<version>]",
|
|
185
169
|
invoke: upgradeCliInvoke,
|
|
186
170
|
},
|
|
187
171
|
};
|
|
188
172
|
function printHelp() {
|
|
189
|
-
const
|
|
173
|
+
const visibleCommands = Object.entries(SUBCOMMANDS).filter(([, command]) => command.hidden !== true && command.deprecated !== true);
|
|
174
|
+
const width = Math.max(...visibleCommands.map(([key]) => key.length));
|
|
190
175
|
writeOutput("Usage: crew <command> [...args]\n");
|
|
191
176
|
writeOutput("Options:");
|
|
192
177
|
writeOutput(" -h, --help Show help");
|
|
193
178
|
writeOutput(" -v, --version Print version");
|
|
194
179
|
writeOutput("");
|
|
195
180
|
writeOutput("Commands:");
|
|
196
|
-
for (const [name, command] of
|
|
197
|
-
if (command.deprecated === true) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
181
|
+
for (const [name, command] of visibleCommands) {
|
|
200
182
|
writeOutput(` ${name.padEnd(width)} ${command.summary}`);
|
|
201
183
|
writeOutput(` ${" ".repeat(width)} → crew ${name} ${command.usage}`);
|
|
202
184
|
}
|
|
@@ -235,14 +217,6 @@ export async function run(argv) {
|
|
|
235
217
|
process.exitCode = 1;
|
|
236
218
|
return;
|
|
237
219
|
}
|
|
238
|
-
if (subcommand !== "upgrade") {
|
|
239
|
-
try {
|
|
240
|
-
await maybeRunUpgradeNudge(packageMetadata());
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// Passive nudge is never load-bearing; never block the user's command.
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
220
|
try {
|
|
247
221
|
await command.invoke(rest);
|
|
248
222
|
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { type InstallKind, type NpmRunResult } from "../lib/npmGlobal.ts";
|
|
2
|
-
import { type VersionFetcher } from "../lib/upgrade.ts";
|
|
3
2
|
export interface UpgradeCliOptions {
|
|
4
|
-
currentVersion: string;
|
|
5
3
|
packageName: string;
|
|
6
4
|
resolveInstall: () => Promise<UpgradeInstallDetails>;
|
|
7
|
-
fetcher: VersionFetcher;
|
|
8
5
|
runInstall: (options: {
|
|
9
6
|
packageName: string;
|
|
10
7
|
version: string;
|
|
11
8
|
npmBin: string;
|
|
12
9
|
}) => 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
10
|
}
|
|
21
11
|
export interface UpgradeInstallDetails {
|
|
22
12
|
installKind: InstallKind;
|
|
@@ -26,7 +16,6 @@ export interface UpgradeInstallDetails {
|
|
|
26
16
|
export type UpgradeCliOptionsInput = UpgradeCliOptions | (() => Promise<UpgradeCliOptions>);
|
|
27
17
|
export declare function upgradeCli(argv: string[], optionsInput: UpgradeCliOptionsInput): Promise<void>;
|
|
28
18
|
export interface CreateUpgradeOptionsArgs {
|
|
29
|
-
currentVersion: string;
|
|
30
19
|
packageName: string;
|
|
31
20
|
cliMetaUrl: string;
|
|
32
21
|
}
|
|
@@ -1 +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;
|
|
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;AAK7B,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACrD,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;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAOD,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,CAAC,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAiD5F,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EAAE,EACd,YAAY,EAAE,sBAAsB,GACnC,OAAO,CAAC,IAAI,CAAC,CAkBf;AAsCD,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,iBAAiB,CAAC,CAqB5B"}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,46 +1,36 @@
|
|
|
1
1
|
import { runCommand } from "../lib/commandRunner.js";
|
|
2
2
|
import { which } from "../lib/host.js";
|
|
3
3
|
import { classifyInstall, createDefaultNpmSpawner, detectInstallPath, detectIsSymlink, detectNpmRootGlobal, runNpmInstallGlobal, } from "../lib/npmGlobal.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const EXPLICIT_FETCH_TIMEOUT_MS = 5000;
|
|
4
|
+
import { writeError, writeOutput } from "../lib/util.js";
|
|
5
|
+
const DEFAULT_UPGRADE_TARGET = "latest";
|
|
7
6
|
function parseArgs(argv) {
|
|
8
|
-
let
|
|
9
|
-
let pinnedVersion;
|
|
7
|
+
let version;
|
|
10
8
|
for (const arg of argv) {
|
|
11
9
|
if (arg === "--help" || arg === "-h") {
|
|
12
10
|
return { kind: "help" };
|
|
13
11
|
}
|
|
14
|
-
if (arg === "--check") {
|
|
15
|
-
check = true;
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
12
|
if (arg.startsWith("-")) {
|
|
19
13
|
return { kind: "error", message: `crew upgrade: unknown argument: ${arg}` };
|
|
20
14
|
}
|
|
21
|
-
if (
|
|
15
|
+
if (arg.length === 0) {
|
|
16
|
+
return { kind: "error", message: "crew upgrade: version cannot be empty" };
|
|
17
|
+
}
|
|
18
|
+
if (version !== undefined) {
|
|
22
19
|
return { kind: "error", message: "crew upgrade: too many positional arguments" };
|
|
23
20
|
}
|
|
24
|
-
|
|
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" };
|
|
21
|
+
version = arg;
|
|
31
22
|
}
|
|
32
|
-
return { kind: "install",
|
|
23
|
+
return { kind: "install", version: version ?? DEFAULT_UPGRADE_TARGET };
|
|
33
24
|
}
|
|
34
25
|
function printHelp() {
|
|
35
|
-
writeOutput("Usage: crew upgrade [<version>]
|
|
26
|
+
writeOutput("Usage: crew upgrade [<version>]");
|
|
36
27
|
writeOutput("");
|
|
37
|
-
writeOutput("Install
|
|
28
|
+
writeOutput("Install crew globally through npm.");
|
|
38
29
|
writeOutput("");
|
|
39
30
|
writeOutput("Arguments:");
|
|
40
|
-
writeOutput(" <version> Install an exact version (
|
|
31
|
+
writeOutput(" <version> Install an exact version or npm tag (default: latest)");
|
|
41
32
|
writeOutput("");
|
|
42
33
|
writeOutput("Options:");
|
|
43
|
-
writeOutput(" --check Report availability without installing");
|
|
44
34
|
writeOutput(" -h, --help Show this help");
|
|
45
35
|
}
|
|
46
36
|
function refusalMessage(kind, installPath, packageName) {
|
|
@@ -64,34 +54,11 @@ export async function upgradeCli(argv, optionsInput) {
|
|
|
64
54
|
return;
|
|
65
55
|
}
|
|
66
56
|
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
57
|
const npmBin = await resolveGlobalNpmBin(options);
|
|
91
58
|
if (npmBin === undefined) {
|
|
92
59
|
return;
|
|
93
60
|
}
|
|
94
|
-
await runInstallAndReport(options, npmBin,
|
|
61
|
+
await runInstallAndReport(options, npmBin, parsed.version);
|
|
95
62
|
}
|
|
96
63
|
async function resolveGlobalNpmBin(options) {
|
|
97
64
|
const install = await options.resolveInstall();
|
|
@@ -107,53 +74,6 @@ async function resolveGlobalNpmBin(options) {
|
|
|
107
74
|
}
|
|
108
75
|
return install.npmBin;
|
|
109
76
|
}
|
|
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
77
|
async function runInstallAndReport(options, npmBin, version) {
|
|
158
78
|
const result = await options.runInstall({
|
|
159
79
|
packageName: options.packageName,
|
|
@@ -164,13 +84,12 @@ async function runInstallAndReport(options, npmBin, version) {
|
|
|
164
84
|
return;
|
|
165
85
|
}
|
|
166
86
|
if (result.sawEacces) {
|
|
167
|
-
writeError("crew upgrade: install failed with EACCES (permission denied). Your global npm prefix may require elevated permissions
|
|
87
|
+
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
88
|
}
|
|
169
89
|
process.exitCode = result.exitCode;
|
|
170
90
|
}
|
|
171
91
|
export async function createDefaultUpgradeCliOptions(args) {
|
|
172
92
|
return {
|
|
173
|
-
currentVersion: args.currentVersion,
|
|
174
93
|
packageName: args.packageName,
|
|
175
94
|
resolveInstall: async () => {
|
|
176
95
|
const installPath = detectInstallPath(args.cliMetaUrl);
|
|
@@ -183,14 +102,9 @@ export async function createDefaultUpgradeCliOptions(args) {
|
|
|
183
102
|
});
|
|
184
103
|
return { installKind, installPath, npmBin };
|
|
185
104
|
},
|
|
186
|
-
fetcher: fetchLatestVersion,
|
|
187
105
|
runInstall: async (options) => await runNpmInstallGlobal({
|
|
188
106
|
...options,
|
|
189
107
|
spawner: createDefaultNpmSpawner(process.stderr),
|
|
190
108
|
}),
|
|
191
|
-
fetchTimeoutMs: EXPLICIT_FETCH_TIMEOUT_MS,
|
|
192
|
-
registry: readEnvironmentVariable("npm_config_registry"),
|
|
193
|
-
cachePath: defaultUpgradeCheckCachePath(),
|
|
194
|
-
now: Date.now,
|
|
195
109
|
};
|
|
196
110
|
}
|
package/package.json
CHANGED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `crew setup repos` — clone every entry of `workspace.knownRepositories`
|
|
3
|
-
* that does not already exist under `workspace.projectDir`. Entries
|
|
4
|
-
* shaped `<owner>/<repo>` are cloned via `gh repo clone`; bare-name
|
|
5
|
-
* entries are skipped with a hint, because they have no canonical URL
|
|
6
|
-
* we can guess at without involving the user's gh login. Idempotent.
|
|
7
|
-
*/
|
|
8
|
-
import { type ResolvedConfig } from "../lib/config.ts";
|
|
9
|
-
export interface SetupReposOptions {
|
|
10
|
-
/** Print the plan without running any clone. */
|
|
11
|
-
dryRun?: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Restrict the action to this subset of `knownRepositories`. Each entry
|
|
14
|
-
* must match an entry in the config or the call rejects before any side
|
|
15
|
-
* effect.
|
|
16
|
-
*/
|
|
17
|
-
only?: readonly string[];
|
|
18
|
-
}
|
|
19
|
-
export type SetupReposSkipKind = "bare-name" | "invalid-repository" | "invalid-target";
|
|
20
|
-
export interface SetupReposSkip {
|
|
21
|
-
repo: string;
|
|
22
|
-
kind: SetupReposSkipKind;
|
|
23
|
-
reason: string;
|
|
24
|
-
}
|
|
25
|
-
export interface SetupReposResult {
|
|
26
|
-
/** Entries already present under `projectDir`. */
|
|
27
|
-
existing: string[];
|
|
28
|
-
/** Entries that would be cloned in dry-run mode. */
|
|
29
|
-
planned: string[];
|
|
30
|
-
/** Entries successfully cloned this run. */
|
|
31
|
-
cloned: string[];
|
|
32
|
-
/** Entries skipped with a reason (e.g. bare names, invalid targets). */
|
|
33
|
-
skipped: SetupReposSkip[];
|
|
34
|
-
/** Entries that failed during clone. */
|
|
35
|
-
failed: {
|
|
36
|
-
repo: string;
|
|
37
|
-
error: Error;
|
|
38
|
-
}[];
|
|
39
|
-
/** True when `gh` is missing and at least one clone was needed. */
|
|
40
|
-
ghMissing: boolean;
|
|
41
|
-
}
|
|
42
|
-
export declare function setupRepos(config: ResolvedConfig, options: SetupReposOptions): Promise<SetupReposResult>;
|
|
43
|
-
export declare function setupReposCli(argv: string[]): Promise<void>;
|
|
44
|
-
//# sourceMappingURL=setupRepos.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setupRepos.d.ts","sourceRoot":"","sources":["../../src/commands/setupRepos.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AAEvF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wCAAwC;IACxC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,CAAC;IACzC,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACpB;AA6JD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B;AAcD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAajE"}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `crew setup repos` — clone every entry of `workspace.knownRepositories`
|
|
3
|
-
* that does not already exist under `workspace.projectDir`. Entries
|
|
4
|
-
* shaped `<owner>/<repo>` are cloned via `gh repo clone`; bare-name
|
|
5
|
-
* entries are skipped with a hint, because they have no canonical URL
|
|
6
|
-
* we can guess at without involving the user's gh login. Idempotent.
|
|
7
|
-
*/
|
|
8
|
-
import { mkdirSync, opendirSync, statSync } from "node:fs";
|
|
9
|
-
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
10
|
-
import { runCommandAsync } from "../lib/commandRunner.js";
|
|
11
|
-
import { loadConfig } from "../lib/config.js";
|
|
12
|
-
import { which } from "../lib/host.js";
|
|
13
|
-
import { errorMessage, log, parseDryRunPositionals, writeOutput } from "../lib/util.js";
|
|
14
|
-
function emptyResult() {
|
|
15
|
-
return {
|
|
16
|
-
existing: [],
|
|
17
|
-
planned: [],
|
|
18
|
-
cloned: [],
|
|
19
|
-
skipped: [],
|
|
20
|
-
failed: [],
|
|
21
|
-
ghMissing: false,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
function selectRepositories(config, only) {
|
|
25
|
-
if (only === undefined) {
|
|
26
|
-
return config.workspace.knownRepositories;
|
|
27
|
-
}
|
|
28
|
-
const known = new Set(config.workspace.knownRepositories);
|
|
29
|
-
const unknown = only.filter((entry) => !known.has(entry));
|
|
30
|
-
if (unknown.length > 0) {
|
|
31
|
-
throw new Error(`Repositories not in workspace.knownRepositories: ${unknown.join(", ")}. Known: ${config.workspace.knownRepositories.join(", ")}`);
|
|
32
|
-
}
|
|
33
|
-
return only;
|
|
34
|
-
}
|
|
35
|
-
function pathExists(path) {
|
|
36
|
-
return statSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
37
|
-
}
|
|
38
|
-
function isDirectoryEmpty(path) {
|
|
39
|
-
const directory = opendirSync(path);
|
|
40
|
-
try {
|
|
41
|
-
return directory.readSync() === null;
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
directory.closeSync();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function existingTargetPlan(target) {
|
|
48
|
-
const stats = statSync(target, { throwIfNoEntry: false });
|
|
49
|
-
if (stats === undefined) {
|
|
50
|
-
return "clone";
|
|
51
|
-
}
|
|
52
|
-
if (!stats.isDirectory()) {
|
|
53
|
-
return "skip-invalid";
|
|
54
|
-
}
|
|
55
|
-
if (pathExists(resolve(target, ".git"))) {
|
|
56
|
-
return "existing";
|
|
57
|
-
}
|
|
58
|
-
return isDirectoryEmpty(target) ? "clone" : "skip-invalid";
|
|
59
|
-
}
|
|
60
|
-
function isInsideProjectDir(projectDir, target) {
|
|
61
|
-
const relativeTarget = relative(projectDir, target);
|
|
62
|
-
return (relativeTarget.length > 0 && !relativeTarget.startsWith("..") && !isAbsolute(relativeTarget));
|
|
63
|
-
}
|
|
64
|
-
function repositoryEntryPlan(repo) {
|
|
65
|
-
const parts = repo.split("/");
|
|
66
|
-
if (parts.length === 1) {
|
|
67
|
-
return "bare-name";
|
|
68
|
-
}
|
|
69
|
-
if (parts.length === 2 && parts.every((part) => part.length > 0)) {
|
|
70
|
-
return "clone";
|
|
71
|
-
}
|
|
72
|
-
return "invalid-repository";
|
|
73
|
-
}
|
|
74
|
-
function bareNameSkip(repo, target) {
|
|
75
|
-
return {
|
|
76
|
-
repo,
|
|
77
|
-
kind: "bare-name",
|
|
78
|
-
reason: `bare name needs owner/ prefix to auto-clone; clone manually into ${target}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function invalidTargetSkip(repo, target) {
|
|
82
|
-
return {
|
|
83
|
-
repo,
|
|
84
|
-
kind: "invalid-target",
|
|
85
|
-
reason: `target exists but is not a git repository or empty directory: ${target}`,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function invalidRepositorySkip(repo, target) {
|
|
89
|
-
return {
|
|
90
|
-
repo,
|
|
91
|
-
kind: "invalid-repository",
|
|
92
|
-
reason: `repository must be owner/repo to auto-clone; clone manually into ${target}`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function escapingTargetSkip(repo, projectDir, target) {
|
|
96
|
-
return {
|
|
97
|
-
repo,
|
|
98
|
-
kind: "invalid-repository",
|
|
99
|
-
reason: `repository resolves outside workspace.projectDir (${projectDir}): ${target}`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function planClones(config, repositories) {
|
|
103
|
-
const projectDir = resolve(config.workspace.projectDir);
|
|
104
|
-
const toClone = [];
|
|
105
|
-
const existing = [];
|
|
106
|
-
const skipped = [];
|
|
107
|
-
const seen = new Set();
|
|
108
|
-
for (const entry of repositories) {
|
|
109
|
-
if (seen.has(entry)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
seen.add(entry);
|
|
113
|
-
const target = resolve(projectDir, entry);
|
|
114
|
-
if (!isInsideProjectDir(projectDir, target)) {
|
|
115
|
-
skipped.push(escapingTargetSkip(entry, projectDir, target));
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const targetPlan = existingTargetPlan(target);
|
|
119
|
-
if (targetPlan === "existing") {
|
|
120
|
-
existing.push(entry);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (targetPlan === "skip-invalid") {
|
|
124
|
-
skipped.push(invalidTargetSkip(entry, target));
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const repositoryPlan = repositoryEntryPlan(entry);
|
|
128
|
-
if (repositoryPlan === "bare-name") {
|
|
129
|
-
skipped.push(bareNameSkip(entry, target));
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (repositoryPlan === "invalid-repository") {
|
|
133
|
-
skipped.push(invalidRepositorySkip(entry, target));
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
toClone.push(entry);
|
|
137
|
-
}
|
|
138
|
-
return { toClone, existing, skipped };
|
|
139
|
-
}
|
|
140
|
-
export async function setupRepos(config, options) {
|
|
141
|
-
const repositories = selectRepositories(config, options.only);
|
|
142
|
-
const plan = planClones(config, repositories);
|
|
143
|
-
const result = emptyResult();
|
|
144
|
-
result.existing = plan.existing;
|
|
145
|
-
result.skipped = plan.skipped;
|
|
146
|
-
for (const entry of plan.existing) {
|
|
147
|
-
log(`[exists] ${entry}`);
|
|
148
|
-
}
|
|
149
|
-
for (const { repo, reason } of plan.skipped) {
|
|
150
|
-
log(`[skip] ${repo} — ${reason}`);
|
|
151
|
-
}
|
|
152
|
-
if (options.dryRun === true) {
|
|
153
|
-
result.planned = plan.toClone;
|
|
154
|
-
for (const entry of plan.toClone) {
|
|
155
|
-
log(`[dry-run] would clone ${entry}`);
|
|
156
|
-
}
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
if (plan.toClone.length === 0) {
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
const ghPath = await which("gh");
|
|
163
|
-
if (ghPath === undefined) {
|
|
164
|
-
result.ghMissing = true;
|
|
165
|
-
writeOutput("gh CLI not found - install GitHub CLI from https://cli.github.com/ (or clone the missing repos manually).");
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
const projectDir = resolve(config.workspace.projectDir);
|
|
169
|
-
// Sequential on purpose: each `gh repo clone` inherits stdio for progress
|
|
170
|
-
// bars and auth prompts. Parallel clones would interleave output and make
|
|
171
|
-
// any interactive 2FA prompt unanswerable.
|
|
172
|
-
for (const entry of plan.toClone) {
|
|
173
|
-
const target = resolve(projectDir, entry);
|
|
174
|
-
log(`[clone] ${entry} → ${target}`);
|
|
175
|
-
try {
|
|
176
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
177
|
-
// oxlint-disable-next-line no-await-in-loop -- see comment above
|
|
178
|
-
await runCommandAsync("gh", ["repo", "clone", entry, target], {
|
|
179
|
-
stdio: "inherit",
|
|
180
|
-
timeoutMs: 0,
|
|
181
|
-
});
|
|
182
|
-
result.cloned.push(entry);
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
const wrapped = error instanceof Error ? error : new Error(errorMessage(error));
|
|
186
|
-
log(`[fail] ${entry}: ${wrapped.message}`);
|
|
187
|
-
result.failed.push({ repo: entry, error: wrapped });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
function parseArguments(argv) {
|
|
193
|
-
const { dryRun, positionals } = parseDryRunPositionals(argv, "crew setup repos [--dry-run] [<repo>...]");
|
|
194
|
-
const options = { dryRun };
|
|
195
|
-
if (positionals.length > 0) {
|
|
196
|
-
options.only = positionals;
|
|
197
|
-
}
|
|
198
|
-
return options;
|
|
199
|
-
}
|
|
200
|
-
export async function setupReposCli(argv) {
|
|
201
|
-
const options = parseArguments(argv);
|
|
202
|
-
const config = await loadConfig();
|
|
203
|
-
const result = await setupRepos(config, options);
|
|
204
|
-
if (result.ghMissing || result.failed.length > 0) {
|
|
205
|
-
process.exitCode = 1;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// Remaining skips mean setup is incomplete — signal that to CI gates.
|
|
209
|
-
if (result.skipped.length > 0) {
|
|
210
|
-
process.exitCode = 1;
|
|
211
|
-
}
|
|
212
|
-
}
|
package/dist/lib/upgrade.d.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/upgrade.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
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
|
-
}
|