@aragon-agent/cli 0.5.8 → 0.5.10
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/CHANGELOG.md +1157 -747
- package/README.md +1552 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +368 -3
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +426 -2
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +79 -2
- package/dist/config/cli-commands.js +185 -3
- package/dist/config/env.d.ts +15 -3
- package/dist/config/env.js +143 -3
- package/dist/config/load.d.ts +66 -0
- package/dist/config/load.js +107 -1
- package/dist/config/schema.d.ts +268 -2
- package/dist/config/schema.js +288 -1
- package/dist/config/store.js +32 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +11 -4
- package/dist/ui/App.js +359 -37
- package/dist/ui/BottomStatusRow.d.ts +65 -0
- package/dist/ui/BottomStatusRow.js +17 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install (cli-auto-update section 3.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
5
|
+
*
|
|
6
|
+
* INVARIANT U-2 - INVOKE NPM'S JS ENTRY THROUGH `process.execPath`, NEVER THE
|
|
7
|
+
* `npm` SHIM. On Windows `npm` is `npm.cmd`, and since the CVE-2024-27980 fix
|
|
8
|
+
* Node refuses to spawn a `.cmd` without `shell: true` - which C-7 forbids
|
|
9
|
+
* outright. Running node's own binary also guarantees the update is installed by
|
|
10
|
+
* the same runtime that will execute it.
|
|
11
|
+
*
|
|
12
|
+
* INVARIANT U-5 - THE CHILD'S OUTPUT GOES TO A FILE, AND ITS LIFETIME IS NEVER
|
|
13
|
+
* BOUNDED BY US. `'ignore'` leaves nothing to put in `update_install_failed`'s
|
|
14
|
+
* `stderrTail`, which is the one diagnostic a feature whose contract is silence
|
|
15
|
+
* actually needs; a PIPE would break D-10, because a detached child whose parent
|
|
16
|
+
* has exited takes `EPIPE` on its next write. A real file descriptor is
|
|
17
|
+
* inherited across the detach and keeps working after the parent is gone.
|
|
18
|
+
*
|
|
19
|
+
* `spawn`, NOT `execFile`, AND THAT IS A CORRECTION TO THE DESIGN (IF-3).
|
|
20
|
+
* Section 3.5 spelled this call as `execFile(..., { detached: true, stdio:
|
|
21
|
+
* [...], timeout: 0 })`, but `execFile` forwards NEITHER `detached` NOR `stdio`
|
|
22
|
+
* to `spawn` - it always builds its own pipes so it can buffer the output it
|
|
23
|
+
* hands to its callback. Measured on this package's own Node: with
|
|
24
|
+
* `stdio: ['ignore', fd, fd]` the log file stays EMPTY and the bytes arrive in
|
|
25
|
+
* the callback's `stdout` instead. Written as designed, U-5 loses its
|
|
26
|
+
* `stderrTail` AND D-10 loses the detach, silently and on every platform.
|
|
27
|
+
* `spawn` with an argv array and an explicit `shell: false` satisfies C-7's
|
|
28
|
+
* actual requirement ("argv array, never a shell, never a concatenated command
|
|
29
|
+
* line"), and it has no `timeout` option at all - so P1-5's "the limit must
|
|
30
|
+
* never become the child's bound" holds STRUCTURALLY here rather than by
|
|
31
|
+
* remembering to leave a field at `0`.
|
|
32
|
+
*/
|
|
33
|
+
import { spawn } from 'node:child_process';
|
|
34
|
+
import { closeSync, existsSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
35
|
+
import { dirname, resolve } from 'node:path';
|
|
36
|
+
import process from 'node:process';
|
|
37
|
+
import { getUpdateInstallLogPath } from '../config/app-paths.js';
|
|
38
|
+
import { UPDATE_LIMITS } from './limits.js';
|
|
39
|
+
import { readSelfManifest } from './install-source.js';
|
|
40
|
+
import { STRICT_VERSION_RE } from './semver.js';
|
|
41
|
+
/**
|
|
42
|
+
* Where npm's JS entry point lives, or `null`.
|
|
43
|
+
*
|
|
44
|
+
* Probes the Windows / nvm4w layout first and the POSIX one second. There is
|
|
45
|
+
* deliberately NO bare-`npm`-on-PATH fallback: on Windows that resolves to
|
|
46
|
+
* `npm.cmd`, which is exactly what U-2 forbids, and a fallback that is correct
|
|
47
|
+
* on one platform and forbidden on another is the kind of branch that gets
|
|
48
|
+
* copied without its condition. A machine with no reachable `npm-cli.js`
|
|
49
|
+
* degrades to notify-only, which is the honest outcome.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveNpmCli(execPath = process.execPath) {
|
|
52
|
+
const nodeDir = dirname(execPath);
|
|
53
|
+
const candidates = [
|
|
54
|
+
resolve(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
55
|
+
resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
56
|
+
];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(candidate) && statSync(candidate).isFile())
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Unreadable candidate; try the next.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `env` minus `NODE_OPTIONS`, plus the three switches that keep npm's
|
|
70
|
+
* postinstall banners from spawning anything interactive.
|
|
71
|
+
*
|
|
72
|
+
* `NODE_OPTIONS` is dropped because a `--require` hook inherited into npm is an
|
|
73
|
+
* unnecessary surface on a process we start unattended and never watch.
|
|
74
|
+
*/
|
|
75
|
+
export function cleanInstallEnv(env) {
|
|
76
|
+
const next = { ...env };
|
|
77
|
+
delete next.NODE_OPTIONS;
|
|
78
|
+
next.npm_config_yes = 'true';
|
|
79
|
+
next.ADBLOCK = '1';
|
|
80
|
+
next.DISABLE_OPENCOLLECTIVE = '1';
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
/** The argv this feature is allowed to build. Exported for AC-24's assertion. */
|
|
84
|
+
export function buildInstallArgs(npmCliJs, packageName, version) {
|
|
85
|
+
return [
|
|
86
|
+
npmCliJs,
|
|
87
|
+
'install',
|
|
88
|
+
'--global',
|
|
89
|
+
`${packageName}@${version}`,
|
|
90
|
+
'--no-fund',
|
|
91
|
+
'--no-audit',
|
|
92
|
+
'--loglevel=error',
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
function tailOf(path, maxChars) {
|
|
96
|
+
try {
|
|
97
|
+
const text = readFileSync(path, 'utf-8');
|
|
98
|
+
return text.length <= maxChars ? text : text.slice(-maxChars);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Install `packageName@version` globally, detached, and verify it landed HERE.
|
|
106
|
+
*
|
|
107
|
+
* Resolves when the child exits (or fails to start). The child is `unref()`ed
|
|
108
|
+
* either way, so if the user quits mid-install the install COMPLETES rather than
|
|
109
|
+
* leaving a global package half-written; the next launch's check then finds
|
|
110
|
+
* local === remote and does nothing. Convergence, not bookkeeping (D-10).
|
|
111
|
+
*
|
|
112
|
+
* NEVER REJECTS. Every failure is an `InstallResult` with `ok: false`, because
|
|
113
|
+
* the only caller is a background timer whose contract is silence.
|
|
114
|
+
*/
|
|
115
|
+
export function runNpmInstall(input) {
|
|
116
|
+
const { packageName, version, root, onSpawn } = input;
|
|
117
|
+
const deps = input.deps ?? {};
|
|
118
|
+
const now = deps.now ?? Date.now;
|
|
119
|
+
const doSpawn = (deps.spawnImpl ?? spawn);
|
|
120
|
+
const logPath = deps.logPath ?? getUpdateInstallLogPath();
|
|
121
|
+
const readVersionAt = deps.readVersionAt ?? ((where) => readSelfManifest(where)?.version ?? null);
|
|
122
|
+
return new Promise((settle) => {
|
|
123
|
+
// The version came back from the registry and has already been through
|
|
124
|
+
// `parseSemver`; this is the gate that makes "not an injection surface" a
|
|
125
|
+
// checkable claim rather than a reasoned one (AC-24). It also satisfies
|
|
126
|
+
// `fetch-source.ts` rule 3 (no leading `-`) by construction.
|
|
127
|
+
if (!STRICT_VERSION_RE.test(version)) {
|
|
128
|
+
settle({ ok: false, failure: 'spawn' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const npmCliJs = deps.npmCliPath !== undefined ? deps.npmCliPath : resolveNpmCli();
|
|
132
|
+
if (!npmCliJs) {
|
|
133
|
+
settle({ ok: false, failure: 'spawn' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// A stale log from a parent that died before its child is unlinked here, so
|
|
137
|
+
// the tail read later can only be THIS install's (U-5).
|
|
138
|
+
try {
|
|
139
|
+
rmSync(logPath, { force: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Best-effort; an unremovable stale log costs one misleading tail.
|
|
143
|
+
}
|
|
144
|
+
let logFd = null;
|
|
145
|
+
try {
|
|
146
|
+
logFd = openSync(logPath, 'w');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// No log file means no `stderrTail`, which is a diagnostic loss and not a
|
|
150
|
+
// reason to skip the install.
|
|
151
|
+
logFd = null;
|
|
152
|
+
}
|
|
153
|
+
const startedAt = now();
|
|
154
|
+
let settled = false;
|
|
155
|
+
const finish = (result) => {
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
settled = true;
|
|
159
|
+
if (logFd !== null) {
|
|
160
|
+
try {
|
|
161
|
+
closeSync(logFd);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Already closed; the child holds its own duplicated descriptor.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
settle({ ...result, elapsedMs: now() - startedAt });
|
|
168
|
+
};
|
|
169
|
+
const options = {
|
|
170
|
+
detached: true,
|
|
171
|
+
stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'],
|
|
172
|
+
env: cleanInstallEnv(deps.env ?? process.env),
|
|
173
|
+
// C-7, spelled out rather than inherited from the default: this argv is
|
|
174
|
+
// built from a manifest on disk and a registry response, and a `true` here
|
|
175
|
+
// would turn both into shell input.
|
|
176
|
+
shell: false,
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
};
|
|
179
|
+
let child;
|
|
180
|
+
try {
|
|
181
|
+
child = doSpawn(process.execPath, buildInstallArgs(npmCliJs, packageName, version), options);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
finish({ ok: false, failure: 'spawn' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
child.on('error', () => finish({ ok: false, failure: 'spawn' }));
|
|
188
|
+
child.on('exit', (code) => {
|
|
189
|
+
const tail = tailOf(logPath, UPDATE_LIMITS.stderrTailChars);
|
|
190
|
+
try {
|
|
191
|
+
rmSync(logPath, { force: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// The next install unlinks it before opening.
|
|
195
|
+
}
|
|
196
|
+
if (code !== 0) {
|
|
197
|
+
finish({
|
|
198
|
+
ok: false,
|
|
199
|
+
failure: 'exit',
|
|
200
|
+
...(tail ? { stderrTail: tail } : {}),
|
|
201
|
+
...(typeof code === 'number' ? { exitCode: code } : {}),
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------
|
|
206
|
+
// U-6 - `npm` EXITING 0 DOES NOT MEAN THIS INSTALLATION MOVED.
|
|
207
|
+
//
|
|
208
|
+
// Section 3.2 classifies the running copy by PATH; `npm install --global`
|
|
209
|
+
// writes to the prefix NPM'S OWN CONFIG resolves. Those are the same
|
|
210
|
+
// directory on an ordinary machine and different directories on one with
|
|
211
|
+
// `prefix=` in `.npmrc`, with two Node installations, or with a global
|
|
212
|
+
// root inherited from a version manager the user has since switched away
|
|
213
|
+
// from. Left unchecked the state machine reaches `ready` on a lie,
|
|
214
|
+
// `pendingRestartVersion` never matches, and the next check re-decides
|
|
215
|
+
// `install` - an UNBOUNDED SILENT REINSTALL, once per interval, forever
|
|
216
|
+
// (R-12). The check costs one `readFileSync` of a file we already know how
|
|
217
|
+
// to parse, and it is the only thing standing between this design and the
|
|
218
|
+
// worst failure shape available to a feature that promised to be invisible.
|
|
219
|
+
// ---------------------------------------------------------------------
|
|
220
|
+
const observed = readVersionAt(root);
|
|
221
|
+
if (observed !== version) {
|
|
222
|
+
finish({
|
|
223
|
+
ok: false,
|
|
224
|
+
failure: 'ineffective',
|
|
225
|
+
...(observed ? { observedVersion: observed } : {}),
|
|
226
|
+
...(tail ? { stderrTail: tail } : {}),
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
finish({ ok: true, observedVersion: observed, exitCode: 0 });
|
|
231
|
+
});
|
|
232
|
+
// Phase two of U-4, and it must happen HERE: the lock had to be taken before
|
|
233
|
+
// the spawn, but `child.pid` does not exist until after it.
|
|
234
|
+
if (typeof child.pid === 'number' && onSpawn)
|
|
235
|
+
onSpawn(child.pid);
|
|
236
|
+
// Survives our own exit (D-10). `dispose()` deliberately never kills it
|
|
237
|
+
// (U-3): killing a running `npm install -g` is the one action in this
|
|
238
|
+
// feature that can leave the user's global installation broken, and it would
|
|
239
|
+
// be triggered by the most ordinary event there is - Ctrl+C.
|
|
240
|
+
child.unref?.();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UPDATE_LIMITS - the single authority on every structural bound the updater
|
|
3
|
+
* enforces (cli-auto-update section 5.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/update/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte. The
|
|
7
|
+
* regex there is a hardcoded directory list, which is why adding this tree and
|
|
8
|
+
* adding `update` to that list are the SAME change (C-2 / AC-18) - a scanner
|
|
9
|
+
* that silently stops scanning is worse than no scanner.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
12
|
+
* `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
|
|
13
|
+
*
|
|
14
|
+
* - The entries below are STRUCTURAL. They describe what a background check, a
|
|
15
|
+
* detached install and a one-row readout can physically carry. A user has no
|
|
16
|
+
* business tuning them.
|
|
17
|
+
* - The four `update.*` keys in `config/schema.ts` are POLICY: whether to
|
|
18
|
+
* install at all, how often to ask, which registry, which dist-tag. Those are
|
|
19
|
+
* clamped, persisted and user-facing.
|
|
20
|
+
*
|
|
21
|
+
* NOTHING HERE IS IMPORTED AT RUNTIME BY `cli.tsx` (AC-1). This module and
|
|
22
|
+
* `types.ts` are pure, dependency-free and side-effect-free; every other module
|
|
23
|
+
* in this tree is reached only through the dynamic `import()` in section 3.8.
|
|
24
|
+
*/
|
|
25
|
+
export declare const UPDATE_LIMITS: {
|
|
26
|
+
/** Never contend with first paint. */
|
|
27
|
+
readonly startupDelayMs: 3000;
|
|
28
|
+
/**
|
|
29
|
+
* A background check has nobody waiting on it.
|
|
30
|
+
*
|
|
31
|
+
* Below `fetch-source.ts`'s `HTTP_TIMEOUT_MS` (15 s) ON PURPOSE: that one
|
|
32
|
+
* bounds a download a human typed a command for, this one bounds a request the
|
|
33
|
+
* user never asked for and must never notice.
|
|
34
|
+
*/
|
|
35
|
+
readonly checkTimeoutMs: 8000;
|
|
36
|
+
/**
|
|
37
|
+
* Wall-clock budget for the `installing` PHASE and the lock's staleness
|
|
38
|
+
* refresh - NEVER FOR THE CHILD (U-5 / P1-5).
|
|
39
|
+
*
|
|
40
|
+
* It decides when the UI stops saying "updating" and when a lock is treated as
|
|
41
|
+
* abandoned. It is deliberately NOT `execFile`'s `timeout`, which stays `0`:
|
|
42
|
+
* wiring it there makes Node `SIGTERM` npm mid-write - the broken global
|
|
43
|
+
* installation U-3 and D-10 exist to prevent - and it would fire on exactly
|
|
44
|
+
* the slow links where a five-minute install is the correct outcome.
|
|
45
|
+
*/
|
|
46
|
+
readonly installTimeoutMs: 300000;
|
|
47
|
+
/** The `/latest` document is ~2 KB; this is the C-8 ceiling, not a target. */
|
|
48
|
+
readonly manifestMaxBytes: number;
|
|
49
|
+
/** Matches `fetch-source.ts`'s `MAX_REDIRECTS`. */
|
|
50
|
+
readonly maxRedirects: 3;
|
|
51
|
+
/**
|
|
52
|
+
* Longer than `skills`' 60 s, and the difference is the point: the holder here
|
|
53
|
+
* is an npm install on a cold cache, not a file rename.
|
|
54
|
+
*/
|
|
55
|
+
readonly lockTtlMs: 600000;
|
|
56
|
+
/** First retry after a failed check or install. */
|
|
57
|
+
readonly backoffBaseMs: 1800000;
|
|
58
|
+
/** Ceiling of the exponential. */
|
|
59
|
+
readonly backoffMaxMs: 86400000;
|
|
60
|
+
/**
|
|
61
|
+
* Consecutive failures before the row says anything at all (D-7).
|
|
62
|
+
*
|
|
63
|
+
* Below this the failure is silent, because silence is the requirement and an
|
|
64
|
+
* unactionable recurring warning is the standard way updaters become hated.
|
|
65
|
+
*/
|
|
66
|
+
readonly failuresBeforeNotice: 3;
|
|
67
|
+
/** Thundering-herd spread on the check interval (D-12). */
|
|
68
|
+
readonly jitterRatio: 0.15;
|
|
69
|
+
/**
|
|
70
|
+
* Bounded, redacted npm error tail, read from the install LOG FILE (U-5).
|
|
71
|
+
*
|
|
72
|
+
* There is a tail to read at all only because the child writes to a real file
|
|
73
|
+
* descriptor rather than to `'ignore'`; a pipe would have died with the parent
|
|
74
|
+
* and broken D-10.
|
|
75
|
+
*/
|
|
76
|
+
readonly stderrTailChars: 500;
|
|
77
|
+
/**
|
|
78
|
+
* Below this many columns the update line renders its short form (section 6.2).
|
|
79
|
+
*
|
|
80
|
+
* DELIBERATELY NOT `FAST_LIMITS.statusCompactCols`' 100 (`fast/limits.ts`).
|
|
81
|
+
* That number governs a multi-part status chip competing with the context
|
|
82
|
+
* gauge for the same row; this one governs a single short clause that owns its
|
|
83
|
+
* row outright. Two numbers about two different things - do not unify them.
|
|
84
|
+
*/
|
|
85
|
+
readonly statusCompactCols: 60;
|
|
86
|
+
/**
|
|
87
|
+
* Non-zero exits of a version WE installed before it is rolled back
|
|
88
|
+
* (cli-auto-update-hardening section 7.2 / D-30).
|
|
89
|
+
*
|
|
90
|
+
* Tested against the count of PREVIOUS launches, so `2` means "the third
|
|
91
|
+
* launch rolls back". One crash can be transient - a machine that ran out of
|
|
92
|
+
* memory once, or whose session file was corrupt, gets a second chance at the
|
|
93
|
+
* release it just installed. Two is a pattern.
|
|
94
|
+
*
|
|
95
|
+
* NOTHING ELSE IN THIS TABLE BOUNDS THE ROLLBACK. There is deliberately no
|
|
96
|
+
* `bootGuardMaxAgeMs`: the guard is armed only while `autoInstalledVersion`
|
|
97
|
+
* equals the RUNNING version, so "a machine offline for a year" is a machine
|
|
98
|
+
* still running the version we installed, and rolling it back to
|
|
99
|
+
* `lastGoodVersion` is exactly as correct on day 400 as on day 1 (P1-3).
|
|
100
|
+
*/
|
|
101
|
+
readonly crashesBeforeRollback: 2;
|
|
102
|
+
/**
|
|
103
|
+
* Bound on the `npm view` fallback probe (H3).
|
|
104
|
+
*
|
|
105
|
+
* SAFE HERE AND FORBIDDEN FOR THE INSTALLER (C-20). A killed `npm view` has
|
|
106
|
+
* written nothing; a killed `npm install -g` leaves a broken global
|
|
107
|
+
* installation, which is why `installTimeoutMs` above bounds the PHASE and
|
|
108
|
+
* never the child.
|
|
109
|
+
*/
|
|
110
|
+
readonly npmViewTimeoutMs: 10000;
|
|
111
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UPDATE_LIMITS - the single authority on every structural bound the updater
|
|
3
|
+
* enforces (cli-auto-update section 5.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/update/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte. The
|
|
7
|
+
* regex there is a hardcoded directory list, which is why adding this tree and
|
|
8
|
+
* adding `update` to that list are the SAME change (C-2 / AC-18) - a scanner
|
|
9
|
+
* that silently stops scanning is worse than no scanner.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
12
|
+
* `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
|
|
13
|
+
*
|
|
14
|
+
* - The entries below are STRUCTURAL. They describe what a background check, a
|
|
15
|
+
* detached install and a one-row readout can physically carry. A user has no
|
|
16
|
+
* business tuning them.
|
|
17
|
+
* - The four `update.*` keys in `config/schema.ts` are POLICY: whether to
|
|
18
|
+
* install at all, how often to ask, which registry, which dist-tag. Those are
|
|
19
|
+
* clamped, persisted and user-facing.
|
|
20
|
+
*
|
|
21
|
+
* NOTHING HERE IS IMPORTED AT RUNTIME BY `cli.tsx` (AC-1). This module and
|
|
22
|
+
* `types.ts` are pure, dependency-free and side-effect-free; every other module
|
|
23
|
+
* in this tree is reached only through the dynamic `import()` in section 3.8.
|
|
24
|
+
*/
|
|
25
|
+
export const UPDATE_LIMITS = {
|
|
26
|
+
/** Never contend with first paint. */
|
|
27
|
+
startupDelayMs: 3_000,
|
|
28
|
+
/**
|
|
29
|
+
* A background check has nobody waiting on it.
|
|
30
|
+
*
|
|
31
|
+
* Below `fetch-source.ts`'s `HTTP_TIMEOUT_MS` (15 s) ON PURPOSE: that one
|
|
32
|
+
* bounds a download a human typed a command for, this one bounds a request the
|
|
33
|
+
* user never asked for and must never notice.
|
|
34
|
+
*/
|
|
35
|
+
checkTimeoutMs: 8_000,
|
|
36
|
+
/**
|
|
37
|
+
* Wall-clock budget for the `installing` PHASE and the lock's staleness
|
|
38
|
+
* refresh - NEVER FOR THE CHILD (U-5 / P1-5).
|
|
39
|
+
*
|
|
40
|
+
* It decides when the UI stops saying "updating" and when a lock is treated as
|
|
41
|
+
* abandoned. It is deliberately NOT `execFile`'s `timeout`, which stays `0`:
|
|
42
|
+
* wiring it there makes Node `SIGTERM` npm mid-write - the broken global
|
|
43
|
+
* installation U-3 and D-10 exist to prevent - and it would fire on exactly
|
|
44
|
+
* the slow links where a five-minute install is the correct outcome.
|
|
45
|
+
*/
|
|
46
|
+
installTimeoutMs: 300_000,
|
|
47
|
+
/** The `/latest` document is ~2 KB; this is the C-8 ceiling, not a target. */
|
|
48
|
+
manifestMaxBytes: 256 * 1024,
|
|
49
|
+
/** Matches `fetch-source.ts`'s `MAX_REDIRECTS`. */
|
|
50
|
+
maxRedirects: 3,
|
|
51
|
+
/**
|
|
52
|
+
* Longer than `skills`' 60 s, and the difference is the point: the holder here
|
|
53
|
+
* is an npm install on a cold cache, not a file rename.
|
|
54
|
+
*/
|
|
55
|
+
lockTtlMs: 600_000,
|
|
56
|
+
/** First retry after a failed check or install. */
|
|
57
|
+
backoffBaseMs: 1_800_000,
|
|
58
|
+
/** Ceiling of the exponential. */
|
|
59
|
+
backoffMaxMs: 86_400_000,
|
|
60
|
+
/**
|
|
61
|
+
* Consecutive failures before the row says anything at all (D-7).
|
|
62
|
+
*
|
|
63
|
+
* Below this the failure is silent, because silence is the requirement and an
|
|
64
|
+
* unactionable recurring warning is the standard way updaters become hated.
|
|
65
|
+
*/
|
|
66
|
+
failuresBeforeNotice: 3,
|
|
67
|
+
/** Thundering-herd spread on the check interval (D-12). */
|
|
68
|
+
jitterRatio: 0.15,
|
|
69
|
+
/**
|
|
70
|
+
* Bounded, redacted npm error tail, read from the install LOG FILE (U-5).
|
|
71
|
+
*
|
|
72
|
+
* There is a tail to read at all only because the child writes to a real file
|
|
73
|
+
* descriptor rather than to `'ignore'`; a pipe would have died with the parent
|
|
74
|
+
* and broken D-10.
|
|
75
|
+
*/
|
|
76
|
+
stderrTailChars: 500,
|
|
77
|
+
/**
|
|
78
|
+
* Below this many columns the update line renders its short form (section 6.2).
|
|
79
|
+
*
|
|
80
|
+
* DELIBERATELY NOT `FAST_LIMITS.statusCompactCols`' 100 (`fast/limits.ts`).
|
|
81
|
+
* That number governs a multi-part status chip competing with the context
|
|
82
|
+
* gauge for the same row; this one governs a single short clause that owns its
|
|
83
|
+
* row outright. Two numbers about two different things - do not unify them.
|
|
84
|
+
*/
|
|
85
|
+
statusCompactCols: 60,
|
|
86
|
+
/**
|
|
87
|
+
* Non-zero exits of a version WE installed before it is rolled back
|
|
88
|
+
* (cli-auto-update-hardening section 7.2 / D-30).
|
|
89
|
+
*
|
|
90
|
+
* Tested against the count of PREVIOUS launches, so `2` means "the third
|
|
91
|
+
* launch rolls back". One crash can be transient - a machine that ran out of
|
|
92
|
+
* memory once, or whose session file was corrupt, gets a second chance at the
|
|
93
|
+
* release it just installed. Two is a pattern.
|
|
94
|
+
*
|
|
95
|
+
* NOTHING ELSE IN THIS TABLE BOUNDS THE ROLLBACK. There is deliberately no
|
|
96
|
+
* `bootGuardMaxAgeMs`: the guard is armed only while `autoInstalledVersion`
|
|
97
|
+
* equals the RUNNING version, so "a machine offline for a year" is a machine
|
|
98
|
+
* still running the version we installed, and rolling it back to
|
|
99
|
+
* `lastGoodVersion` is exactly as correct on day 400 as on day 1 (P1-3).
|
|
100
|
+
*/
|
|
101
|
+
crashesBeforeRollback: 2,
|
|
102
|
+
/**
|
|
103
|
+
* Bound on the `npm view` fallback probe (H3).
|
|
104
|
+
*
|
|
105
|
+
* SAFE HERE AND FORBIDDEN FOR THE INSTALLER (C-20). A killed `npm view` has
|
|
106
|
+
* written nothing; a killed `npm install -g` leaves a broken global
|
|
107
|
+
* installation, which is why `installTimeoutMs` above bounds the PHASE and
|
|
108
|
+
* never the child.
|
|
109
|
+
*/
|
|
110
|
+
npmViewTimeoutMs: 10_000,
|
|
111
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy-aware fallback probe (cli-auto-update-hardening H3 / section 5.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
5
|
+
*
|
|
6
|
+
* NODE'S `fetch` (undici) IGNORES `HTTP_PROXY`. v1 recorded that as R-5 and
|
|
7
|
+
* accepted it: "the check fails, silently, forever". When the direct request
|
|
8
|
+
* returns `null`, the check now retries ONCE through the npm this machine
|
|
9
|
+
* already has, which honours `proxy`, `https-proxy`, `.npmrc` and any auth the
|
|
10
|
+
* user configured.
|
|
11
|
+
*
|
|
12
|
+
* WE DELEGATE RATHER THAN IMPLEMENT A PROXY (D-34). We do not read `.npmrc`, we
|
|
13
|
+
* do not parse `HTTP_PROXY`, and we attach credentials to nothing - D-13's "NO
|
|
14
|
+
* CREDENTIALS, EVER" is untouched because we never see them. We run the command
|
|
15
|
+
* the user could have typed and read one version string out of its JSON.
|
|
16
|
+
*
|
|
17
|
+
* `execFile` HERE, `spawn` IN `installer.ts`, AND THAT IS NOT AN INCONSISTENCY
|
|
18
|
+
* (D-36 / C-20). v1's IF-3 rejected `execFile` for the installer because it
|
|
19
|
+
* forwards neither `stdio` nor `detached`. This probe wants neither: it wants
|
|
20
|
+
* the buffered stdout `execFile` exists to provide, and it must NOT be detached.
|
|
21
|
+
* `timeout` is likewise safe here and forbidden there - a killed `npm view` has
|
|
22
|
+
* written nothing, a killed `npm install -g` has left a broken global install.
|
|
23
|
+
*/
|
|
24
|
+
import { type ExecFileOptions } from 'node:child_process';
|
|
25
|
+
import type { LatestManifest } from './types.js';
|
|
26
|
+
/** The `execFile` shape this module needs. Injectable so no test spawns npm. */
|
|
27
|
+
export type ExecFileImpl = (file: string, args: string[], options: ExecFileOptions, callback: (error: Error | null, stdout: string, stderr: string) => void) => unknown;
|
|
28
|
+
export interface NpmViewDeps {
|
|
29
|
+
execFileImpl?: ExecFileImpl;
|
|
30
|
+
/** `null` forces the "no npm entry point" branch; `undefined` probes. */
|
|
31
|
+
npmCliPath?: string | null;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The argv this probe is allowed to build. Exported for AC-47's assertion, on
|
|
37
|
+
* the `buildInstallArgs` precedent.
|
|
38
|
+
*
|
|
39
|
+
* `--registry` IS NOT OPTIONAL, and leaving it out was a real divergence (P1-4 /
|
|
40
|
+
* D-41). The HTTP path resolves `update.registry` through `resolveRegistryUrl`;
|
|
41
|
+
* `npm view` without the flag obeys whatever `.npmrc` says. The two would then
|
|
42
|
+
* answer about DIFFERENT REGISTRIES - and the population where they differ is a
|
|
43
|
+
* mirror-configured corporate machine, which is the same population this
|
|
44
|
+
* fallback exists for. The `registry` passed in is the already-resolved string
|
|
45
|
+
* `runCheck` computed, so the two probes describe one registry by construction.
|
|
46
|
+
*
|
|
47
|
+
* Auth is unaffected: `--registry` selects a URL, and npm still applies whatever
|
|
48
|
+
* `.npmrc` credentials are scoped to it. We neither read nor forward any (D-13).
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildNpmViewArgs(npmCliJs: string, packageName: string, distTag: string, registry: string): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Ask the machine's own npm what the dist-tag points at, or `null`.
|
|
53
|
+
*
|
|
54
|
+
* NEVER THROWS - the same contract as `fetchLatestManifest`, for the same
|
|
55
|
+
* reason: the only caller is a background timer whose entire contract is that
|
|
56
|
+
* the user does not find out. Any non-zero exit, any timeout, any unparseable
|
|
57
|
+
* stdout and a `resolveNpmCli()` of `null` all resolve to `null`.
|
|
58
|
+
*
|
|
59
|
+
* NO BARE-`npm`-ON-PATH FALLBACK, for the reason `installer.ts` already gives:
|
|
60
|
+
* on Windows that resolves to `npm.cmd`, which U-2 forbids.
|
|
61
|
+
*/
|
|
62
|
+
export declare function fetchLatestViaNpm(registry: string, packageName: string, distTag: string, deps?: NpmViewDeps): Promise<LatestManifest | null>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy-aware fallback probe (cli-auto-update-hardening H3 / section 5.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
5
|
+
*
|
|
6
|
+
* NODE'S `fetch` (undici) IGNORES `HTTP_PROXY`. v1 recorded that as R-5 and
|
|
7
|
+
* accepted it: "the check fails, silently, forever". When the direct request
|
|
8
|
+
* returns `null`, the check now retries ONCE through the npm this machine
|
|
9
|
+
* already has, which honours `proxy`, `https-proxy`, `.npmrc` and any auth the
|
|
10
|
+
* user configured.
|
|
11
|
+
*
|
|
12
|
+
* WE DELEGATE RATHER THAN IMPLEMENT A PROXY (D-34). We do not read `.npmrc`, we
|
|
13
|
+
* do not parse `HTTP_PROXY`, and we attach credentials to nothing - D-13's "NO
|
|
14
|
+
* CREDENTIALS, EVER" is untouched because we never see them. We run the command
|
|
15
|
+
* the user could have typed and read one version string out of its JSON.
|
|
16
|
+
*
|
|
17
|
+
* `execFile` HERE, `spawn` IN `installer.ts`, AND THAT IS NOT AN INCONSISTENCY
|
|
18
|
+
* (D-36 / C-20). v1's IF-3 rejected `execFile` for the installer because it
|
|
19
|
+
* forwards neither `stdio` nor `detached`. This probe wants neither: it wants
|
|
20
|
+
* the buffered stdout `execFile` exists to provide, and it must NOT be detached.
|
|
21
|
+
* `timeout` is likewise safe here and forbidden there - a killed `npm view` has
|
|
22
|
+
* written nothing, a killed `npm install -g` has left a broken global install.
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from 'node:child_process';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { cleanInstallEnv, resolveNpmCli } from './installer.js';
|
|
27
|
+
import { UPDATE_LIMITS } from './limits.js';
|
|
28
|
+
/**
|
|
29
|
+
* The argv this probe is allowed to build. Exported for AC-47's assertion, on
|
|
30
|
+
* the `buildInstallArgs` precedent.
|
|
31
|
+
*
|
|
32
|
+
* `--registry` IS NOT OPTIONAL, and leaving it out was a real divergence (P1-4 /
|
|
33
|
+
* D-41). The HTTP path resolves `update.registry` through `resolveRegistryUrl`;
|
|
34
|
+
* `npm view` without the flag obeys whatever `.npmrc` says. The two would then
|
|
35
|
+
* answer about DIFFERENT REGISTRIES - and the population where they differ is a
|
|
36
|
+
* mirror-configured corporate machine, which is the same population this
|
|
37
|
+
* fallback exists for. The `registry` passed in is the already-resolved string
|
|
38
|
+
* `runCheck` computed, so the two probes describe one registry by construction.
|
|
39
|
+
*
|
|
40
|
+
* Auth is unaffected: `--registry` selects a URL, and npm still applies whatever
|
|
41
|
+
* `.npmrc` credentials are scoped to it. We neither read nor forward any (D-13).
|
|
42
|
+
*/
|
|
43
|
+
export function buildNpmViewArgs(npmCliJs, packageName, distTag, registry) {
|
|
44
|
+
return [
|
|
45
|
+
npmCliJs,
|
|
46
|
+
'view',
|
|
47
|
+
`${packageName}@${distTag}`,
|
|
48
|
+
'--json',
|
|
49
|
+
'--loglevel=error',
|
|
50
|
+
`--registry=${registry}`,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalise `npm view`'s object into the SAME shape `fetchLatestManifest`
|
|
55
|
+
* returns, so `decideUpdate` cannot tell the two sources apart.
|
|
56
|
+
*
|
|
57
|
+
* `npm view pkg@tag --json` on a dist-tag yields one object. An ARRAY comes back
|
|
58
|
+
* for a range, which we never send - but npm is not ours, so take the last
|
|
59
|
+
* element rather than failing the whole probe on a shape we did not ask for.
|
|
60
|
+
*/
|
|
61
|
+
function normalise(parsed) {
|
|
62
|
+
const one = Array.isArray(parsed) ? parsed[parsed.length - 1] : parsed;
|
|
63
|
+
if (!one || typeof one !== 'object')
|
|
64
|
+
return null;
|
|
65
|
+
const doc = one;
|
|
66
|
+
if (typeof doc.name !== 'string' || typeof doc.version !== 'string')
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
name: doc.name,
|
|
70
|
+
version: doc.version,
|
|
71
|
+
...(doc.engines && typeof doc.engines === 'object'
|
|
72
|
+
? { engines: { ...(typeof doc.engines.node === 'string' ? { node: doc.engines.node } : {}) } }
|
|
73
|
+
: {}),
|
|
74
|
+
// Present ONLY when the version is deprecated. npm writes a string, but
|
|
75
|
+
// `true` appears in the wild; both mean "do not auto-install this".
|
|
76
|
+
...(doc.deprecated !== undefined ? { deprecated: String(doc.deprecated) } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ask the machine's own npm what the dist-tag points at, or `null`.
|
|
81
|
+
*
|
|
82
|
+
* NEVER THROWS - the same contract as `fetchLatestManifest`, for the same
|
|
83
|
+
* reason: the only caller is a background timer whose entire contract is that
|
|
84
|
+
* the user does not find out. Any non-zero exit, any timeout, any unparseable
|
|
85
|
+
* stdout and a `resolveNpmCli()` of `null` all resolve to `null`.
|
|
86
|
+
*
|
|
87
|
+
* NO BARE-`npm`-ON-PATH FALLBACK, for the reason `installer.ts` already gives:
|
|
88
|
+
* on Windows that resolves to `npm.cmd`, which U-2 forbids.
|
|
89
|
+
*/
|
|
90
|
+
export function fetchLatestViaNpm(registry, packageName, distTag, deps = {}) {
|
|
91
|
+
return new Promise((settle) => {
|
|
92
|
+
const npmCliJs = deps.npmCliPath !== undefined ? deps.npmCliPath : resolveNpmCli();
|
|
93
|
+
if (!npmCliJs) {
|
|
94
|
+
settle(null);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const run = deps.execFileImpl ?? execFile;
|
|
98
|
+
const options = {
|
|
99
|
+
// C-7, spelled out rather than inherited: this argv carries a package name
|
|
100
|
+
// read from a manifest on disk and a registry URL from config.
|
|
101
|
+
shell: false,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
timeout: deps.timeoutMs ?? UPDATE_LIMITS.npmViewTimeoutMs,
|
|
104
|
+
maxBuffer: UPDATE_LIMITS.manifestMaxBytes,
|
|
105
|
+
env: cleanInstallEnv(deps.env ?? process.env),
|
|
106
|
+
};
|
|
107
|
+
let settled = false;
|
|
108
|
+
const finish = (value) => {
|
|
109
|
+
if (settled)
|
|
110
|
+
return;
|
|
111
|
+
settled = true;
|
|
112
|
+
settle(value);
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
run(process.execPath, buildNpmViewArgs(npmCliJs, packageName, distTag, registry), options, (error, stdout) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
finish(null);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
finish(normalise(JSON.parse(String(stdout))));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
finish(null);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// `execFile` can throw synchronously on a bad argv or a missing binary.
|
|
130
|
+
finish(null);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|