@superblocksteam/sdk 2.0.123 → 2.0.124-next.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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts +37 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +162 -10
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +377 -8
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts +21 -0
- package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs +83 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts +2 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs +51 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs +170 -14
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +33 -2
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-token-priming.test.d.mts +31 -0
- package/dist/cli-replacement/dev-token-priming.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs +87 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts +36 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.d.mts +2 -0
- package/dist/cli-replacement/dev.interception.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev.interception.test.mjs +68 -0
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.mjs +396 -62
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts +180 -0
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.mjs +283 -0
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts +10 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.mjs +582 -0
- package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs +125 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs +260 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts +58 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs +224 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts +11 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs +317 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts +26 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs +148 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +84 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs +26 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +23 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +21 -9
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.d.mts +2 -0
- package/dist/dev-utils/dev-server.status.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.status.test.mjs +41 -0
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -0
- package/dist/dev-utils/token-manager.d.ts +31 -0
- package/dist/dev-utils/token-manager.d.ts.map +1 -1
- package/dist/dev-utils/token-manager.js +34 -0
- package/dist/dev-utils/token-manager.js.map +1 -1
- package/dist/telemetry/local-obs.js +1 -1
- package/dist/telemetry/local-obs.js.map +1 -1
- package/dist/telemetry/util.js +1 -1
- package/dist/types/scoped-jwt-token-payload.d.ts +1 -0
- package/dist/types/scoped-jwt-token-payload.d.ts.map +1 -1
- package/dist/version-control.d.mts.map +1 -1
- package/dist/version-control.mjs +6 -7
- package/dist/version-control.mjs.map +1 -1
- package/package.json +12 -12
- package/src/cli-replacement/automatic-upgrades.test.ts +530 -8
- package/src/cli-replacement/automatic-upgrades.ts +179 -7
- package/src/cli-replacement/dependency-install-classifier.mts +118 -0
- package/src/cli-replacement/dependency-install-classifier.test.mts +72 -0
- package/src/cli-replacement/dev-s3-restore.test.mts +210 -14
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +35 -2
- package/src/cli-replacement/dev-token-priming.test.mts +103 -0
- package/src/cli-replacement/dev.interception.test.mts +80 -0
- package/src/cli-replacement/dev.mts +495 -92
- package/src/cli-replacement/home-npmrc.mts +409 -0
- package/src/cli-replacement/home-npmrc.test.mts +757 -0
- package/src/cli-replacement/install-packages.classify.test.mts +168 -0
- package/src/cli-replacement/install-packages.npm-registry.test.mts +345 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.mts +296 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.test.mts +482 -0
- package/src/cli-replacement/userconfig-env.integration.test.mts +189 -0
- package/src/dev-utils/dev-server-metrics.mts +96 -0
- package/src/dev-utils/dev-server-metrics.test.mts +38 -0
- package/src/dev-utils/dev-server.mts +48 -8
- package/src/dev-utils/dev-server.status.test.mts +58 -0
- package/src/dev-utils/token-manager.ts +36 -0
- package/src/telemetry/local-obs.ts +1 -1
- package/src/telemetry/util.ts +1 -1
- package/src/types/scoped-jwt-token-payload.ts +1 -0
- package/src/version-control.mts +8 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-publish-package.log +0 -0
|
@@ -13,12 +13,19 @@ import {
|
|
|
13
13
|
NON_SB_ORG_UPDATE_ERROR,
|
|
14
14
|
traceFunction,
|
|
15
15
|
} from "@superblocksteam/shared";
|
|
16
|
+
import {
|
|
17
|
+
bucketNpmRegistryHost,
|
|
18
|
+
redactNpmRegistryHostsFromText,
|
|
19
|
+
} from "@superblocksteam/telemetry";
|
|
16
20
|
import type { LockService } from "@superblocksteam/vite-plugin-file-sync/lock-service";
|
|
21
|
+
import { classifyNpmExecStderr } from "@superblocksteam/vite-plugin-file-sync/npm-error-parser";
|
|
17
22
|
|
|
18
23
|
import type { ResponseMeta } from "../socket/handlers.js";
|
|
19
24
|
import { getTracer } from "../telemetry/index.js";
|
|
20
25
|
import { getErrorMeta, getLogger } from "../telemetry/logging.js";
|
|
21
26
|
import type { ApplicationConfigWithTokenConfigAndUserInfo } from "../types/common.js";
|
|
27
|
+
import { superblocksLogsPath, superblocksNpmrcPath } from "./home-npmrc.mjs";
|
|
28
|
+
import { stripUpgradedCliLockfiles } from "./post-upgrade-lockfile-strip.mjs";
|
|
22
29
|
import {
|
|
23
30
|
getCurrentCliVersion,
|
|
24
31
|
clearCliVersionCache,
|
|
@@ -26,6 +33,70 @@ import {
|
|
|
26
33
|
|
|
27
34
|
export const AUTO_UPGRADE_EXIT_CODE = 99;
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Build the argument vector for the global CLI auto-upgrade install.
|
|
38
|
+
* Extracted so the build path can be tested without driving an exec
|
|
39
|
+
* subprocess.
|
|
40
|
+
*
|
|
41
|
+
* `--audit=false` is npm-only. Userconfig pinning lives in the spawned
|
|
42
|
+
* subprocess env (`NPM_CONFIG_USERCONFIG`) rather than a CLI flag,
|
|
43
|
+
* because pnpm does not accept `--userconfig` (open issue
|
|
44
|
+
* pnpm/pnpm#6036) but does honor the env var through its npm-config
|
|
45
|
+
* compatibility layer. Using the env var means a single mechanism
|
|
46
|
+
* works for both agents.
|
|
47
|
+
*/
|
|
48
|
+
export function buildGlobalInstallArgs(
|
|
49
|
+
pm: { agent: DetectResult["agent"] },
|
|
50
|
+
packageName: string,
|
|
51
|
+
): string[] {
|
|
52
|
+
const installArgs = ["--fund=false", "--save-exact"];
|
|
53
|
+
if (pm.agent === "npm") {
|
|
54
|
+
installArgs.push("--audit=false");
|
|
55
|
+
}
|
|
56
|
+
installArgs.push("--force");
|
|
57
|
+
installArgs.push(packageName);
|
|
58
|
+
return installArgs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the env overlay for a Superblocks-spawned npm/pnpm subprocess.
|
|
63
|
+
* Pins both `NPM_CONFIG_USERCONFIG` (npm + pnpm <= 10) and
|
|
64
|
+
* `PNPM_CONFIG_USERCONFIG` (pnpm 11+) so the install reads its
|
|
65
|
+
* registry/auth lines from the Superblocks-owned `~/.superblocks/npmrc`
|
|
66
|
+
* instead of the default `~/.npmrc`.
|
|
67
|
+
*
|
|
68
|
+
* Why both: pnpm 11 stopped recognising `npm_config_*` env vars (the
|
|
69
|
+
* @pnpm/npm-conf compatibility layer was tightened) and now requires
|
|
70
|
+
* `pnpm_config_*` for its config keys. pnpm <= 10 honors only the
|
|
71
|
+
* `npm_config_*` form; npm has always honored only the `npm_config_*`
|
|
72
|
+
* form. Setting both is harmless on the agent that ignores one of
|
|
73
|
+
* them. Verified by `userconfig-env.integration.test.mts` against both
|
|
74
|
+
* pnpm 10 and pnpm 11 binaries.
|
|
75
|
+
*
|
|
76
|
+
* The CLI-flag alternative (`--userconfig=` for npm,
|
|
77
|
+
* `--config.userconfig=` for pnpm) would require branching on the
|
|
78
|
+
* agent at every call site; the env-overlay approach is uniform.
|
|
79
|
+
*/
|
|
80
|
+
export function buildInstallEnv(
|
|
81
|
+
userconfigPath: string,
|
|
82
|
+
logsDir?: string,
|
|
83
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
84
|
+
): NodeJS.ProcessEnv {
|
|
85
|
+
return {
|
|
86
|
+
...baseEnv,
|
|
87
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
88
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
89
|
+
// Route npm's per-run debug log to `<app>/.superblocks/logs` so a
|
|
90
|
+
// failed install in the live-edit pod leaves a collectable log in a
|
|
91
|
+
// predictable place. npm writes `<logs-dir>/<ts>-debug-0.log` on every
|
|
92
|
+
// run (pruned to `logs-max`, default 10) and creates the dir if it is
|
|
93
|
+
// missing. pnpm has no equivalent logs-dir config and ignores this var
|
|
94
|
+
// (npm-only by design — see `superblocksLogsPath`). Only set when a
|
|
95
|
+
// logs dir is supplied so callers that don't opt in are unaffected.
|
|
96
|
+
...(logsDir ? { NPM_CONFIG_LOGS_DIR: logsDir } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
29
100
|
const exec = promisify(child_process.exec);
|
|
30
101
|
const logger = getLogger();
|
|
31
102
|
const tracer = getTracer();
|
|
@@ -105,17 +176,17 @@ async function upgradeCliWithPackageManager(
|
|
|
105
176
|
pm: DetectResult,
|
|
106
177
|
targetVersion: string,
|
|
107
178
|
alias?: string,
|
|
179
|
+
hasAnyRegistryConfigured?: boolean,
|
|
180
|
+
// Directory npm should drop its per-run debug log into (the app's
|
|
181
|
+
// `.superblocks/logs`). Optional so callers/tests that don't care keep
|
|
182
|
+
// npm's default `_logs` location.
|
|
183
|
+
logsDir?: string,
|
|
108
184
|
): Promise<void> {
|
|
109
185
|
const packageName = alias
|
|
110
186
|
? `@superblocksteam/cli@npm:${alias}@${targetVersion}`
|
|
111
187
|
: `@superblocksteam/cli@${targetVersion}`;
|
|
112
188
|
|
|
113
|
-
const installArgs =
|
|
114
|
-
if (pm.agent === "npm") {
|
|
115
|
-
installArgs.push("--audit=false");
|
|
116
|
-
}
|
|
117
|
-
installArgs.push("--force");
|
|
118
|
-
installArgs.push(packageName);
|
|
189
|
+
const installArgs = buildGlobalInstallArgs({ agent: pm.agent }, packageName);
|
|
119
190
|
|
|
120
191
|
const installCommand = resolveCommand(pm.agent, "global", installArgs);
|
|
121
192
|
|
|
@@ -132,13 +203,84 @@ async function upgradeCliWithPackageManager(
|
|
|
132
203
|
resolver = resolve;
|
|
133
204
|
rejecter = reject;
|
|
134
205
|
});
|
|
206
|
+
// Capture stderr alongside the streaming logger.warn so that, on failure,
|
|
207
|
+
// we can pattern-match the npm error code (APPS-4324) and emit a structured
|
|
208
|
+
// NpmInstallBlocked diagnostic before rejecting. The k8s crash-loop log
|
|
209
|
+
// then carries the reason code rather than just "Failed to upgrade".
|
|
210
|
+
let stderrBuffer = "";
|
|
135
211
|
const cp = child_process.exec(
|
|
136
212
|
`${command} ${args.join(" ")}`,
|
|
137
213
|
{
|
|
138
214
|
cwd: process.cwd(),
|
|
215
|
+
// Point the package manager at the Superblocks-owned userconfig so
|
|
216
|
+
// the global install resolves through whatever registry config
|
|
217
|
+
// `syncHomeNpmrc` materialised at startup, without touching the
|
|
218
|
+
// developer's `~/.npmrc`. Works for both npm and pnpm. `logsDir`
|
|
219
|
+
// also routes npm's debug log into the app's `.superblocks/logs`.
|
|
220
|
+
env: buildInstallEnv(superblocksNpmrcPath(), logsDir),
|
|
139
221
|
},
|
|
140
222
|
(error) => {
|
|
141
223
|
if (error) {
|
|
224
|
+
// `child_process.exec` aggregates stderr on the rejected error (capped
|
|
225
|
+
// by `maxBuffer`); fall back to it when our `stderr.on('data', …)`
|
|
226
|
+
// listener missed the chunks — a subprocess that fails fast can emit
|
|
227
|
+
// and exit before the listener attaches, leaving `stderrBuffer` empty
|
|
228
|
+
// and dropping the structured diagnostic this PR exists to guarantee.
|
|
229
|
+
const errStderr = (error as { stderr?: unknown }).stderr;
|
|
230
|
+
const stderrText =
|
|
231
|
+
stderrBuffer || (typeof errStderr === "string" ? errStderr : "");
|
|
232
|
+
const blocked = classifyNpmExecStderr(stderrText, {
|
|
233
|
+
requestedPackages: [{ name: packageName }],
|
|
234
|
+
hasAnyRegistryConfigured,
|
|
235
|
+
});
|
|
236
|
+
if (blocked) {
|
|
237
|
+
// Structured fields are grep-able from the message body because
|
|
238
|
+
// the local logger contract limits the meta arg to
|
|
239
|
+
// `{ error: { kind, message, stack } }`. The encoding mirrors the
|
|
240
|
+
// `errorId=...` convention in `dev.mts` so operator alerts and
|
|
241
|
+
// log drains can key on `reason=...`. Logged ahead of the legacy
|
|
242
|
+
// "Failed to upgrade" line so the diagnostic still surfaces if a
|
|
243
|
+
// downstream sink truncates further lines.
|
|
244
|
+
// Pass the raw host through `bucketNpmRegistryHost` so this log
|
|
245
|
+
// emits a bounded enum (`public_npm` | `private` | `unknown`)
|
|
246
|
+
// instead of a customer's private registry hostname. Operational
|
|
247
|
+
// logs are a shared destination; raw hosts there leak customer
|
|
248
|
+
// infra identifiers and create unbounded log facets. This mirrors
|
|
249
|
+
// APPS-4189 (PR #19622) which bucketed the same field for llmobs
|
|
250
|
+
// tags via the same helper.
|
|
251
|
+
//
|
|
252
|
+
// `summary` is npm-emitted free text (first non-code error line —
|
|
253
|
+
// "401 Unauthorized - GET https://...", "self-signed certificate
|
|
254
|
+
// in chain", etc.). The phrasing carries diagnostic value beyond
|
|
255
|
+
// the structured `reason` / `subreason` / `npmErrorCode` facets
|
|
256
|
+
// (CA-bundle hints, proxy hints), but it also still echoes the
|
|
257
|
+
// raw URL/host from the npm stderr line. Redact URLs and bare
|
|
258
|
+
// hostnames before logging so this shared operational sink
|
|
259
|
+
// doesn't leak customer registry hostnames (APPS-4381 PR #19634
|
|
260
|
+
// re-review).
|
|
261
|
+
//
|
|
262
|
+
// We also omit the meta arg here: `getErrorMeta(blocked)` would
|
|
263
|
+
// synthesize `{ error: { kind, message, stack } }` from the
|
|
264
|
+
// `NpmInstallBlocked` instance, and the constructor builds
|
|
265
|
+
// `message` as `"npm install blocked (...) against ${host} [code]"`
|
|
266
|
+
// — same host leak, different field. The structured body fields
|
|
267
|
+
// above already convey the kind (`reason` + `subreason` +
|
|
268
|
+
// `npmErrorCode`) without the hostname, so the meta arg has no
|
|
269
|
+
// signal to add.
|
|
270
|
+
const sanitizedSummary = redactNpmRegistryHostsFromText(
|
|
271
|
+
blocked.summary ?? "",
|
|
272
|
+
);
|
|
273
|
+
logger.error(
|
|
274
|
+
`[npm-install-blocked] reason=${blocked.reason}` +
|
|
275
|
+
` subreason=${blocked.subreason ?? "n/a"}` +
|
|
276
|
+
` npmErrorCode=${blocked.npmErrorCode ?? "unknown"}` +
|
|
277
|
+
` registryHost=${bucketNpmRegistryHost(blocked.registryHost)}` +
|
|
278
|
+
` httpStatus=${blocked.httpStatus ?? "n/a"}` +
|
|
279
|
+
` hasAnyRegistryConfigured=${blocked.hasAnyRegistryConfigured ?? "unknown"}` +
|
|
280
|
+
` packages=${JSON.stringify(blocked.packages)}` +
|
|
281
|
+
` summary=${JSON.stringify(sanitizedSummary)}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
142
284
|
logger.error(`Failed to upgrade packages to ${targetVersion}`);
|
|
143
285
|
rejecter(error);
|
|
144
286
|
} else {
|
|
@@ -150,6 +292,7 @@ async function upgradeCliWithPackageManager(
|
|
|
150
292
|
logger.info(data);
|
|
151
293
|
});
|
|
152
294
|
cp.stderr?.on("data", (data) => {
|
|
295
|
+
stderrBuffer += String(data);
|
|
153
296
|
logger.warn(data);
|
|
154
297
|
});
|
|
155
298
|
|
|
@@ -182,6 +325,12 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
182
325
|
config: ApplicationConfigWithTokenConfigAndUserInfo,
|
|
183
326
|
forceUpgrade: boolean = false,
|
|
184
327
|
skipCliUpgrade: boolean = false,
|
|
328
|
+
// App working directory, used to route the CLI-upgrade install's npm
|
|
329
|
+
// debug log into `<app>/.superblocks/logs`. Defaults to `process.cwd()`
|
|
330
|
+
// (the dev-server's cwd === app root) so existing callers/tests are
|
|
331
|
+
// unaffected; `dev()` passes its `cwd` explicitly for consistency with
|
|
332
|
+
// the startup install.
|
|
333
|
+
appDir: string = process.cwd(),
|
|
185
334
|
): Promise<{
|
|
186
335
|
cliUpdated: boolean;
|
|
187
336
|
upgradePromises: Promise<void>[];
|
|
@@ -269,6 +418,16 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
269
418
|
return;
|
|
270
419
|
}
|
|
271
420
|
|
|
421
|
+
// Failure-mode contrast with AppShell's per-app install path: AppShell
|
|
422
|
+
// routes exec failures through `classifyNpmExecError` and emits a
|
|
423
|
+
// structured `NpmInstallBlocked` diagnostic. The auto-upgrade subprocess
|
|
424
|
+
// below does NOT route through that classifier — it `exec`s
|
|
425
|
+
// `npm install -g @superblocksteam/cli@...` directly. A perimeter block
|
|
426
|
+
// (ECONNREFUSED) surfaces here as a generic `Error` caught by the outer
|
|
427
|
+
// try/catch (`hasFailedToUpgrade = true` → release lock → `process.exit`),
|
|
428
|
+
// which Kubernetes restarts. The operator sees a visible crash-loop with
|
|
429
|
+
// "Error upgrading packages" rather than a silent fallback. For symmetry
|
|
430
|
+
// with the install path, wrap the upgrade exec in the same classifier.
|
|
272
431
|
try {
|
|
273
432
|
if (
|
|
274
433
|
!skipCliUpgrade &&
|
|
@@ -291,7 +450,6 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
291
450
|
});
|
|
292
451
|
if (!oclifUpgradeSucceeded) {
|
|
293
452
|
logger.info(`Falling back to package manager upgrade for CLI`);
|
|
294
|
-
// Fall back to package manager upgrade
|
|
295
453
|
await traceFunction({
|
|
296
454
|
name: "upgradePackageWithPackageManager - cli",
|
|
297
455
|
tracer,
|
|
@@ -300,11 +458,25 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
300
458
|
packageManager,
|
|
301
459
|
targetVersions.cli,
|
|
302
460
|
currentCliInfo.alias,
|
|
461
|
+
undefined,
|
|
462
|
+
superblocksLogsPath(appDir),
|
|
303
463
|
);
|
|
304
464
|
},
|
|
305
465
|
});
|
|
306
466
|
cliUpdated = true;
|
|
307
467
|
}
|
|
468
|
+
// The freshly fetched tarball may ship bundled lockfiles with
|
|
469
|
+
// stale public-host `resolved` URLs; strip them so the next
|
|
470
|
+
// install does not bypass the active registry. Non-fatal — a
|
|
471
|
+
// failed post-step must not crash the pod after a working
|
|
472
|
+
// upgrade.
|
|
473
|
+
await traceFunction({
|
|
474
|
+
name: "stripUpgradedCliLockfiles",
|
|
475
|
+
tracer,
|
|
476
|
+
fn: async () => {
|
|
477
|
+
await stripUpgradedCliLockfiles(logger);
|
|
478
|
+
},
|
|
479
|
+
});
|
|
308
480
|
},
|
|
309
481
|
});
|
|
310
482
|
upgradePromises.push(cliUpgradePromise);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { DependencyInstallError } from "@superblocksteam/library-shared/types";
|
|
2
|
+
import {
|
|
3
|
+
classifyNpmExecStderr,
|
|
4
|
+
parseNpmJsonDiagnostic,
|
|
5
|
+
parseNpmJsonError,
|
|
6
|
+
scrubSecrets,
|
|
7
|
+
type NpmInstallBlocked,
|
|
8
|
+
type NpmInstallBlockedReason,
|
|
9
|
+
type ParseContext,
|
|
10
|
+
} from "@superblocksteam/vite-plugin-file-sync/npm-registry";
|
|
11
|
+
|
|
12
|
+
/** Marker thrown by the INITIAL verification install so the dev.mts catch can
|
|
13
|
+
* degrade by ORIGIN (never on classifiability). Carries the classified error. */
|
|
14
|
+
export class InitialInstallFailed extends Error {
|
|
15
|
+
readonly serverError: DependencyInstallError;
|
|
16
|
+
constructor(serverError: DependencyInstallError) {
|
|
17
|
+
super(`initial dependency install failed (${serverError.category})`);
|
|
18
|
+
this.name = "InitialInstallFailed";
|
|
19
|
+
this.serverError = serverError;
|
|
20
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExecFailure {
|
|
25
|
+
stdout?: string;
|
|
26
|
+
stderr?: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const REASON_TO_CATEGORY: Record<
|
|
31
|
+
NpmInstallBlockedReason,
|
|
32
|
+
DependencyInstallError["category"]
|
|
33
|
+
> = {
|
|
34
|
+
not_in_registry: "not_in_registry",
|
|
35
|
+
registry_auth_failed: "registry_auth_failed",
|
|
36
|
+
registry_unreachable: "registry_unreachable",
|
|
37
|
+
tls_failed: "tls_failed",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEP_CONFLICT_CODES = /\b(ERESOLVE|ETARGET|EPEERINVALID)\b/;
|
|
41
|
+
|
|
42
|
+
/** Best-effort: pull package specs out of an ERESOLVE/ETARGET `--json` detail. */
|
|
43
|
+
export function parseDependencyConflict(
|
|
44
|
+
text: string,
|
|
45
|
+
): { name: string; version?: string }[] {
|
|
46
|
+
const out: { name: string; version?: string }[] = [];
|
|
47
|
+
const re =
|
|
48
|
+
/(?:Found:|peer|resolve)\s+(@?[\w./-]+)@("?[^",\s]+"?|undefined)/gi;
|
|
49
|
+
let m: RegExpExecArray | null;
|
|
50
|
+
while ((m = re.exec(text)) !== null) {
|
|
51
|
+
const name = m[1];
|
|
52
|
+
const version = m[2] === "undefined" ? undefined : m[2].replace(/"/g, "");
|
|
53
|
+
if (!out.some((p) => p.name === name)) out.push({ name, version });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fromBlocked(
|
|
59
|
+
b: NpmInstallBlocked,
|
|
60
|
+
rawError: string,
|
|
61
|
+
): DependencyInstallError {
|
|
62
|
+
return {
|
|
63
|
+
type: "dev-server/dependency-install",
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
category: REASON_TO_CATEGORY[b.reason],
|
|
66
|
+
subreason: b.subreason,
|
|
67
|
+
packages: b.packages.map((name) => ({ name })),
|
|
68
|
+
registryHost: b.registryHost,
|
|
69
|
+
npmErrorCode: b.npmErrorCode,
|
|
70
|
+
hasAnyRegistryConfigured: b.hasAnyRegistryConfigured,
|
|
71
|
+
rawError,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Always returns a DependencyInstallError (never null). */
|
|
76
|
+
export function classifyInitialInstallError(
|
|
77
|
+
failure: ExecFailure,
|
|
78
|
+
ctx: ParseContext,
|
|
79
|
+
): DependencyInstallError {
|
|
80
|
+
const stdout = failure.stdout ?? "";
|
|
81
|
+
const stderr = failure.stderr ?? "";
|
|
82
|
+
// Use `||` (not `??`) so an empty `stderr` ("" — the default on L:stderr, and
|
|
83
|
+
// common under `--json`) falls through to the exec rejection's `.message`
|
|
84
|
+
// ("Command failed: …") instead of yielding a blank rawError / Details panel.
|
|
85
|
+
const rawError = scrubSecrets(
|
|
86
|
+
parseNpmJsonDiagnostic(stdout) || stderr || failure.message || "",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const blockedFromJson = parseNpmJsonError(stdout, ctx);
|
|
90
|
+
if (blockedFromJson) return fromBlocked(blockedFromJson, rawError);
|
|
91
|
+
|
|
92
|
+
const combined = `${stdout}\n${stderr}`;
|
|
93
|
+
const codeMatch = combined.match(DEP_CONFLICT_CODES);
|
|
94
|
+
if (codeMatch) {
|
|
95
|
+
return {
|
|
96
|
+
type: "dev-server/dependency-install",
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
category: "dependency_conflict",
|
|
99
|
+
packages: parseDependencyConflict(combined),
|
|
100
|
+
npmErrorCode: codeMatch[1],
|
|
101
|
+
hasAnyRegistryConfigured: ctx.hasAnyRegistryConfigured,
|
|
102
|
+
rawError,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const blockedFromStderr = classifyNpmExecStderr(stderr, ctx);
|
|
107
|
+
if (blockedFromStderr) return fromBlocked(blockedFromStderr, rawError);
|
|
108
|
+
|
|
109
|
+
const unknownCode = combined.match(/npm (?:error|ERR!) code (\S+)/)?.[1];
|
|
110
|
+
return {
|
|
111
|
+
type: "dev-server/dependency-install",
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
category: "unknown",
|
|
114
|
+
npmErrorCode: unknownCode,
|
|
115
|
+
hasAnyRegistryConfigured: ctx.hasAnyRegistryConfigured,
|
|
116
|
+
rawError,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InitialInstallFailed,
|
|
5
|
+
classifyInitialInstallError,
|
|
6
|
+
} from "./dependency-install-classifier.mjs";
|
|
7
|
+
|
|
8
|
+
const ERESOLVE_JSON = JSON.stringify({
|
|
9
|
+
error: {
|
|
10
|
+
code: "ERESOLVE",
|
|
11
|
+
summary: "unable to resolve dependency tree",
|
|
12
|
+
detail:
|
|
13
|
+
'Found: react-router@undefined\nCould not resolve dependency:\npeer react-router@">=6.4" from @superblocksteam/library@2.0.0-SNAPSHOT',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("classifyInitialInstallError", () => {
|
|
18
|
+
it("classifies ERESOLVE as dependency_conflict and extracts packages", () => {
|
|
19
|
+
const e = classifyInitialInstallError(
|
|
20
|
+
{
|
|
21
|
+
stdout: ERESOLVE_JSON,
|
|
22
|
+
stderr: "npm error code ERESOLVE",
|
|
23
|
+
message: "Command failed",
|
|
24
|
+
},
|
|
25
|
+
{ hasAnyRegistryConfigured: true, requestedPackages: [] },
|
|
26
|
+
);
|
|
27
|
+
expect(e.type).toBe("dev-server/dependency-install");
|
|
28
|
+
expect(e.category).toBe("dependency_conflict");
|
|
29
|
+
expect(e.npmErrorCode).toBe("ERESOLVE");
|
|
30
|
+
expect(e.packages?.some((p) => p.name === "react-router")).toBe(true);
|
|
31
|
+
expect(e.rawError.length).toBeGreaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("maps an E404 registry block to not_in_registry", () => {
|
|
35
|
+
const E404 = JSON.stringify({
|
|
36
|
+
error: {
|
|
37
|
+
code: "E404",
|
|
38
|
+
summary: "Not Found - GET https://reg.corp/foo",
|
|
39
|
+
detail: "'foo@1.0.0' is not in this registry.",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const e = classifyInitialInstallError(
|
|
43
|
+
{ stdout: E404, stderr: "", message: "x" },
|
|
44
|
+
{ hasAnyRegistryConfigured: true, requestedPackages: [{ name: "foo" }] },
|
|
45
|
+
);
|
|
46
|
+
expect(e.category).toBe("not_in_registry");
|
|
47
|
+
expect(e.registryHost).toBe("reg.corp");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("falls back to unknown for an unmapped code (still a DependencyInstallError)", () => {
|
|
51
|
+
const e = classifyInitialInstallError(
|
|
52
|
+
{
|
|
53
|
+
stdout: "not json",
|
|
54
|
+
stderr: "npm error code EWEIRD\nsomething broke",
|
|
55
|
+
message: "x",
|
|
56
|
+
},
|
|
57
|
+
{},
|
|
58
|
+
);
|
|
59
|
+
expect(e.category).toBe("unknown");
|
|
60
|
+
expect(e.rawError).toContain("EWEIRD");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("InitialInstallFailed carries the classified error", () => {
|
|
64
|
+
const se = classifyInitialInstallError(
|
|
65
|
+
{ stdout: ERESOLVE_JSON, stderr: "", message: "x" },
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
const marker = new InitialInstallFailed(se);
|
|
69
|
+
expect(marker).toBeInstanceOf(Error);
|
|
70
|
+
expect(marker.serverError).toBe(se);
|
|
71
|
+
});
|
|
72
|
+
});
|