ghostrail 0.15.0 → 0.15.2
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 +203 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +64 -40
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/update-diagnosis.d.ts +52 -0
- package/dist/cli/update-diagnosis.d.ts.map +1 -0
- package/dist/cli/update-diagnosis.js +87 -0
- package/dist/cli/update-diagnosis.js.map +1 -0
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/server.d.ts +20 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +17 -4
- package/dist/dashboard/server.js.map +1 -1
- package/dist/factory/state.d.ts +1 -0
- package/dist/factory/state.d.ts.map +1 -1
- package/dist/factory/state.js +44 -1
- package/dist/factory/state.js.map +1 -1
- package/dist/global/index.d.ts +1 -1
- package/dist/global/index.d.ts.map +1 -1
- package/dist/global/index.js +1 -1
- package/dist/global/index.js.map +1 -1
- package/dist/global/registry.d.ts +46 -0
- package/dist/global/registry.d.ts.map +1 -1
- package/dist/global/registry.js +63 -2
- package/dist/global/registry.js.map +1 -1
- package/package.json +1 -1
- package/skill/ghostrail/SKILL.md +24 -4
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Ghostrail turns a repo into a factory by dropping in one config file and one pro
|
|
|
10
10
|
|
|
11
11
|
A software factory is thin glue over three primitive layers: where work comes from, what agent does the work, and where the work runs in isolation. Most people rebuild that glue per repo. Ghostrail extracts it once, so the tracker, the agent job, the gate, and the isolation backend are all configuration:
|
|
12
12
|
|
|
13
|
-
- **Config-driven.** A `ghostrail.toml` plus a prompt file is the whole factory.
|
|
13
|
+
- **Config-driven.** A [`ghostrail.toml`](#configuration-reference) plus a prompt file is the whole factory.
|
|
14
14
|
- **Pluggable backends.** `local-worktree` and `container-per-run` ship first-class; Firecracker, E2B, and remote sandboxes are adapters behind the same five-verb interface.
|
|
15
15
|
- **Pluggable agents.** Claude Code headless is the day-one agent, behind an `[agent]` seam so other agents can slot in.
|
|
16
16
|
- **Local and simple by default.** The quickstart runs on your own hardware with zero infrastructure and no additional cost. Cloud backends and a hosted board are opt-in.
|
|
@@ -26,6 +26,208 @@ ghostrail respond # fold new human PR comments back into branch
|
|
|
26
26
|
ghostrail board # watch runs at http://127.0.0.1:5050/
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Configuration reference
|
|
30
|
+
|
|
31
|
+
`ghostrail.toml` is the whole factory: one file, one table per adapter seam.
|
|
32
|
+
Every key below is read and validated in
|
|
33
|
+
[`src/config/load.ts`](./src/config/load.ts); the defaults come from
|
|
34
|
+
[`src/config/schema.ts`](./src/config/schema.ts) — this table is checked
|
|
35
|
+
against that code, not against the illustrative templates under
|
|
36
|
+
[`templates/`](./templates) or [`examples/`](./examples), which drift.
|
|
37
|
+
|
|
38
|
+
A few things apply across every table, worth knowing before you read them:
|
|
39
|
+
|
|
40
|
+
- **Durations are strings.** `timeout` and `claim_ttl` are parsed by a small
|
|
41
|
+
parser that accepts a whole positive number plus one unit: `"30m"`, `"2h"`,
|
|
42
|
+
`"90s"`. A bare number (`30`) is a validation error, not minutes.
|
|
43
|
+
- **An invalid value does not stop the rest of the file from validating.**
|
|
44
|
+
Every key is checked independently: a missing or malformed value is
|
|
45
|
+
recorded as an error and that one key falls back to its default, so one
|
|
46
|
+
typo doesn't hide problems elsewhere in the file. That said, `ghostrail
|
|
47
|
+
run`/`watch` still refuse to start if *any* error was recorded — look for
|
|
48
|
+
`config error at <path>: ...` lines on stderr. A couple of lookups made
|
|
49
|
+
before a run starts (like the factory name in early log output) instead
|
|
50
|
+
treat a config that fails to validate as absent and quietly fall back to
|
|
51
|
+
their own hardcoded default, without printing anything.
|
|
52
|
+
- **An omitted section takes every default in it.** `[guardrails]`,
|
|
53
|
+
`[backend]`, `[gate]`, and `[output]` can all be left out of the file
|
|
54
|
+
entirely.
|
|
55
|
+
|
|
56
|
+
### `[factory]`
|
|
57
|
+
|
|
58
|
+
| Key | Type | Values | Default | Required |
|
|
59
|
+
| --- | --- | --- | --- | --- |
|
|
60
|
+
| `name` | string | any non-empty string | none | yes |
|
|
61
|
+
| `base_branch` | string | any non-empty string | `"main"` | no |
|
|
62
|
+
| `source` | string | a git URL or local path | none — operates on `--repo` in place | no |
|
|
63
|
+
|
|
64
|
+
### `[source]`
|
|
65
|
+
|
|
66
|
+
| Key | Type | Values | Default | Required |
|
|
67
|
+
| --- | --- | --- | --- | --- |
|
|
68
|
+
| `kind` | string | `"linear"` \| `"github-issues"` \| `"beads"` | `"linear"`* | yes |
|
|
69
|
+
| `team` | string | tracker team key | `""` | yes for `linear`, no otherwise |
|
|
70
|
+
| `project` | string | project name within the team | none | no |
|
|
71
|
+
|
|
72
|
+
\* A missing or invalid `kind` is recorded as an error, then falls back to
|
|
73
|
+
`"linear"` rather than blocking the rest of the file — see the note above.
|
|
74
|
+
|
|
75
|
+
#### `[source.eligible]`
|
|
76
|
+
|
|
77
|
+
Filters which tracker items are eligible work. The same shape is reused,
|
|
78
|
+
verbatim, by [`[triage.eligible]`](#triage-optional).
|
|
79
|
+
|
|
80
|
+
| Key | Type | Values | Default | Required |
|
|
81
|
+
| --- | --- | --- | --- | --- |
|
|
82
|
+
| `state` | string | a tracker workflow state name | none — not filtered | no |
|
|
83
|
+
| `label` | string or array of strings | one or more label names (AND'd) | `[]` — not filtered | no |
|
|
84
|
+
| `assignee` | string | `"unassigned"` \| `"me"` \| a user email or id | none — not filtered | no |
|
|
85
|
+
|
|
86
|
+
### `[agent]`
|
|
87
|
+
|
|
88
|
+
| Key | Type | Values | Default | Required |
|
|
89
|
+
| --- | --- | --- | --- | --- |
|
|
90
|
+
| `kind` | string | `"claude-code"` today | `"claude-code"`* | yes |
|
|
91
|
+
| `prompt` | string | path to a prompt file, relative to the config | none | yes |
|
|
92
|
+
| `allowed_tools` | array of strings | tool names the agent may call | `["Read", "Edit", "Write", "Bash", "Grep", "Glob"]` | no |
|
|
93
|
+
|
|
94
|
+
\* Same fallback as `[source].kind`: missing or invalid is an error, and the
|
|
95
|
+
field still resolves to `"claude-code"`.
|
|
96
|
+
|
|
97
|
+
### `[backend]`
|
|
98
|
+
|
|
99
|
+
| Key | Type | Values | Default | Required |
|
|
100
|
+
| --- | --- | --- | --- | --- |
|
|
101
|
+
| `kind` | string | `"local-worktree"` \| `"container"` \| `"firecracker"` \| `"e2b"` \| `"remote"` | `"local-worktree"` | no |
|
|
102
|
+
| `image` | string | a container image reference | none | no — needed once `kind = "container"` |
|
|
103
|
+
| `managed_checkout` | boolean | `true` \| `false` | `true` | no |
|
|
104
|
+
|
|
105
|
+
`managed_checkout` only affects `local-worktree`: `true` makes worktrees from
|
|
106
|
+
a bare clone ghostrail owns under its own state dir; `false` borrows your
|
|
107
|
+
working checkout instead. See [choosing a backend](docs/backends.md).
|
|
108
|
+
|
|
109
|
+
### `[gate]`
|
|
110
|
+
|
|
111
|
+
| Key | Type | Values | Default | Required |
|
|
112
|
+
| --- | --- | --- | --- | --- |
|
|
113
|
+
| `commands` | array of strings | shell commands, run in order in the workspace | `[]` | no |
|
|
114
|
+
|
|
115
|
+
### `[guardrails]`
|
|
116
|
+
|
|
117
|
+
| Key | Type | Values | Default | Required |
|
|
118
|
+
| --- | --- | --- | --- | --- |
|
|
119
|
+
| `max_items_per_run` | number | > 0 | `3` | no |
|
|
120
|
+
| `budget_usd_per_item` | number | > 0 | `3` | no |
|
|
121
|
+
| `max_cost_usd_per_run` | number | > 0 | `5` | no |
|
|
122
|
+
| `timeout` | duration string | e.g. `"30m"` | `"30m"` | no |
|
|
123
|
+
| `claim_ttl` | duration string | e.g. `"2h"` | `"2h"` | no |
|
|
124
|
+
| `concurrency` | number | > 0 | `1` | no |
|
|
125
|
+
|
|
126
|
+
- `budget_usd_per_item` does two jobs: it's the hard per-item cost ceiling,
|
|
127
|
+
*and* it's the cost ghostrail assumes for an item that finishes without
|
|
128
|
+
reporting any cost at all. Raising it to give one item more headroom also
|
|
129
|
+
raises what an unreported-cost item gets charged.
|
|
130
|
+
- Neither budget key can express "unlimited" today
|
|
131
|
+
([TJ-1581](https://linear.app/timothyjordan/issue/TJ-1581/featguardrails-allow-an-unlimited-budget-for-budget-usd-per-item-and)
|
|
132
|
+
tracks adding one); update this row once it ships.
|
|
133
|
+
- This table says what each key accepts. What each one bounds inside a tick,
|
|
134
|
+
and how they interact with `concurrency`, is the subject of the tick
|
|
135
|
+
documentation (tracked in
|
|
136
|
+
[TJ-1580](https://linear.app/timothyjordan/issue/TJ-1580/docs-explain-how-a-tick-works-and-how-every-guardrail-lever-changes-it));
|
|
137
|
+
until it lands, see "How it works" in [`docs/index.md`](docs/index.md).
|
|
138
|
+
|
|
139
|
+
### `[output]`
|
|
140
|
+
|
|
141
|
+
| Key | Type | Values | Default | Required |
|
|
142
|
+
| --- | --- | --- | --- | --- |
|
|
143
|
+
| `open_pr` | boolean | `true` \| `false` | `true` | no |
|
|
144
|
+
| `draft` | boolean | `true` \| `false` | `false` | no |
|
|
145
|
+
| `auto_merge` | boolean | `true` \| `false` | `false` | no |
|
|
146
|
+
| `branch_prefix` | string | any non-empty string | `"ghostrail"` | no |
|
|
147
|
+
| `commit_type` | string | a conventional-commit type, e.g. `"feat"`, `"fix"`, `"chore"` | `"chore"` | no |
|
|
148
|
+
|
|
149
|
+
`auto_merge` is policy, not just a default: it hard-defaults to `false`
|
|
150
|
+
because a human owns every merge. No combination of keys makes ghostrail
|
|
151
|
+
merge its own PR.
|
|
152
|
+
|
|
153
|
+
### Top level
|
|
154
|
+
|
|
155
|
+
| Key | Type | Values | Default | Required |
|
|
156
|
+
| --- | --- | --- | --- | --- |
|
|
157
|
+
| `artifacts` | array of strings | names of global artifacts under `~/.config/ghostrail/artifacts/` | `[]` | no |
|
|
158
|
+
|
|
159
|
+
### `[respond]` (optional)
|
|
160
|
+
|
|
161
|
+
Enables `ghostrail respond`. Omit the whole section to leave it disabled.
|
|
162
|
+
|
|
163
|
+
| Key | Type | Values | Default | Required |
|
|
164
|
+
| --- | --- | --- | --- | --- |
|
|
165
|
+
| `prompt` | string | path to a prompt file, relative to the config | none | yes, if `[respond]` is present |
|
|
166
|
+
|
|
167
|
+
### `[triage]` (optional)
|
|
168
|
+
|
|
169
|
+
Required by `ghostrail triage`; without it, that command has nothing to run.
|
|
170
|
+
|
|
171
|
+
| Key | Type | Values | Default | Required |
|
|
172
|
+
| --- | --- | --- | --- | --- |
|
|
173
|
+
| `prompt` | string | path to a prompt file, relative to the config | none | yes, if `[triage]` is present |
|
|
174
|
+
| `eligible` | inline table | same shape as [`[source.eligible]`](#sourceeligible) | `{ labels: [] }` | no |
|
|
175
|
+
|
|
176
|
+
### A complete example
|
|
177
|
+
|
|
178
|
+
```toml
|
|
179
|
+
[factory]
|
|
180
|
+
name = "fix-bugs"
|
|
181
|
+
base_branch = "main" # default; shown for clarity
|
|
182
|
+
|
|
183
|
+
[source]
|
|
184
|
+
kind = "linear" # linear | github-issues | beads
|
|
185
|
+
team = "TJ" # required for linear: its team key
|
|
186
|
+
project = "myproject" # optional: scope to one project
|
|
187
|
+
eligible = { state = "Todo", label = "factory", assignee = "unassigned" }
|
|
188
|
+
|
|
189
|
+
[agent]
|
|
190
|
+
kind = "claude-code"
|
|
191
|
+
prompt = "prompts/fix-bug.md"
|
|
192
|
+
# allowed_tools defaults to ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]
|
|
193
|
+
|
|
194
|
+
[backend]
|
|
195
|
+
kind = "local-worktree" # default; container | firecracker | e2b | remote also declared
|
|
196
|
+
# managed_checkout = true # default: local-worktree owns its clone
|
|
197
|
+
|
|
198
|
+
[gate]
|
|
199
|
+
commands = [
|
|
200
|
+
"pnpm install --frozen-lockfile",
|
|
201
|
+
"pnpm lint",
|
|
202
|
+
"pnpm typecheck",
|
|
203
|
+
"pnpm test:unit",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
[guardrails]
|
|
207
|
+
max_items_per_run = 3 # default
|
|
208
|
+
budget_usd_per_item = 3 # default; also the assumed cost of an unreported-cost item
|
|
209
|
+
max_cost_usd_per_run = 5 # default
|
|
210
|
+
timeout = "30m" # default
|
|
211
|
+
claim_ttl = "2h" # default
|
|
212
|
+
concurrency = 1 # default: one item at a time
|
|
213
|
+
|
|
214
|
+
[output]
|
|
215
|
+
open_pr = true # default
|
|
216
|
+
draft = false # default
|
|
217
|
+
auto_merge = false # hard default: a human owns every merge
|
|
218
|
+
branch_prefix = "ghostrail" # default
|
|
219
|
+
commit_type = "chore" # default
|
|
220
|
+
|
|
221
|
+
artifacts = ["style-guide"] # optional: shared references staged into every run
|
|
222
|
+
|
|
223
|
+
[respond] # optional: enables `ghostrail respond`
|
|
224
|
+
prompt = "prompts/respond.md"
|
|
225
|
+
|
|
226
|
+
[triage] # optional: required by `ghostrail triage`
|
|
227
|
+
prompt = "prompts/triage.md"
|
|
228
|
+
eligible = { state = "Todo", label = "triage", assignee = "unassigned" }
|
|
229
|
+
```
|
|
230
|
+
|
|
29
231
|
## Credentials
|
|
30
232
|
|
|
31
233
|
Ghostrail reads its credentials from two dotenv-style files before every `run`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/commands.js
CHANGED
|
@@ -10,10 +10,10 @@ import { createBoardServer, isLoopback, normalizeToken } from "../board/server.j
|
|
|
10
10
|
import { buildProposal, writeProfile } from "../claude-profile/index.js";
|
|
11
11
|
import { parseDuration } from "../config/duration.js";
|
|
12
12
|
import { parseConfig, parseMergedConfig } from "../config/load.js";
|
|
13
|
-
import { createDashboardServer } from "../dashboard/index.js";
|
|
13
|
+
import { createDashboardServer, defaultStateDir } from "../dashboard/index.js";
|
|
14
14
|
import { buildLoopDeps, buildRespondDeps, buildTriageDeps, repoGhRunner, resolveRepoSlug, } from "../factory/build.js";
|
|
15
15
|
import { describeMigration, migrateLegacyStore, resolveWorkRepo, stateDirFor, } from "../factory/state.js";
|
|
16
|
-
import { entryKey,
|
|
16
|
+
import { entryKey, findByPath, findForRepoResolving, replaceEntry, upsert, } from "../global/registry.js";
|
|
17
17
|
import { canonicalRepoId, localRepoId } from "../global/repo-id.js";
|
|
18
18
|
import { GlobalStore, resolveGlobalDir } from "../global/store.js";
|
|
19
19
|
import { DEFAULT_FACTORY_IMAGE, dockerBuildArgs, dockerDir, parseImageFlags, } from "../image/build.js";
|
|
@@ -34,6 +34,7 @@ import { FileStore } from "../store/file-store.js";
|
|
|
34
34
|
import { triageTick } from "../triage/engine.js";
|
|
35
35
|
import { VERSION } from "../version.js";
|
|
36
36
|
import { askOnTerminal, parseInstallOffers } from "./prompt.js";
|
|
37
|
+
import { describeStalledUpdate } from "./update-diagnosis.js";
|
|
37
38
|
/**
|
|
38
39
|
* Parse the flags shared by `run` and `respond`: `--config`/`-c <path>` and
|
|
39
40
|
* `--repo`/`-C <path>`, starting from the given defaults. Unknown tokens are
|
|
@@ -436,6 +437,7 @@ function dashboardDeps(env, manager) {
|
|
|
436
437
|
readRegistry: () => store.readRegistry(),
|
|
437
438
|
writeRegistry: (entries) => store.writeRegistry(entries),
|
|
438
439
|
health: ghostrailHealth,
|
|
440
|
+
stateDir: defaultStateDir,
|
|
439
441
|
states: async (entries) => {
|
|
440
442
|
if ((await manager.kind()) !== "systemd")
|
|
441
443
|
return new Map();
|
|
@@ -1068,16 +1070,27 @@ async function registerGhostrail(repo, env, streams) {
|
|
|
1068
1070
|
return;
|
|
1069
1071
|
}
|
|
1070
1072
|
const repoId = await resolveRepoId(path);
|
|
1071
|
-
const existing =
|
|
1073
|
+
const existing = await findForRepoResolving(entries, repoId, path, resolveRepoId);
|
|
1072
1074
|
if (existing !== undefined) {
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1075
|
+
// Everything the entry already carries survives, `watch` above all:
|
|
1076
|
+
// re-registering must never switch a ghostrail back on that the operator
|
|
1077
|
+
// deliberately turned off.
|
|
1078
|
+
const adopted = { ...existing, repo: repoId, path };
|
|
1079
|
+
if (existing.repo === repoId && existing.path === path)
|
|
1077
1080
|
return;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
+
// By the entry's *old* key: adopting an identity re-keys it, so an
|
|
1082
|
+
// ordinary upsert would append beside it rather than replace it.
|
|
1083
|
+
await store.writeRegistry(replaceEntry(entries, entryKey(existing), adopted));
|
|
1084
|
+
if (existing.repo === undefined) {
|
|
1085
|
+
// Registered before repo identities existed. Adopting it here is the
|
|
1086
|
+
// migration; appending instead would give one repo two entries and two
|
|
1087
|
+
// service units, which is the failure this all exists to remove.
|
|
1088
|
+
streams.out(`adopted the existing ghostrail "${existing.name}" for ${repoId}\n`);
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
streams.out(`this repo already has the ghostrail "${existing.name}"; pointed it at this checkout\n` +
|
|
1092
|
+
` was: ${existing.path}\n now: ${path}\n`);
|
|
1093
|
+
}
|
|
1081
1094
|
return;
|
|
1082
1095
|
}
|
|
1083
1096
|
const entry = {
|
|
@@ -1309,10 +1322,29 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
|
|
|
1309
1322
|
}
|
|
1310
1323
|
streams.out("\n");
|
|
1311
1324
|
await registerGhostrail(repo, env, streams);
|
|
1312
|
-
streams.out(
|
|
1313
|
-
streams.out(" then run `/ghostrail customize` there to tailor the prompts to this repo\n");
|
|
1325
|
+
streams.out(await initNextStepsHint(env, cwd));
|
|
1314
1326
|
return 0;
|
|
1315
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* The `next:` hint at the end of a plain `ghostrail init`. Skips telling the
|
|
1330
|
+
* user to run `ghostrail skill` when it is already installed and current, so
|
|
1331
|
+
* a repeat `init` does not repeat advice already followed. Detection reuses
|
|
1332
|
+
* the same plan `ghostrail skill --check` reports on; any failure there is
|
|
1333
|
+
* best-effort only and falls back to the original two-line hint rather than
|
|
1334
|
+
* failing `init`, which already succeeded at scaffolding.
|
|
1335
|
+
*/
|
|
1336
|
+
async function initNextStepsHint(env, cwd) {
|
|
1337
|
+
const customizeOnly = "next: run `/ghostrail customize` in your coding agent to tailor the prompts to this repo\n";
|
|
1338
|
+
const installAndCustomize = "next: `ghostrail skill` installs the /ghostrail skill for your coding agent,\n" +
|
|
1339
|
+
" then run `/ghostrail customize` there to tailor the prompts to this repo\n";
|
|
1340
|
+
try {
|
|
1341
|
+
const plan = await planSkillInstall(pathCtx(env, cwd), { scope: "global" });
|
|
1342
|
+
return plan.upToDate ? customizeOnly : installAndCustomize;
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
return installAndCustomize;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1316
1348
|
/**
|
|
1317
1349
|
* Parse `ghostrail skill` options. Default scope is `global` (the user's own
|
|
1318
1350
|
* agent config), because the skill drives ghostrail wherever you are; `--project`
|
|
@@ -1511,17 +1543,20 @@ export async function cmdSelfUpdate(argv, streams, deps = {}) {
|
|
|
1511
1543
|
// after "updated X -> Y" (the CLI's status) with nothing marking which one
|
|
1512
1544
|
// it described.
|
|
1513
1545
|
streams.out("ghostrail:\n");
|
|
1514
|
-
// Resolve `latest` BEFORE installing
|
|
1515
|
-
//
|
|
1516
|
-
// first, an unchanged version can only mean the install went somewhere PATH
|
|
1517
|
-
// does not resolve. Without this check the two are indistinguishable, and we
|
|
1518
|
-
// used to blame a split install for what was simply nothing to do.
|
|
1546
|
+
// Resolve `latest` BEFORE installing, so "nothing newer existed" is ruled out
|
|
1547
|
+
// before anything else is inferred from an unchanged version.
|
|
1519
1548
|
const latest = await readLatest();
|
|
1520
1549
|
if (latest !== undefined && latest === current) {
|
|
1521
1550
|
streams.out(` already on the latest version (${current})\n`);
|
|
1522
1551
|
return finishWithSkill(argv, streams, runSkill);
|
|
1523
1552
|
}
|
|
1524
|
-
|
|
1553
|
+
// The exact version, not the tag. `npm view` reads the registry while
|
|
1554
|
+
// `npm install` may resolve `latest` from a cached packument, so for a few
|
|
1555
|
+
// minutes after a release the two disagree and npm cheerfully reinstalls the
|
|
1556
|
+
// version already present, exits 0, and leaves nothing to distinguish that
|
|
1557
|
+
// from a failed install. Naming the version closes the window: a stale cache
|
|
1558
|
+
// now produces a real npm error instead of a silent no-op.
|
|
1559
|
+
const installResult = await install(latest ?? "latest");
|
|
1525
1560
|
if (installResult.exitCode !== 0) {
|
|
1526
1561
|
streams.err(installResult.stderr || installResult.stdout);
|
|
1527
1562
|
streams.err(` update failed (npm exited ${installResult.exitCode})\n`);
|
|
@@ -1537,16 +1572,18 @@ export async function cmdSelfUpdate(argv, streams, deps = {}) {
|
|
|
1537
1572
|
return 1;
|
|
1538
1573
|
}
|
|
1539
1574
|
if (installed === current) {
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1575
|
+
// Which explanation fits is decided by the machine, not assumed. Ruling out
|
|
1576
|
+
// "nothing newer existed" does not leave a split install as the only
|
|
1577
|
+
// possibility, and asserting it while printing two agreeing paths is what
|
|
1578
|
+
// sent an operator looking for a second node install that did not exist.
|
|
1579
|
+
const locations = await (deps.locations ?? probeInstallLocations)();
|
|
1580
|
+
for (const line of describeStalledUpdate({
|
|
1581
|
+
current,
|
|
1582
|
+
...(latest === undefined ? {} : { wanted: latest }),
|
|
1583
|
+
locations,
|
|
1584
|
+
})) {
|
|
1585
|
+
streams.err(` ${line}\n`);
|
|
1548
1586
|
}
|
|
1549
|
-
await reportInstallLocations(streams, deps);
|
|
1550
1587
|
return 1;
|
|
1551
1588
|
}
|
|
1552
1589
|
streams.out(` updated ${current} -> ${installed}\n`);
|
|
@@ -1594,19 +1631,6 @@ async function installedVersion() {
|
|
|
1594
1631
|
const version = result.stdout.trim();
|
|
1595
1632
|
return version.length === 0 ? undefined : version;
|
|
1596
1633
|
}
|
|
1597
|
-
/**
|
|
1598
|
-
* Print the two facts that identify a split install: where npm puts global
|
|
1599
|
-
* packages, and which `ghostrail` the shell actually runs. When they disagree,
|
|
1600
|
-
* that is the whole answer.
|
|
1601
|
-
*/
|
|
1602
|
-
async function reportInstallLocations(streams, deps = {}) {
|
|
1603
|
-
const { npmRoot, which } = await (deps.locations ?? probeInstallLocations)();
|
|
1604
|
-
if (npmRoot !== undefined)
|
|
1605
|
-
streams.err(` npm installs into: ${npmRoot}\n`);
|
|
1606
|
-
if (which !== undefined)
|
|
1607
|
-
streams.err(` your shell runs: ${which}\n`);
|
|
1608
|
-
streams.err(" If those disagree, you have more than one node version and the update landed under a different one.\n");
|
|
1609
|
-
}
|
|
1610
1634
|
/** Ask npm where it installs, and the shell which ghostrail it runs. */
|
|
1611
1635
|
async function probeInstallLocations() {
|
|
1612
1636
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|