@superblocksteam/sdk 2.0.123 → 2.0.124-next.1
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 +403 -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 +47 -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 +486 -65
- 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 +554 -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 +597 -95
- 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,19 +13,22 @@ import { maskUnixSignals } from "@superblocksteam/util";
|
|
|
13
13
|
import { AiService, AiServiceFeatureFlags, SnapshotManager, isSdkApiTemplate, stripResolvedFromLockfile, } from "@superblocksteam/vite-plugin-file-sync/ai-service";
|
|
14
14
|
import { createGitService } from "@superblocksteam/vite-plugin-file-sync/git-service";
|
|
15
15
|
import { LockService, LockType, } from "@superblocksteam/vite-plugin-file-sync/lock-service";
|
|
16
|
+
import { shouldIgnoreInstallScripts, } from "@superblocksteam/vite-plugin-file-sync/npm-registry";
|
|
16
17
|
import { OperationQueue } from "@superblocksteam/vite-plugin-file-sync/operation-queue";
|
|
17
18
|
import { AutoConnectingRpcClient } from "@superblocksteam/vite-plugin-file-sync/server-rpc";
|
|
18
19
|
import { SyncService } from "@superblocksteam/vite-plugin-file-sync/sync-service";
|
|
20
|
+
import { devServerMetrics } from "../dev-utils/dev-server-metrics.mjs";
|
|
19
21
|
import { createDevServer } from "../dev-utils/dev-server.mjs";
|
|
20
22
|
import { AUTO_UPGRADE_EXIT_CODE } from "../index.js";
|
|
21
23
|
import { getTracer } from "../telemetry/index.js";
|
|
22
24
|
import { getErrorMeta, getLogger } from "../telemetry/logging.js";
|
|
23
|
-
import { checkVersionsAndWritePackageJson } from "./automatic-upgrades.js";
|
|
25
|
+
import { buildInstallEnv, checkVersionsAndWritePackageJson, } from "./automatic-upgrades.js";
|
|
26
|
+
import { classifyInitialInstallError, InitialInstallFailed, } from "./dependency-install-classifier.mjs";
|
|
24
27
|
import { ensureRemoteHasDefaultBranch, getGitErrorFields, } from "./git-repo-setup.mjs";
|
|
28
|
+
import { superblocksLogsPath, superblocksNpmrcPath, syncHomeNpmrc, } from "./home-npmrc.mjs";
|
|
25
29
|
import { normalizeWorkspaceProtocolForNpm } from "./normalize-workspace-protocol.js";
|
|
26
30
|
import { didPackageJsonSnapshotChange, packageJsonSnapshot, packageJsonSnapshotDiagnostic, restoreManagedPackageDependencies, } from "./package-json-snapshot.mjs";
|
|
27
31
|
import { getCurrentCliVersion } from "./version-detection.js";
|
|
28
|
-
const exec = promisify(child_process.exec);
|
|
29
32
|
const passErrorToVSCode = (message, logger) => {
|
|
30
33
|
if (message && process.env.SUPERBLOCKS_VSCODE === "true") {
|
|
31
34
|
// Prefixing with `clierr:` will make the VS code extension capture this message and show it to the user.
|
|
@@ -127,6 +130,73 @@ async function readPkgJson(cwd) {
|
|
|
127
130
|
return null;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
133
|
+
async function readPackageLock(cwd) {
|
|
134
|
+
try {
|
|
135
|
+
return await nodeFs.readFile(path.join(cwd, "package-lock.json"), "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Canonicalize a lockfile for post-install change detection. Every boot,
|
|
143
|
+
* `stripResolvedFromLockfile` (APPS-4300) removes the `resolved` URLs and
|
|
144
|
+
* the validation install writes them back, so comparing raw bytes flags
|
|
145
|
+
* "changed" on EVERY registry-validation boot — re-uploading the full
|
|
146
|
+
* workspace to DBFS forever. Dropping `resolved` from both sides of the
|
|
147
|
+
* comparison (same npm v2+ `.packages` shape the strip targets) means only
|
|
148
|
+
* material resolution changes — added/removed packages, version bumps,
|
|
149
|
+
* integrity changes — trigger the upload.
|
|
150
|
+
*/
|
|
151
|
+
export function lockfileComparisonKey(raw) {
|
|
152
|
+
if (raw === null)
|
|
153
|
+
return null;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
if (parsed?.packages && typeof parsed.packages === "object") {
|
|
157
|
+
for (const entry of Object.values(parsed.packages)) {
|
|
158
|
+
if (entry && typeof entry === "object") {
|
|
159
|
+
delete entry.resolved;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return JSON.stringify(parsed);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Malformed lockfile: fall back to byte-level comparison.
|
|
167
|
+
return raw;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function shouldValidatePrivateRegistryInstall(npmRegistryClient, logger) {
|
|
171
|
+
if (!npmRegistryClient) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const result = await npmRegistryClient.getConfig();
|
|
176
|
+
// `configured` / `stale` → the org is known to route installs through a
|
|
177
|
+
// registry (`stale` = last-known-good config during a config-service
|
|
178
|
+
// outage), so the startup install must run even on identical
|
|
179
|
+
// package.json snapshots: with `resolved` stripped from the lockfiles
|
|
180
|
+
// (APPS-4300), npm re-resolves the entire baked tree through that
|
|
181
|
+
// registry, surfacing missing packages at app-creation time instead of
|
|
182
|
+
// at deploy-time bundle build (APPS-4527).
|
|
183
|
+
//
|
|
184
|
+
// `not-configured` and `unreachable` deliberately do NOT force the
|
|
185
|
+
// install. Not-configured orgs gain nothing from re-resolving against
|
|
186
|
+
// public npm on every boot. And when the config service is unreachable
|
|
187
|
+
// with no last-known-good, we cannot materialize the right `.npmrc` —
|
|
188
|
+
// forcing an install would re-resolve a private-registry org's lockfile
|
|
189
|
+
// against whatever registry happens to be on disk, "validating" against
|
|
190
|
+
// the wrong host and writing its URLs back into the lockfile. Fail
|
|
191
|
+
// closed instead (same principle as the AppShell short-circuit,
|
|
192
|
+
// APPS-4370): skip validation and keep today's snapshot decision.
|
|
193
|
+
return result.source === "configured" || result.source === "stale";
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
logger.warn("Could not resolve npm registry config for startup install validation; preserving package snapshot decision", getErrorMeta(err));
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
130
200
|
async function normalizePackageJsonForNpm(cwd, logger) {
|
|
131
201
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
132
202
|
let raw;
|
|
@@ -160,7 +230,7 @@ async function normalizePackageJsonForNpm(cwd, logger) {
|
|
|
160
230
|
throw new Error(`Failed to write normalized package.json at ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
161
231
|
}
|
|
162
232
|
}
|
|
163
|
-
async function installPackages(cwd, logger) {
|
|
233
|
+
export async function installPackages(cwd, logger, npmRegistryClient) {
|
|
164
234
|
try {
|
|
165
235
|
const pm = await detect({
|
|
166
236
|
strategies: [
|
|
@@ -181,24 +251,145 @@ async function installPackages(cwd, logger) {
|
|
|
181
251
|
// resolve a published version instead.
|
|
182
252
|
await normalizePackageJsonForNpm(cwd, logger);
|
|
183
253
|
}
|
|
184
|
-
|
|
254
|
+
// Honor the per-org `allow_install_scripts` policy on the dev-server
|
|
255
|
+
// startup install just like AppShell does for agent-driven installs
|
|
256
|
+
// (see `shell.ts`). When the org has opted out (`allowInstallScripts ===
|
|
257
|
+
// false`), append `--ignore-scripts` so lifecycle scripts don't run
|
|
258
|
+
// during dev-server boot. `undefined` (LD flag off, server omitted the
|
|
259
|
+
// field, no client wired up, or the resolution itself failed) keeps
|
|
260
|
+
// today's behaviour. The client owns last-known-good / cross-outage
|
|
261
|
+
// policy preservation; we deliberately swallow resolution errors here
|
|
262
|
+
// so a transient registry-endpoint outage doesn't abort the user's
|
|
263
|
+
// startup install — the policy default is "scripts allowed" anyway.
|
|
264
|
+
let ignoreScripts = false;
|
|
265
|
+
if (npmRegistryClient) {
|
|
266
|
+
try {
|
|
267
|
+
const result = await npmRegistryClient.getConfig();
|
|
268
|
+
ignoreScripts = shouldIgnoreInstallScripts(result);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
logger.warn("Could not resolve npm install-scripts policy; proceeding without --ignore-scripts", getErrorMeta(err));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// `--json` makes npm channel the structured error envelope (code +
|
|
275
|
+
// summary + detail) onto stdout so a failure can be classified into a
|
|
276
|
+
// DependencyInstallError. Output-only: it does not change resolution,
|
|
277
|
+
// only how npm reports it. The success-path `logger.info(stdout)` below
|
|
278
|
+
// then logs JSON, which is acceptable.
|
|
279
|
+
const baseArgs = pm.agent === "npm" ? ["--fund=false", "--audit=false", "--json"] : [];
|
|
280
|
+
const installArgs = ignoreScripts
|
|
281
|
+
? [...baseArgs, "--ignore-scripts"]
|
|
282
|
+
: baseArgs;
|
|
283
|
+
const installCommand = resolveCommand(pm.agent, "install", installArgs);
|
|
185
284
|
if (!installCommand) {
|
|
186
285
|
logger.warn(`Could not determine install command for ${pm.agent}, skipping package installation`);
|
|
187
286
|
return;
|
|
188
287
|
}
|
|
189
288
|
const { command, args } = installCommand;
|
|
190
289
|
logger.info(`Running ${command} ${args.join(" ")} to install dependencies…`);
|
|
290
|
+
// Pin both npm and pnpm to the Superblocks-owned userconfig via env
|
|
291
|
+
// overlay. `buildInstallEnv` (from `automatic-upgrades.ts`) sets
|
|
292
|
+
// both `NPM_CONFIG_USERCONFIG` (npm, pnpm <= 10) and
|
|
293
|
+
// `PNPM_CONFIG_USERCONFIG` (pnpm 11+) — pnpm 11 stopped honouring
|
|
294
|
+
// the `npm_config_*` env vars. CLI flags differ between agents
|
|
295
|
+
// (`--userconfig=` for npm, `--config.userconfig=` for pnpm), so
|
|
296
|
+
// the env overlay is the uniform mechanism. Without this, customer
|
|
297
|
+
// pods using a private registry would read default `~/.npmrc`
|
|
298
|
+
// (empty after the relocation) and fail to resolve private
|
|
299
|
+
// packages. Centralised in `buildInstallEnv` so this and the CLI
|
|
300
|
+
// auto-upgrade install stay in sync.
|
|
301
|
+
// Ensure the per-app log dir exists so npm's debug log (routed via
|
|
302
|
+
// `NPM_CONFIG_LOGS_DIR` in `buildInstallEnv`) lands in a predictable
|
|
303
|
+
// place. npm would create it on demand, but doing it up front means the
|
|
304
|
+
// folder exists even for pnpm runs (which ignore the var) and for any
|
|
305
|
+
// log-collection sidecar watching `<app>/.superblocks/logs`.
|
|
306
|
+
const logsDir = superblocksLogsPath(cwd);
|
|
307
|
+
await nodeFs.mkdir(logsDir, { recursive: true }).catch(() => undefined);
|
|
308
|
+
// Resolve the promisified `exec` at CALL time — not module scope — so a
|
|
309
|
+
// test's `vi.mock("node:child_process")` is always honoured regardless of
|
|
310
|
+
// when `dev.mjs` was first evaluated. A module-scope capture binds to
|
|
311
|
+
// whichever `child_process.exec` was live at first import; if a sibling
|
|
312
|
+
// test imports this module before installing its own mock (e.g.
|
|
313
|
+
// `dev-token-priming` / `dev.interception`, which don't mock
|
|
314
|
+
// `child_process`), that binding is the REAL npm and the classify test's
|
|
315
|
+
// mock silently never takes effect — the exact APPS-4450 CI failure
|
|
316
|
+
// (`category: "unknown"`, `npmErrorCode: undefined` from real npm against a
|
|
317
|
+
// missing `/tmp/app`).
|
|
318
|
+
const exec = promisify(child_process.exec);
|
|
191
319
|
const { stdout } = await exec(`${command} ${args.join(" ")}`, {
|
|
192
320
|
cwd,
|
|
321
|
+
env: buildInstallEnv(superblocksNpmrcPath(), logsDir),
|
|
193
322
|
});
|
|
194
323
|
logger.info("Package installation completed successfully");
|
|
195
324
|
logger.info(stdout);
|
|
196
325
|
}
|
|
197
326
|
catch (error) {
|
|
198
327
|
logger.error("Error during package installation", getErrorMeta(error));
|
|
199
|
-
|
|
328
|
+
// `util.promisify(child_process.exec)` preserves `.stdout`/`.stderr` on
|
|
329
|
+
// the rejected error separately; with `--json`, the structured npm error
|
|
330
|
+
// envelope lands on `.stdout`. Classify it into a DependencyInstallError
|
|
331
|
+
// and throw the `InitialInstallFailed` marker so the `dev()` catch can
|
|
332
|
+
// degrade by origin rather than crash-looping the dev-server pod.
|
|
333
|
+
const ctx = await buildInstallParseContext(cwd, npmRegistryClient);
|
|
334
|
+
const f = error;
|
|
335
|
+
const serverError = classifyInitialInstallError({ stdout: f.stdout, stderr: f.stderr, message: f.message }, ctx);
|
|
336
|
+
throw new InitialInstallFailed(serverError);
|
|
200
337
|
}
|
|
201
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Build the `ParseContext` the dependency-install classifier needs from the
|
|
341
|
+
* failing install's working directory + registry client:
|
|
342
|
+
*
|
|
343
|
+
* - `requestedPackages` — the union of `dependencies` + `devDependencies`
|
|
344
|
+
* in the app's `package.json`, used by the registry-blocked renderers to
|
|
345
|
+
* name the failing specs when npm's `--json` `detail` doesn't.
|
|
346
|
+
* - `hasAnyRegistryConfigured` — the tri-state derived from the registry
|
|
347
|
+
* client's most recent `getConfig()`, mirroring AppShell's
|
|
348
|
+
* `deriveHasAnyRegistryConfigured` (`shell.ts`): `configured`/`stale`
|
|
349
|
+
* → `true` (rows known to exist), `not-configured` → `false` (deliberate
|
|
350
|
+
* "no rows"), `unreachable` → `undefined` ("we don't know" — the renderer
|
|
351
|
+
* falls back to the default variant). Left `undefined` when no client is
|
|
352
|
+
* wired in or the resolution itself throws.
|
|
353
|
+
*
|
|
354
|
+
* Best-effort throughout: a missing/unparseable package.json or a registry
|
|
355
|
+
* outage must not mask the underlying install failure, so both lookups are
|
|
356
|
+
* caught and degrade to empty/undefined.
|
|
357
|
+
*/
|
|
358
|
+
async function buildInstallParseContext(cwd, npmRegistryClient) {
|
|
359
|
+
let requestedPackages = [];
|
|
360
|
+
try {
|
|
361
|
+
const pkg = JSON.parse(await nodeFs.readFile(path.join(cwd, "package.json"), "utf8"));
|
|
362
|
+
requestedPackages = Object.entries({
|
|
363
|
+
...(pkg.dependencies ?? {}),
|
|
364
|
+
...(pkg.devDependencies ?? {}),
|
|
365
|
+
}).map(([name, version]) => ({ name, version: String(version) }));
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* best-effort: a missing/unparseable package.json yields no specs */
|
|
369
|
+
}
|
|
370
|
+
let hasAnyRegistryConfigured;
|
|
371
|
+
if (npmRegistryClient) {
|
|
372
|
+
try {
|
|
373
|
+
const result = await npmRegistryClient.getConfig();
|
|
374
|
+
switch (result.source) {
|
|
375
|
+
case "configured":
|
|
376
|
+
case "stale":
|
|
377
|
+
hasAnyRegistryConfigured = true;
|
|
378
|
+
break;
|
|
379
|
+
case "not-configured":
|
|
380
|
+
hasAnyRegistryConfigured = false;
|
|
381
|
+
break;
|
|
382
|
+
case "unreachable":
|
|
383
|
+
hasAnyRegistryConfigured = undefined;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
/* leave undefined → renderers treat as "don't know" / default variant */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { requestedPackages, hasAnyRegistryConfigured };
|
|
392
|
+
}
|
|
202
393
|
export var DevServerAutoUpgradeMode;
|
|
203
394
|
(function (DevServerAutoUpgradeMode) {
|
|
204
395
|
DevServerAutoUpgradeMode["SKIP"] = "skip-upgrade";
|
|
@@ -212,8 +403,61 @@ export var DevServerAutoUpgradeMode;
|
|
|
212
403
|
*/
|
|
213
404
|
DevServerAutoUpgradeMode["SKIP_CLI_ONLY"] = "skip-cli-only";
|
|
214
405
|
})(DevServerAutoUpgradeMode || (DevServerAutoUpgradeMode = {}));
|
|
406
|
+
/**
|
|
407
|
+
* Seed `tokenManager` with the initial token the CLI received from auth.json
|
|
408
|
+
* (standalone dev) or `/_sb_activate` (SABS live-edit pod activation), so the
|
|
409
|
+
* `NpmRegistryClient`'s JWT source has a usable bearer credential on cold
|
|
410
|
+
* boot.
|
|
411
|
+
*
|
|
412
|
+
* Without this seeding, `TokenManager.updateToken` is only ever called by
|
|
413
|
+
* `AuthHotReloadServer` (`auth-hot-reload.mts`) — and that socket is
|
|
414
|
+
* explicitly disabled on live-edit pods (`sabs/entrypoint-local.sh` sets
|
|
415
|
+
* `SUPERBLOCKS_AUTH_HOT_RELOAD=false`), so `NpmRegistryClient.getConfig()`
|
|
416
|
+
* cannot authenticate its server fetch on cold boot. The result is
|
|
417
|
+
* `source: "unreachable"`, `syncHomeNpmrc` skips with `~/.npmrc` left
|
|
418
|
+
* untouched, and the CLI auto-upgrade that fires moments later resolves
|
|
419
|
+
* `npm install -g @superblocksteam/cli@…` through public npm instead of the
|
|
420
|
+
* customer's configured private registry.
|
|
421
|
+
*
|
|
422
|
+
* Call-site ordering is intentionally NOT load-bearing: `TokenManager`
|
|
423
|
+
* retains the current token as state and `AiService` reads
|
|
424
|
+
* `tokenManager.getCurrentToken()` synchronously at construction time, so
|
|
425
|
+
* the prime call works whether it runs before or after consumer
|
|
426
|
+
* construction. The `tokenUpdated` event stream stays as the refresh
|
|
427
|
+
* channel for future rotations (hot-reload pushes, JWT renewal), so this
|
|
428
|
+
* seed is purely additive.
|
|
429
|
+
*/
|
|
430
|
+
export function primeTokenManagerWithInitialToken(tokenManager, token) {
|
|
431
|
+
if (token) {
|
|
432
|
+
tokenManager.updateToken(token);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/** Decide how the startup catch handles an error: degrade (record, keep Vite up)
|
|
436
|
+
* for an app-install failure (the InitialInstallFailed marker), or exit for
|
|
437
|
+
* anything else (lock/sync/upgrade). Pure + unit-tested. Does NOT call process.exit. */
|
|
438
|
+
export function handleStartupError(error, status, logger) {
|
|
439
|
+
if (error instanceof InitialInstallFailed) {
|
|
440
|
+
status.serverErrors.push(error.serverError);
|
|
441
|
+
logger.error("[dev-server] initial dependency install failed; keeping dev server up", getErrorMeta(error));
|
|
442
|
+
devServerMetrics.recordInitialInstallFailure({
|
|
443
|
+
category: error.serverError.category,
|
|
444
|
+
npmErrorCode: error.serverError.npmErrorCode,
|
|
445
|
+
hasAnyRegistryConfigured: error.serverError.hasAnyRegistryConfigured,
|
|
446
|
+
});
|
|
447
|
+
return "degrade";
|
|
448
|
+
}
|
|
449
|
+
return "exit";
|
|
450
|
+
}
|
|
215
451
|
export async function dev(options) {
|
|
216
452
|
const { cwd, tokenConfig, devServerPort, skipSync, applicationConfig, autoUpgradeMode, tokenManager, authHotReloadServer, sdk, } = options;
|
|
453
|
+
// Seed the tokenManager with the initial CLI token so downstream consumers
|
|
454
|
+
// (AiService, AutoConnectingRpcClient, syncHomeNpmrc) have a usable
|
|
455
|
+
// bearer credential without waiting on the AuthHotReloadServer push
|
|
456
|
+
// refresh (which is disabled on live-edit pods). Order-independent: the
|
|
457
|
+
// manager retains state and AiService reads it synchronously during
|
|
458
|
+
// construction. See `primeTokenManagerWithInitialToken` for the
|
|
459
|
+
// cold-boot rationale.
|
|
460
|
+
primeTokenManagerWithInitialToken(tokenManager, tokenConfig.token);
|
|
217
461
|
// May be overridden by a pending snapshot restore
|
|
218
462
|
let { downloadFirst, uploadFirst } = options;
|
|
219
463
|
// Services that will be created
|
|
@@ -224,17 +468,40 @@ export async function dev(options) {
|
|
|
224
468
|
let snapshotManager;
|
|
225
469
|
let gitUserName;
|
|
226
470
|
let gitUserEmail;
|
|
227
|
-
// In-flight
|
|
228
|
-
//
|
|
229
|
-
//
|
|
471
|
+
// In-flight handles for the two background jobs we launch from sync.
|
|
472
|
+
// We keep them SEPARATE so an install rejection cannot swallow an upgrade
|
|
473
|
+
// rejection (or vice-versa) the way a single `Promise.all` would: that
|
|
474
|
+
// bundle settles on the FIRST rejection and silently absorbs the other's
|
|
475
|
+
// outcome, breaking APPS-4457's intent that auto-upgrade failures still
|
|
476
|
+
// exit (not degrade) and CLI-restart-on-upgrade still happens even if the
|
|
477
|
+
// app install fails.
|
|
478
|
+
//
|
|
479
|
+
// - packageUpgradePromise: library upgrades (and their `npm install`
|
|
480
|
+
// side-effects) from `checkVersionsAndWritePackageJson`. Rejection is
|
|
481
|
+
// OUT of scope for graceful degrade (APPS-4457) — `handleStartupError`
|
|
482
|
+
// routes it to `process.exit(1)`.
|
|
483
|
+
// - packageInstallPromise: the app's verification `npm install`.
|
|
484
|
+
// Rejection MAY be the `InitialInstallFailed` marker, which
|
|
485
|
+
// `handleStartupError` routes to the degrade path.
|
|
486
|
+
let packageUpgradePromise;
|
|
230
487
|
let packageInstallPromise;
|
|
231
488
|
const tracer = getTracer();
|
|
232
489
|
const logger = getLogger(options.logger);
|
|
233
|
-
// Joins the in-flight
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
490
|
+
// Joins the in-flight upgrade. Rejection propagates so the caller's step
|
|
491
|
+
// can abort cleanly (handleStartupError exits — auto-upgrade graceful
|
|
492
|
+
// degrade is OOS per APPS-4457).
|
|
493
|
+
const joinPackageUpgrade = async (reason) => {
|
|
494
|
+
if (!packageUpgradePromise) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
logger.info(`Waiting for background package upgrade (${reason})…`);
|
|
498
|
+
const promise = packageUpgradePromise;
|
|
499
|
+
packageUpgradePromise = undefined;
|
|
500
|
+
await promise;
|
|
501
|
+
};
|
|
502
|
+
// Joins the in-flight install. Rejection propagates so the caller's step
|
|
503
|
+
// can abort cleanly (handleStartupError degrades for InitialInstallFailed,
|
|
504
|
+
// exits otherwise).
|
|
238
505
|
const joinPackageInstall = async (reason) => {
|
|
239
506
|
if (!packageInstallPromise) {
|
|
240
507
|
return;
|
|
@@ -244,10 +511,34 @@ export async function dev(options) {
|
|
|
244
511
|
packageInstallPromise = undefined;
|
|
245
512
|
await promise;
|
|
246
513
|
};
|
|
514
|
+
// Settles the in-flight install WITHOUT throwing. Used on the CLI-restart
|
|
515
|
+
// path so an install failure doesn't preempt the restart (the new CLI's
|
|
516
|
+
// first boot re-runs install). Still waits for npm to finish so we don't
|
|
517
|
+
// SIGKILL it mid-rename.
|
|
518
|
+
const settlePackageInstall = async (reason) => {
|
|
519
|
+
if (!packageInstallPromise) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
logger.info(`Settling background package install (${reason})…`);
|
|
523
|
+
const promise = packageInstallPromise;
|
|
524
|
+
packageInstallPromise = undefined;
|
|
525
|
+
await Promise.allSettled([promise]);
|
|
526
|
+
};
|
|
527
|
+
// Joins upgrade THEN install. Upgrade failures surface first so
|
|
528
|
+
// handleStartupError exits (per APPS-4457) before an install rejection
|
|
529
|
+
// gets a chance to route to degrade.
|
|
530
|
+
const joinUpgradeThenInstall = async (reason) => {
|
|
531
|
+
await joinPackageUpgrade(reason);
|
|
532
|
+
await joinPackageInstall(reason);
|
|
533
|
+
};
|
|
247
534
|
const skipAutoUpgrade = autoUpgradeMode === DevServerAutoUpgradeMode.SKIP;
|
|
248
535
|
const skipCliUpgrade = skipAutoUpgrade ||
|
|
249
536
|
autoUpgradeMode === DevServerAutoUpgradeMode.SKIP_CLI_ONLY;
|
|
250
537
|
await tracer.startActiveSpan("devServerStartup", async (startupSpan) => {
|
|
538
|
+
// Mutable startup status surfaced to the browser via createDevServer's
|
|
539
|
+
// /_sb_connect + /_sb_status. The startup catch records an app-install
|
|
540
|
+
// failure here (degrade path) so Vite still starts and serves the error.
|
|
541
|
+
const devServerStatus = { serverErrors: [] };
|
|
251
542
|
try {
|
|
252
543
|
// Add check for node_modules
|
|
253
544
|
if (!fs.existsSync(path.join(cwd, "node_modules"))) {
|
|
@@ -536,15 +827,39 @@ export async function dev(options) {
|
|
|
536
827
|
else if (downloadFirst && isSynced) {
|
|
537
828
|
logger.info("[dev-startup] Skipping download, already in sync");
|
|
538
829
|
}
|
|
539
|
-
// Unconditional lockfile sanitation: strip
|
|
540
|
-
// `resolved` URLs from any lockfile on disk
|
|
541
|
-
// install runs next. The lockfile here is
|
|
542
|
-
// DBFS path (downloadFirst overwrite,
|
|
543
|
-
// import); npm honors `resolved` verbatim
|
|
544
|
-
// active registry. `integrity` is
|
|
545
|
-
// cross-registry tarball drift surfaces as
|
|
546
|
-
// there's no lockfile or no `resolved`
|
|
830
|
+
// Unconditional lockfile sanitation (APPS-4300): strip
|
|
831
|
+
// cross-registry `resolved` URLs from any lockfile on disk
|
|
832
|
+
// regardless of whether install runs next. The lockfile here is
|
|
833
|
+
// whatever survived the DBFS path (downloadFirst overwrite,
|
|
834
|
+
// prior boot, brownfield import); npm honors `resolved` verbatim
|
|
835
|
+
// and would bypass the active registry. `integrity` is
|
|
836
|
+
// preserved, so genuine cross-registry tarball drift surfaces as
|
|
837
|
+
// EINTEGRITY. No-op when there's no lockfile or no `resolved`
|
|
838
|
+
// entries.
|
|
547
839
|
await stripResolvedFromLockfile(cwd);
|
|
840
|
+
// Materialise `~/.superblocks/npmrc` from the server-fetched
|
|
841
|
+
// per-org npm registry config BEFORE the global Superblocks CLI
|
|
842
|
+
// auto-upgrade fires. With the file in place, the auto-upgrade's
|
|
843
|
+
// `npm install -g @superblocksteam/cli@…` resolves through the
|
|
844
|
+
// customer's private registry instead of `registry.npmjs.org`.
|
|
845
|
+
await tracer.startActiveSpan("syncHomeNpmrc", async (span) => {
|
|
846
|
+
try {
|
|
847
|
+
if (!aiService) {
|
|
848
|
+
logger.info("[home-npmrc] skipped: AiService unavailable at startup");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
await syncHomeNpmrc({
|
|
852
|
+
npmRegistryClient: aiService.getNpmRegistryClient(),
|
|
853
|
+
logger,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
logger.warn("[home-npmrc] sync step failed unexpectedly; ~/.superblocks/npmrc left untouched", getErrorMeta(error));
|
|
858
|
+
}
|
|
859
|
+
finally {
|
|
860
|
+
span.end();
|
|
861
|
+
}
|
|
862
|
+
});
|
|
548
863
|
let hasCliUpdated = false;
|
|
549
864
|
let upgradePromises = [];
|
|
550
865
|
const forceUpgrade = options.autoUpgradeMode === DevServerAutoUpgradeMode.FORCE;
|
|
@@ -565,7 +880,10 @@ export async function dev(options) {
|
|
|
565
880
|
organizationId: currentUser.user.currentOrganizationId,
|
|
566
881
|
featureFlags: sdk.getFeatureFlagsForUser(currentUser),
|
|
567
882
|
};
|
|
568
|
-
const result = await checkVersionsAndWritePackageJson(lockService, applicationConfigWithTokenConfigAndUserInfo, forceUpgrade, skipCliUpgrade
|
|
883
|
+
const result = await checkVersionsAndWritePackageJson(lockService, applicationConfigWithTokenConfigAndUserInfo, forceUpgrade, skipCliUpgrade,
|
|
884
|
+
// Route the CLI-upgrade install's npm debug log into the
|
|
885
|
+
// same `<app>/.superblocks/logs` as the startup install.
|
|
886
|
+
cwd);
|
|
569
887
|
hasCliUpdated = result.cliUpdated;
|
|
570
888
|
upgradePromises = result.upgradePromises;
|
|
571
889
|
}
|
|
@@ -604,11 +922,17 @@ export async function dev(options) {
|
|
|
604
922
|
if (!packageJsonBefore && packageJsonRequiresInstall) {
|
|
605
923
|
logger.info("package.json was created, installing packages…");
|
|
606
924
|
}
|
|
925
|
+
const npmRegistryClient = aiService?.getNpmRegistryClient();
|
|
607
926
|
const forcePackageInstallRequested = !!options.forcePackageInstall;
|
|
608
927
|
let forcePackageInstall = forcePackageInstallRequested;
|
|
928
|
+
const privateRegistryRequiresInstallValidation = packageJsonAfter
|
|
929
|
+
? await shouldValidatePrivateRegistryInstall(npmRegistryClient, logger)
|
|
930
|
+
: false;
|
|
609
931
|
if (forcePackageInstallRequested &&
|
|
610
932
|
hasPackageJsonSnapshotBeforeRestore) {
|
|
611
|
-
forcePackageInstall =
|
|
933
|
+
forcePackageInstall =
|
|
934
|
+
packageJsonRequiresInstall ||
|
|
935
|
+
privateRegistryRequiresInstallValidation;
|
|
612
936
|
}
|
|
613
937
|
logger.info("Package install decision", {
|
|
614
938
|
packageJsonBeforePresent: !!packageJsonBefore,
|
|
@@ -617,14 +941,45 @@ export async function dev(options) {
|
|
|
617
941
|
packageJsonRequiresInstall,
|
|
618
942
|
forcePackageInstall,
|
|
619
943
|
forcePackageInstallRequested,
|
|
944
|
+
privateRegistryRequiresInstallValidation,
|
|
620
945
|
upgradePromiseCount: upgradePromises.length,
|
|
621
946
|
packageJsonSnapshotBefore: packageJsonSnapshotDiagnostic(packageJsonSnapshotBefore),
|
|
622
947
|
packageJsonInstallBaselineSnapshot: packageJsonSnapshotDiagnostic(packageJsonInstallBaselineSnapshot),
|
|
623
948
|
packageJsonSnapshotAfter: packageJsonSnapshotDiagnostic(packageJsonSnapshotAfter),
|
|
624
949
|
});
|
|
950
|
+
const installApplicationId = applicationConfig.id;
|
|
951
|
+
// Launch upgrades and app install as INDEPENDENT promises (not a
|
|
952
|
+
// single `Promise.all`) so each outcome is observed on its own
|
|
953
|
+
// join: upgrade failure exits (APPS-4457), install failure may
|
|
954
|
+
// degrade (InitialInstallFailed). A bundled `Promise.all` would
|
|
955
|
+
// settle on the first rejection and silently absorb the other's
|
|
956
|
+
// result. The `.catch` backstops convert "no join fired" cases
|
|
957
|
+
// into logged-then-handled rejections instead of unhandled ones.
|
|
958
|
+
if (upgradePromises.length > 0) {
|
|
959
|
+
logger.info("Starting package upgrade in background…");
|
|
960
|
+
const launchedUpgradeCount = upgradePromises.length;
|
|
961
|
+
packageUpgradePromise = tracer.startActiveSpan("packageUpgrades", async (span) => {
|
|
962
|
+
try {
|
|
963
|
+
await Promise.all(upgradePromises);
|
|
964
|
+
}
|
|
965
|
+
finally {
|
|
966
|
+
span.end();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
packageUpgradePromise.catch((err) => {
|
|
970
|
+
logger.error(`Background package upgrade failed [errorId=DEV_SERVER_BG_UPGRADE_FAILED applicationId=${installApplicationId} cwd=${cwd} upgradePromiseCount=${launchedUpgradeCount}]`, getErrorMeta(err));
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
// Run the verification install when EITHER the package.json
|
|
974
|
+
// requires it, upgrades just modified package.json/lockfile
|
|
975
|
+
// (re-syncing node_modules), the caller forced it, or a custom
|
|
976
|
+
// registry must validate that the required packages resolve there.
|
|
977
|
+
let packageLockBeforeInstall = null;
|
|
625
978
|
if (packageJsonRequiresInstall ||
|
|
626
979
|
upgradePromises.length > 0 ||
|
|
627
|
-
forcePackageInstall
|
|
980
|
+
forcePackageInstall ||
|
|
981
|
+
privateRegistryRequiresInstallValidation) {
|
|
982
|
+
packageLockBeforeInstall = await readPackageLock(cwd);
|
|
628
983
|
// Launch the install while the upload/restart decisions below
|
|
629
984
|
// are still being evaluated. The synchronous joins at upload,
|
|
630
985
|
// CLI restart, and pre-Vite-startup all observe and surface
|
|
@@ -634,50 +989,80 @@ export async function dev(options) {
|
|
|
634
989
|
logger.info("Starting package install in background…");
|
|
635
990
|
packageInstallPromise = tracer.startActiveSpan("installPackages", async (span) => {
|
|
636
991
|
try {
|
|
637
|
-
|
|
638
|
-
await Promise.all([
|
|
639
|
-
...upgradePromises,
|
|
640
|
-
installPackages(cwd, logger),
|
|
641
|
-
]);
|
|
992
|
+
await installPackages(cwd, logger, npmRegistryClient);
|
|
642
993
|
}
|
|
643
994
|
finally {
|
|
644
995
|
span.end();
|
|
645
996
|
}
|
|
646
997
|
});
|
|
647
|
-
// Backstop: assigning `.catch` to a separate (discarded) promise
|
|
648
|
-
// keeps `packageInstallPromise` itself rejecting, so the joins
|
|
649
|
-
// can still observe and abort. Without this, an install that
|
|
650
|
-
// fails before any join fires becomes an unhandled rejection.
|
|
651
|
-
const installApplicationId = applicationConfig.id;
|
|
652
|
-
const installUpgradeCount = upgradePromises.length;
|
|
653
998
|
packageInstallPromise.catch((err) => {
|
|
654
999
|
logger.error(
|
|
655
1000
|
// errorId is encoded into the message body because the
|
|
656
1001
|
// logger.error contract limits structured attributes to
|
|
657
1002
|
// `{ error: { kind, message, stack } }`. The id stays
|
|
658
1003
|
// grep-able for Datadog/Sentry alert rules.
|
|
659
|
-
`Background package install failed [errorId=DEV_SERVER_BG_INSTALL_FAILED applicationId=${installApplicationId} cwd=${cwd}
|
|
1004
|
+
`Background package install failed [errorId=DEV_SERVER_BG_INSTALL_FAILED applicationId=${installApplicationId} cwd=${cwd}]`, getErrorMeta(err));
|
|
660
1005
|
});
|
|
661
1006
|
}
|
|
662
1007
|
else {
|
|
663
1008
|
logger.info("package.json has not changed, skipping package installation");
|
|
664
1009
|
}
|
|
665
1010
|
const shouldUploadPackageState = hasPackageChanged || forcePackageInstall;
|
|
666
|
-
|
|
1011
|
+
let shouldUploadAfterInstall = shouldUploadPackageState;
|
|
1012
|
+
if (shouldUploadPackageState ||
|
|
1013
|
+
uploadFirst ||
|
|
1014
|
+
privateRegistryRequiresInstallValidation) {
|
|
667
1015
|
// Upload serializes the post-install lockfile + node_modules
|
|
668
|
-
// tree to DBFS, so it must observe
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1016
|
+
// tree to DBFS, so it must observe quiesced upgrade+install.
|
|
1017
|
+
// Upgrade first — its rejection exits before an install
|
|
1018
|
+
// rejection can take the degrade path (APPS-4457).
|
|
1019
|
+
//
|
|
1020
|
+
// BUT: if a successful CLI upgrade is pending restart
|
|
1021
|
+
// (`hasCliUpdated`), the restart branch below MUST win over
|
|
1022
|
+
// the install-failure degrade path. Otherwise the outer
|
|
1023
|
+
// sync/setup catch routes `InitialInstallFailed` to "degrade"
|
|
1024
|
+
// and skips the restart entirely — masking a successful CLI
|
|
1025
|
+
// upgrade. Catch ONLY the join (not the upload body) so a
|
|
1026
|
+
// post-join upload failure still propagates normally.
|
|
1027
|
+
let skipStartupUploadForCliRestart = false;
|
|
1028
|
+
try {
|
|
1029
|
+
await joinUpgradeThenInstall("before upload");
|
|
1030
|
+
}
|
|
1031
|
+
catch (joinError) {
|
|
1032
|
+
if (hasCliUpdated &&
|
|
1033
|
+
joinError instanceof InitialInstallFailed) {
|
|
1034
|
+
logger.info("Initial package install failed before startup upload, but CLI was updated; skipping upload and letting the restart proceed");
|
|
1035
|
+
skipStartupUploadForCliRestart = true;
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
throw joinError;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (privateRegistryRequiresInstallValidation &&
|
|
1042
|
+
!shouldUploadAfterInstall &&
|
|
1043
|
+
lockfileComparisonKey(packageLockBeforeInstall) !==
|
|
1044
|
+
lockfileComparisonKey(await readPackageLock(cwd))) {
|
|
1045
|
+
shouldUploadAfterInstall = true;
|
|
1046
|
+
}
|
|
1047
|
+
if (!skipStartupUploadForCliRestart &&
|
|
1048
|
+
(shouldUploadAfterInstall || uploadFirst)) {
|
|
1049
|
+
logger.info(`Uploading local files to branch '${activeDbfsBranchName}' on server before starting`);
|
|
1050
|
+
await tracer.startActiveSpan("uploadFirstOrPackageChanged", async (span) => {
|
|
1051
|
+
await syncService.uploadDirectory("cli:sdk");
|
|
1052
|
+
await syncService.uploadDirectoryNowIfNeeded("cli:sdk");
|
|
1053
|
+
span.end();
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
676
1056
|
}
|
|
677
1057
|
if (hasCliUpdated) {
|
|
678
|
-
//
|
|
679
|
-
// the next boot
|
|
680
|
-
|
|
1058
|
+
// Restart must NOT be preempted by an app-install failure:
|
|
1059
|
+
// the next boot re-runs install with the new CLI. Join the
|
|
1060
|
+
// upgrade (it must succeed or we exit before restarting) and
|
|
1061
|
+
// SETTLE the install (don't observe its rejection — but wait
|
|
1062
|
+
// for npm to finish so the restart doesn't SIGKILL it
|
|
1063
|
+
// mid-rename and leave a half-written lockfile).
|
|
1064
|
+
await joinPackageUpgrade("before CLI restart");
|
|
1065
|
+
await settlePackageInstall("before CLI restart");
|
|
681
1066
|
try {
|
|
682
1067
|
logger.info("Releasing lock before restarting the dev server");
|
|
683
1068
|
await aiService?.removeIntegrationCache();
|
|
@@ -692,14 +1077,20 @@ export async function dev(options) {
|
|
|
692
1077
|
});
|
|
693
1078
|
}
|
|
694
1079
|
catch (error) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
await lockService?.shutdownAndExit();
|
|
1080
|
+
if (handleStartupError(error, devServerStatus, logger) === "degrade") {
|
|
1081
|
+
// app-install failure: do NOT exit — fall through to Vite startup below.
|
|
1082
|
+
// upload + CLI-restart were already skipped (the rejecting join threw first).
|
|
699
1083
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1084
|
+
else {
|
|
1085
|
+
logger.error("[dev-server] Startup failed during sync/lock/setup (exiting with code 1)", getErrorMeta(error));
|
|
1086
|
+
try {
|
|
1087
|
+
await aiService?.removeIntegrationCache();
|
|
1088
|
+
await lockService?.shutdownAndExit();
|
|
1089
|
+
}
|
|
1090
|
+
finally {
|
|
1091
|
+
// this is redundant, but it's here to make sure the lock service is shutdown and the process exits
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
703
1094
|
}
|
|
704
1095
|
}
|
|
705
1096
|
}
|
|
@@ -713,7 +1104,36 @@ export async function dev(options) {
|
|
|
713
1104
|
// cache embeds partial state that survives across reloads. Awaiting
|
|
714
1105
|
// here costs at most the install's remaining wall time — the upload
|
|
715
1106
|
// and CLI-restart joins above usually drain it first.
|
|
716
|
-
|
|
1107
|
+
// The "before upload" / "before CLI restart" joins above run inside the
|
|
1108
|
+
// sync/lock catch that degrades on `InitialInstallFailed`. But when the
|
|
1109
|
+
// install ran yet none of those joins fired (e.g.
|
|
1110
|
+
// `packageJsonRequiresInstall && !forcePackageInstall && !upload &&
|
|
1111
|
+
// !hasCliUpdated`), the install rejection first surfaces HERE — outside
|
|
1112
|
+
// that catch. Route it through the SAME origin gate so an app-install
|
|
1113
|
+
// failure still degrades (keep Vite up + record the error) instead of
|
|
1114
|
+
// escaping `dev()` to `process.exit(1)`. Non-install errors (including
|
|
1115
|
+
// any background upgrade rejection that escaped the prior joins) take
|
|
1116
|
+
// the same explicit exit path as the sync/lock catch above so the
|
|
1117
|
+
// process actually terminates (the startupSpan catch only rethrows,
|
|
1118
|
+
// and an unhandled rejection at that depth doesn't shut the lock
|
|
1119
|
+
// service down or guarantee `process.exit(1)`).
|
|
1120
|
+
try {
|
|
1121
|
+
await joinUpgradeThenInstall("before Vite startup");
|
|
1122
|
+
}
|
|
1123
|
+
catch (error) {
|
|
1124
|
+
if (handleStartupError(error, devServerStatus, logger) === "exit") {
|
|
1125
|
+
logger.error("[dev-server] Startup failed during pre-Vite install join (exiting with code 1)", getErrorMeta(error));
|
|
1126
|
+
try {
|
|
1127
|
+
await aiService?.removeIntegrationCache();
|
|
1128
|
+
await lockService?.shutdownAndExit();
|
|
1129
|
+
}
|
|
1130
|
+
finally {
|
|
1131
|
+
// Redundant with `shutdownAndExit`; here so a thrown shutdown
|
|
1132
|
+
// path can't leave the process hanging on a stuck handle.
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
717
1137
|
const activateRuntimeGitService = async () => {
|
|
718
1138
|
if (gitService) {
|
|
719
1139
|
const hasGit = await nodeFs.access(path.join(cwd, ".git")).then(() => true, () => false);
|
|
@@ -816,9 +1236,7 @@ export async function dev(options) {
|
|
|
816
1236
|
superblocksBaseUrl: tokenConfig.superblocksBaseUrl,
|
|
817
1237
|
existingServer: options.existingServer,
|
|
818
1238
|
warmActivationStart: options.warmActivationStart,
|
|
819
|
-
|
|
820
|
-
// CreateDevServerOptions directly so new required fields cause a
|
|
821
|
-
// compile error instead of silently passing undefined.
|
|
1239
|
+
devServerStatus,
|
|
822
1240
|
};
|
|
823
1241
|
const result = await createDevServer(createDevServerOptions);
|
|
824
1242
|
span.end();
|
|
@@ -833,13 +1251,16 @@ export async function dev(options) {
|
|
|
833
1251
|
logger.warn(`Error stopping auth hot-reload server: ${error}`);
|
|
834
1252
|
});
|
|
835
1253
|
}
|
|
836
|
-
// Drain any straggler install before tear-down. By this
|
|
837
|
-
// pre-Vite join has already run on the success path, so
|
|
838
|
-
//
|
|
1254
|
+
// Drain any straggler upgrade/install before tear-down. By this
|
|
1255
|
+
// point the pre-Vite join has already run on the success path, so
|
|
1256
|
+
// usually both promises are undefined and this is a no-op; if abort
|
|
839
1257
|
// races createDevServer (or fires during the inner sync block on
|
|
840
|
-
// some error path), draining here keeps the spawned npm
|
|
841
|
-
// being SIGKILL'd mid-rename. Errors only get logged — we are
|
|
842
|
-
// down anyway.
|
|
1258
|
+
// some error path), draining here keeps the spawned npm children
|
|
1259
|
+
// from being SIGKILL'd mid-rename. Errors only get logged — we are
|
|
1260
|
+
// tearing down anyway.
|
|
1261
|
+
joinPackageUpgrade("during abort").catch((error) => {
|
|
1262
|
+
logger.warn("Error draining background upgrade during abort", getErrorMeta(error));
|
|
1263
|
+
});
|
|
843
1264
|
joinPackageInstall("during abort").catch((error) => {
|
|
844
1265
|
logger.warn("Error draining background install during abort", getErrorMeta(error));
|
|
845
1266
|
});
|