@microwiseai/snapshot 0.8.0 → 0.9.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/dist/build-info.json +1 -0
- package/dist/commands/access.d.ts +25 -9
- package/dist/commands/access.d.ts.map +1 -1
- package/dist/commands/access.js +101 -47
- package/dist/commands/access.js.map +1 -1
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +37 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/install.d.ts +54 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +480 -65
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/publish.d.ts +10 -0
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +94 -12
- package/dist/commands/publish.js.map +1 -1
- package/dist/index.js +45 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/access-index.d.ts +81 -0
- package/dist/lib/access-index.d.ts.map +1 -0
- package/dist/lib/access-index.js +175 -0
- package/dist/lib/access-index.js.map +1 -0
- package/dist/lib/access-sync.d.ts +66 -17
- package/dist/lib/access-sync.d.ts.map +1 -1
- package/dist/lib/access-sync.js +391 -217
- package/dist/lib/access-sync.js.map +1 -1
- package/dist/lib/extends-access.d.ts +34 -0
- package/dist/lib/extends-access.d.ts.map +1 -0
- package/dist/lib/extends-access.js +82 -0
- package/dist/lib/extends-access.js.map +1 -0
- package/dist/lib/gitlab-publisher.d.ts +70 -0
- package/dist/lib/gitlab-publisher.d.ts.map +1 -0
- package/dist/lib/gitlab-publisher.js +165 -0
- package/dist/lib/gitlab-publisher.js.map +1 -0
- package/dist/lib/gitlab.d.ts +4 -0
- package/dist/lib/gitlab.d.ts.map +1 -1
- package/dist/lib/gitlab.js +1 -1
- package/dist/lib/gitlab.js.map +1 -1
- package/dist/lib/grant.d.ts +157 -0
- package/dist/lib/grant.d.ts.map +1 -0
- package/dist/lib/grant.js +352 -0
- package/dist/lib/grant.js.map +1 -0
- package/dist/lib/install-verify.d.ts +16 -2
- package/dist/lib/install-verify.d.ts.map +1 -1
- package/dist/lib/install-verify.js +24 -3
- package/dist/lib/install-verify.js.map +1 -1
- package/dist/lib/ledger.d.ts +13 -0
- package/dist/lib/ledger.d.ts.map +1 -1
- package/dist/lib/ledger.js.map +1 -1
- package/dist/lib/prerequisites.d.ts.map +1 -1
- package/dist/lib/prerequisites.js +4 -1
- package/dist/lib/prerequisites.js.map +1 -1
- package/dist/lib/proxy-auth-globalconfig.d.ts +55 -0
- package/dist/lib/proxy-auth-globalconfig.d.ts.map +1 -0
- package/dist/lib/proxy-auth-globalconfig.js +110 -0
- package/dist/lib/proxy-auth-globalconfig.js.map +1 -0
- package/dist/lib/semver-range.d.ts +32 -0
- package/dist/lib/semver-range.d.ts.map +1 -0
- package/dist/lib/semver-range.js +243 -0
- package/dist/lib/semver-range.js.map +1 -0
- package/dist/lib/session.d.ts +5 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/session.js +9 -0
- package/dist/lib/session.js.map +1 -1
- package/dist/lib/skit-adapter.d.ts.map +1 -1
- package/dist/lib/skit-adapter.js +26 -3
- package/dist/lib/skit-adapter.js.map +1 -1
- package/dist/lib/timing.d.ts +7 -0
- package/dist/lib/timing.d.ts.map +1 -0
- package/dist/lib/timing.js +14 -0
- package/dist/lib/timing.js.map +1 -0
- package/dist/lib/types.d.ts +3 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.d.ts +25 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.js +55 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.d.ts +52 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.js +62 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.d.ts +30 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.js +39 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.d.ts +35 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.js +31 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.d.ts +42 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.js +36 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.d.ts +11 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.js +46 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.d.ts +11 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.js +81 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.d.ts +29 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.d.ts.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.js +6 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.js.map +1 -0
- package/node_modules/@microwiseai/snapshot-downgrade-guard/package.json +55 -0
- package/package.json +5 -2
- package/dist/lib/transitive-resolver.d.ts +0 -89
- package/dist/lib/transitive-resolver.d.ts.map +0 -1
- package/dist/lib/transitive-resolver.js +0 -447
- package/dist/lib/transitive-resolver.js.map +0 -1
package/dist/commands/install.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
2
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
3
3
|
import { writeFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
4
4
|
import { homedir, platform, arch } from 'os';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { fetchSnapshotFromRepo, snapshotNameToRepoName } from '../lib/gitlab.js';
|
|
7
7
|
import { saveCurrentSnapshot } from '../lib/snapshot.js';
|
|
8
|
-
import { getLicenseKey, getProxyAuthToken, REGISTRY_PROXY_URL, VENDOR_NAME, STORE_URL, PACKAGE_NAME, getIstCredentials, LICENSE_API_URL, verifyLicenseKey } from '../lib/config.js';
|
|
8
|
+
import { getLicenseKey, getProxyAuthToken, REGISTRY_PROXY_URL, VENDOR_NAME, STORE_URL, PACKAGE_NAME, getIstCredentials, isLoggedIn, LICENSE_API_URL, verifyLicenseKey } from '../lib/config.js';
|
|
9
9
|
import { resolveSnapshotDependencies } from '../lib/package-resolver.js';
|
|
10
|
-
// transitive-resolver
|
|
10
|
+
// (transitive-resolver deleted — npm resolves dependencies; see git history)
|
|
11
11
|
import { installSkitPackage } from '../lib/skit-adapter.js';
|
|
12
12
|
import { markLicenseChecked } from '@microwiseai/license';
|
|
13
|
-
import { authorizeSession, packagesToSpecs } from '../lib/session.js';
|
|
13
|
+
import { authorizeSession, packagesToSpecs, maskSessionId } from '../lib/session.js';
|
|
14
|
+
import { readGrantMode, readGrantTimeoutMs, requestSnapshotGrant, GrantState, GrantError, checkGrantCoverage, findRootMemberVersion, formatGrantLog, splitSnapshotSpec, resolveInstallVersion, } from '../lib/grant.js';
|
|
14
15
|
import { parallelInstall, recordToEntries } from '@microwiseai/snapshot-parallel';
|
|
16
|
+
import { detectDowngrades, resolveDowngradePolicy, readOneShotOverride, getMachineDowngradeMode, formatDowngradeReport, extractGlobalInstallSpecs, } from '@microwiseai/snapshot-downgrade-guard';
|
|
15
17
|
import { dedupWithReport, dedupInstallBlock, recordInstalledSpecs, recordReferences, computeReplacePlan, applyReplaceHistory, } from '@microwiseai/snapshot-dedup';
|
|
16
18
|
import { createInterface } from 'readline';
|
|
19
|
+
import { timingStart } from '../lib/timing.js';
|
|
17
20
|
import { StepRecorder, buildLedger, writeLedger, queryDependencyTree, } from '../lib/ledger.js';
|
|
18
21
|
import { coreFloorAssert, formatCoreFloorOffenders, DEFAULT_CORE_FLOOR_PKG, DEFAULT_CORE_FLOOR_MIN_VERSION, } from '../lib/core-floor.js';
|
|
19
22
|
import { verifyInstallMap, runConverge, readInstalledNpmVersion, formatVerificationFailure, } from '../lib/install-verify.js';
|
|
@@ -198,27 +201,148 @@ function commandExists(cmd) {
|
|
|
198
201
|
// Captures the failure reason of the most recent runCommand() call so callers
|
|
199
202
|
// can surface it (debugging visibility). Undefined when the last call succeeded.
|
|
200
203
|
let lastCommandError;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
// ============================================================
|
|
205
|
+
// Downgrade guard — npm-exec choke point (defense in depth)
|
|
206
|
+
// ============================================================
|
|
207
|
+
// EVERY global install funnels through runCommand / runCommandSync (the spawn
|
|
208
|
+
// wrappers) — including installCommand's batches, a direct installSnapshot()
|
|
209
|
+
// library call, extends recursion, and parallelInstall's per-spec callback.
|
|
210
|
+
// Detecting downgrades HERE, at that single execution boundary, means NO entry
|
|
211
|
+
// point can bypass the guard (closes the lib.ts public-export bypass). This is a
|
|
212
|
+
// SECOND layer; installCommand's earlier pre-install guard stays as-is.
|
|
213
|
+
//
|
|
214
|
+
// Policy threading: installCommand resolves the mode once and publishes it via
|
|
215
|
+
// setActiveDowngradeMode(); this boundary reuses it (no recompute, no duplicate
|
|
216
|
+
// report). With no upstream context (direct library call), the boundary
|
|
217
|
+
// self-resolves from env (SNAPSHOT_ALLOW_LOCAL) + machine config, defaulting to
|
|
218
|
+
// the safe 'refuse'.
|
|
219
|
+
export class DowngradeRefusedError extends Error {
|
|
220
|
+
constructor(message) {
|
|
221
|
+
super(message);
|
|
222
|
+
this.name = 'DowngradeRefusedError';
|
|
208
223
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
}
|
|
225
|
+
let activeDowngradeMode;
|
|
226
|
+
/**
|
|
227
|
+
* Publish (or clear) the downgrade policy resolved upstream by installCommand so
|
|
228
|
+
* the exec boundary reuses it. Exported as a test seam.
|
|
229
|
+
*/
|
|
230
|
+
export function setActiveDowngradeMode(mode) {
|
|
231
|
+
activeDowngradeMode = mode;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Enforce the downgrade policy for a global-install command about to execute.
|
|
235
|
+
* No-op unless the command is a global install carrying versioned specs AND at
|
|
236
|
+
* least one spec would downgrade an already-installed global package. On a
|
|
237
|
+
* 'refuse' verdict it throws DowngradeRefusedError BEFORE spawn (zero partial
|
|
238
|
+
* install); 'warn' logs and proceeds; 'allow' proceeds silently. Pure detection
|
|
239
|
+
* is delegated to @microwiseai/snapshot-downgrade-guard.
|
|
240
|
+
*/
|
|
241
|
+
function assertNoRefusedDowngrade(command) {
|
|
242
|
+
const specs = extractGlobalInstallSpecs(command);
|
|
243
|
+
if (Object.keys(specs).length === 0)
|
|
244
|
+
return;
|
|
245
|
+
const { wouldDowngrade, offenders } = detectDowngrades(specs, {
|
|
246
|
+
readVersion: readInstalledNpmVersion,
|
|
247
|
+
});
|
|
248
|
+
if (!wouldDowngrade)
|
|
249
|
+
return;
|
|
250
|
+
// Reuse the upstream-resolved mode when present (installCommand path); else
|
|
251
|
+
// self-resolve for the context-free direct library-call path.
|
|
252
|
+
const threaded = activeDowngradeMode !== undefined;
|
|
253
|
+
const mode = threaded
|
|
254
|
+
? activeDowngradeMode
|
|
255
|
+
: resolveDowngradePolicy({
|
|
256
|
+
override: readOneShotOverride({ env: process.env }),
|
|
257
|
+
machine: getMachineDowngradeMode(),
|
|
258
|
+
// snapshot.json policy is not available at this boundary; the default
|
|
259
|
+
// 'refuse' keeps the direct-call path on the safe side.
|
|
260
|
+
});
|
|
261
|
+
if (mode === 'refuse') {
|
|
262
|
+
// Only the self-resolved (direct-call) path reports here — the installCommand
|
|
263
|
+
// path already printed upstream and cannot reach a refuse at this boundary
|
|
264
|
+
// (it exits during pre-install), so this avoids duplicate output.
|
|
265
|
+
if (!threaded) {
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(chalk.red(formatDowngradeReport(offenders, mode)));
|
|
219
268
|
}
|
|
269
|
+
throw new DowngradeRefusedError(formatDowngradeReport(offenders, mode));
|
|
270
|
+
}
|
|
271
|
+
if (mode === 'warn' && !threaded) {
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(chalk.yellow(formatDowngradeReport(offenders, mode)));
|
|
274
|
+
}
|
|
275
|
+
// mode === 'allow' → proceed.
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Run a command with FULLY LIVE output (spawn, async).
|
|
279
|
+
*
|
|
280
|
+
* Why spawn+async (not execSync): npm writes its progress bar to STDERR, and the
|
|
281
|
+
* previous `execSync(stdio:['inherit','inherit','pipe'])` piped stderr — which
|
|
282
|
+
* (a) hid npm's live progress (looked "frozen" for tens of seconds) and
|
|
283
|
+
* (b) blocked the event loop so no JS spinner could ever turn. Inheriting all
|
|
284
|
+
* three streams lets npm's own progress render live to the terminal while the
|
|
285
|
+
* event loop stays free. See install-speed-visibility work (B).
|
|
286
|
+
*
|
|
287
|
+
* On failure we can no longer capture the stderr text (it went straight to the
|
|
288
|
+
* terminal, so the user already SAW it) — `lastCommandError` carries the exit
|
|
289
|
+
* code instead.
|
|
290
|
+
*/
|
|
291
|
+
// Exported as a test seam (unit-tested in install.runcommand.test.ts).
|
|
292
|
+
export function runCommand(command) {
|
|
293
|
+
// Depth-defense: abort BEFORE spawn if this global install would downgrade and
|
|
294
|
+
// policy is refuse. Throws DowngradeRefusedError (no partial install).
|
|
295
|
+
assertNoRefusedDowngrade(command);
|
|
296
|
+
lastCommandError = undefined;
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
// shell:true so the plain command string (e.g. "npm install -g a@1 b@2")
|
|
299
|
+
// is parsed exactly as it was under execSync.
|
|
300
|
+
const child = spawn(command, { stdio: 'inherit', shell: true });
|
|
301
|
+
child.on('error', (err) => {
|
|
302
|
+
lastCommandError = `spawn error — ${err.message}`;
|
|
303
|
+
console.log(chalk.red(` Command failed to start: ${command}`));
|
|
304
|
+
console.log(chalk.red(err.message));
|
|
305
|
+
resolve(false);
|
|
306
|
+
});
|
|
307
|
+
child.on('close', (code) => {
|
|
308
|
+
if (code === 0) {
|
|
309
|
+
resolve(true);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
lastCommandError = `exit code ${code ?? 'unknown'}`;
|
|
313
|
+
console.log(chalk.red(` Command failed (exit code ${code ?? 'unknown'}): ${command}`));
|
|
314
|
+
resolve(false);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Synchronous streaming variant — used ONLY by the converge re-pin callback,
|
|
320
|
+
* which is invoked from the pure/sync `runConverge` (lib/install-verify.ts).
|
|
321
|
+
* Keeping this sync avoids making `runConverge` async (which would ripple into
|
|
322
|
+
* its unit-test suite and break its pure-function contract). `spawnSync` with
|
|
323
|
+
* inherited stdio still streams npm's live progress to the terminal (the child
|
|
324
|
+
* writes directly to the inherited fd), so the converge phase no longer looks
|
|
325
|
+
* frozen either — it only blocks the JS event loop for its (batched, rare)
|
|
326
|
+
* duration.
|
|
327
|
+
*/
|
|
328
|
+
// Exported as a test seam (unit-tested in install.runcommand.test.ts).
|
|
329
|
+
export function runCommandSync(command) {
|
|
330
|
+
// Depth-defense: abort BEFORE spawn if this global install would downgrade and
|
|
331
|
+
// policy is refuse. Throws DowngradeRefusedError (no partial install).
|
|
332
|
+
assertNoRefusedDowngrade(command);
|
|
333
|
+
lastCommandError = undefined;
|
|
334
|
+
const res = spawnSync(command, { stdio: 'inherit', shell: true });
|
|
335
|
+
if (res.error) {
|
|
336
|
+
lastCommandError = `spawn error — ${res.error.message}`;
|
|
337
|
+
console.log(chalk.red(` Command failed to start: ${command}`));
|
|
338
|
+
console.log(chalk.red(res.error.message));
|
|
220
339
|
return false;
|
|
221
340
|
}
|
|
341
|
+
if (res.status === 0)
|
|
342
|
+
return true;
|
|
343
|
+
lastCommandError = `exit code ${res.status ?? 'unknown'}`;
|
|
344
|
+
console.log(chalk.red(` Command failed (exit code ${res.status ?? 'unknown'}): ${command}`));
|
|
345
|
+
return false;
|
|
222
346
|
}
|
|
223
347
|
/**
|
|
224
348
|
* Check if a skit package is already installed with the specified version
|
|
@@ -264,8 +388,23 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId, re
|
|
|
264
388
|
const total = packageEntries.length;
|
|
265
389
|
console.log('');
|
|
266
390
|
console.log(chalk.yellow(`${indent}=== ${snapshotName}: ${total} skits ===`));
|
|
267
|
-
// Prepare install options with sessionId if available
|
|
268
|
-
|
|
391
|
+
// Prepare install options with sessionId if available. Forward onProgress so
|
|
392
|
+
// the skit lib's live install messages reach the terminal (skit-adapter.ts
|
|
393
|
+
// already forwards options.onProgress into lib.installSkit) — otherwise each
|
|
394
|
+
// skit install is silent until it completes.
|
|
395
|
+
// grant-unify P1: a function source is read PER KIT, right before the skit
|
|
396
|
+
// call, so a re-issued grant token is picked up (grant mode passes the istsg_
|
|
397
|
+
// token as the skit session → X-Session-Id on the kit tarball download).
|
|
398
|
+
// A plain string (legacy authorizeSession id) behaves exactly as before.
|
|
399
|
+
const skitOptionsFor = () => {
|
|
400
|
+
const sid = typeof sessionId === 'function' ? sessionId() : sessionId;
|
|
401
|
+
return {
|
|
402
|
+
...(sid ? { sessionId: sid } : {}),
|
|
403
|
+
onProgress: (message) => {
|
|
404
|
+
console.log(chalk.gray(`${indent} ${message}`));
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
};
|
|
269
408
|
// Install packages in parallel using snapshot-parallel
|
|
270
409
|
const entries = recordToEntries(packages);
|
|
271
410
|
const parallelResult = await parallelInstall(entries, async (entry) => {
|
|
@@ -273,7 +412,11 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId, re
|
|
|
273
412
|
if (isSkitInstalled(entry.name, entry.version)) {
|
|
274
413
|
return { success: true, skipped: true };
|
|
275
414
|
}
|
|
276
|
-
|
|
415
|
+
// Per-skit start log so a slow single install doesn't look frozen.
|
|
416
|
+
console.log(chalk.gray(`${indent} installing ${entry.name}@${entry.version}…`));
|
|
417
|
+
const endKit = timingStart(`kit ${entry.name}@${entry.version}`);
|
|
418
|
+
const installResult = await installSkitPackage(entry.name, entry.version, skitOptionsFor());
|
|
419
|
+
endKit();
|
|
277
420
|
return {
|
|
278
421
|
success: installResult.success,
|
|
279
422
|
error: installResult.error,
|
|
@@ -333,7 +476,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
333
476
|
if (!commandExists(cmdName)) {
|
|
334
477
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
335
478
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
336
|
-
if (runCommand(installerConfig.hint)) {
|
|
479
|
+
if (await runCommand(installerConfig.hint)) {
|
|
337
480
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
338
481
|
}
|
|
339
482
|
else {
|
|
@@ -353,6 +496,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
353
496
|
if (isNpmInstaller) {
|
|
354
497
|
// Batch install: collect all not-yet-installed packages and run a single npm install -g
|
|
355
498
|
const toInstall = [];
|
|
499
|
+
const endProbe = timingStart(`layer already-installed probes (${entries.length})`);
|
|
356
500
|
for (const entry of entries) {
|
|
357
501
|
if (isNpmGlobalInstalled(entry.name, entry.version)) {
|
|
358
502
|
console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
|
|
@@ -362,11 +506,12 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
362
506
|
toInstall.push(entry);
|
|
363
507
|
}
|
|
364
508
|
}
|
|
509
|
+
endProbe();
|
|
365
510
|
if (toInstall.length > 0) {
|
|
366
511
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
367
512
|
const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
|
|
368
513
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
369
|
-
const success = runCommand(batchCommand);
|
|
514
|
+
const success = await runCommand(batchCommand);
|
|
370
515
|
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
371
516
|
recorder?.record({
|
|
372
517
|
phase: 'layer-batch',
|
|
@@ -389,7 +534,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
389
534
|
for (const entry of toInstall) {
|
|
390
535
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
391
536
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
392
|
-
const ok = runCommand(command);
|
|
537
|
+
const ok = await runCommand(command);
|
|
393
538
|
recorder?.record({
|
|
394
539
|
phase: 'layer-fallback',
|
|
395
540
|
installer: installerName,
|
|
@@ -410,14 +555,18 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
410
555
|
}
|
|
411
556
|
}
|
|
412
557
|
else {
|
|
413
|
-
// Non-npm installer:
|
|
558
|
+
// Non-npm installer: run SERIALLY (concurrency 1). runCommand now spawns with
|
|
559
|
+
// stdio:'inherit', so concurrent children would interleave output on the same
|
|
560
|
+
// terminal AND race the module-global lastCommandError (mis-attributing the
|
|
561
|
+
// failure reason). Before the B change this path used sync execSync and was
|
|
562
|
+
// therefore effectively serial; concurrency:1 restores that (zero regression).
|
|
414
563
|
const parallelResult = await parallelInstall(entries, async (entry) => {
|
|
415
564
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
416
565
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
417
|
-
const success = runCommand(command);
|
|
566
|
+
const success = await runCommand(command);
|
|
418
567
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
419
568
|
}, {
|
|
420
|
-
concurrency:
|
|
569
|
+
concurrency: 1,
|
|
421
570
|
onProgress: (event) => {
|
|
422
571
|
const { result: r, current, total: t } = event;
|
|
423
572
|
const progress = `[${current}/${t}]`;
|
|
@@ -463,6 +612,32 @@ function applySnapshotOverrides(snapshot, overrides) {
|
|
|
463
612
|
}
|
|
464
613
|
return next;
|
|
465
614
|
}
|
|
615
|
+
// ============================================================
|
|
616
|
+
// Grant transport token (grant-unify P1, SNAPSHOT_GRANT_MODE)
|
|
617
|
+
// ============================================================
|
|
618
|
+
// When a snapshot grant is active, EVERY registry-proxy snapshot fetch (incl.
|
|
619
|
+
// extends bases) carries the istsg_ grant token explicitly — it takes precedence
|
|
620
|
+
// over the license key / IST token resolution inside fetchSnapshotFromRepo.
|
|
621
|
+
// null (always, in SNAPSHOT_GRANT_MODE=off) → the legacy call shape is kept.
|
|
622
|
+
let activeGrantToken = null;
|
|
623
|
+
/** Fetch an extends base snapshot (grant token when a grant is active). */
|
|
624
|
+
function fetchBaseSnapshot(spec) {
|
|
625
|
+
return activeGrantToken ? fetchSnapshotFromRepo(spec, activeGrantToken) : fetchSnapshotFromRepo(spec);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Collect the extends chain of a snapshot as exact {name, version} specs (the
|
|
629
|
+
* ancestor generics). Grant mode only — used for the closure coverage check.
|
|
630
|
+
*/
|
|
631
|
+
async function collectExtendsChain(snapshot) {
|
|
632
|
+
const chain = [];
|
|
633
|
+
let cur = snapshot;
|
|
634
|
+
for (let depth = 0; cur.extends && depth < 16; depth++) {
|
|
635
|
+
const base = await fetchBaseSnapshot(cur.extends);
|
|
636
|
+
chain.push({ name: base.name, version: base.version });
|
|
637
|
+
cur = base;
|
|
638
|
+
}
|
|
639
|
+
return chain;
|
|
640
|
+
}
|
|
466
641
|
/**
|
|
467
642
|
* FB — the subset of pin overrides that actually apply to this install (i.e. the
|
|
468
643
|
* overridden package IS declared in the effective npm map). These are the
|
|
@@ -502,7 +677,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
502
677
|
console.log('');
|
|
503
678
|
console.log(chalk.cyan(`${indent}► Extends: ${snapshot.extends}`));
|
|
504
679
|
try {
|
|
505
|
-
const baseSnapshot = await
|
|
680
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
506
681
|
console.log(chalk.gray(`${indent} Installing base snapshot...`));
|
|
507
682
|
const baseResult = await installSnapshot(baseSnapshot, depth + 1, installers, options);
|
|
508
683
|
result.installed.push(...baseResult.result.installed);
|
|
@@ -589,21 +764,22 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
589
764
|
const resolved = await resolveSnapshotDependencies(snapshot, { skipRegistry: true });
|
|
590
765
|
// skit 패키지 설치 (skit-adapter 사용: lib 우선, CLI fallback)
|
|
591
766
|
if (Object.keys(resolved.skit).length > 0) {
|
|
592
|
-
|
|
767
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
768
|
+
const skitResult = await installSkitPackages(resolved.skit, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
593
769
|
result.installed.push(...skitResult.installed);
|
|
594
770
|
result.failed.push(...skitResult.failed);
|
|
595
771
|
}
|
|
596
772
|
// npm 패키지 설치 (proxy 사용 시 npm-proxy, 아니면 glpkg)
|
|
597
773
|
if (Object.keys(resolved.npm).length > 0) {
|
|
598
774
|
const npmInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
599
|
-
const npmResult = await installPackagesByType(resolved.npm, npmInstaller, installers, snapshot.name, indent, 'npm packages', options.sessionId, options.recorder);
|
|
775
|
+
const npmResult = await installPackagesByType(resolved.npm, npmInstaller, installers, snapshot.name, indent, 'npm packages', (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
600
776
|
result.installed.push(...npmResult.installed);
|
|
601
777
|
result.failed.push(...npmResult.failed);
|
|
602
778
|
}
|
|
603
779
|
// generic 패키지 설치 (glpkg)
|
|
604
780
|
if (Object.keys(resolved.generic).length > 0) {
|
|
605
781
|
const genericInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
606
|
-
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', options.sessionId, options.recorder);
|
|
782
|
+
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
607
783
|
result.installed.push(...genericResult.installed);
|
|
608
784
|
result.failed.push(...genericResult.failed);
|
|
609
785
|
}
|
|
@@ -620,7 +796,8 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
620
796
|
continue;
|
|
621
797
|
// Skit packages: use skit-adapter (lib 우선, CLI fallback)
|
|
622
798
|
if (installerName === 'skit') {
|
|
623
|
-
|
|
799
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
800
|
+
const skitResult = await installSkitPackages(packages, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
624
801
|
result.installed.push(...skitResult.installed);
|
|
625
802
|
result.failed.push(...skitResult.failed);
|
|
626
803
|
continue;
|
|
@@ -639,7 +816,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
639
816
|
if (!commandExists(cmdName)) {
|
|
640
817
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
641
818
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
642
|
-
if (runCommand(installerConfig.hint)) {
|
|
819
|
+
if (await runCommand(installerConfig.hint)) {
|
|
643
820
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
644
821
|
}
|
|
645
822
|
else {
|
|
@@ -660,6 +837,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
660
837
|
if (isNpmType) {
|
|
661
838
|
// Batch install: collect all not-yet-installed packages and run a single npm install -g
|
|
662
839
|
const toInstall = [];
|
|
840
|
+
const endProbe = timingStart(`layer already-installed probes (${installEntries.length})`);
|
|
663
841
|
for (const entry of installEntries) {
|
|
664
842
|
if (isNpmGlobalInstalled(entry.name, entry.version)) {
|
|
665
843
|
console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
|
|
@@ -669,11 +847,12 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
669
847
|
toInstall.push(entry);
|
|
670
848
|
}
|
|
671
849
|
}
|
|
850
|
+
endProbe();
|
|
672
851
|
if (toInstall.length > 0) {
|
|
673
852
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
674
853
|
const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
|
|
675
854
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
676
|
-
const success = runCommand(batchCommand);
|
|
855
|
+
const success = await runCommand(batchCommand);
|
|
677
856
|
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
678
857
|
options.recorder?.record({
|
|
679
858
|
phase: 'layer-batch',
|
|
@@ -696,7 +875,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
696
875
|
for (const entry of toInstall) {
|
|
697
876
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
698
877
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
699
|
-
const ok = runCommand(command);
|
|
878
|
+
const ok = await runCommand(command);
|
|
700
879
|
options.recorder?.record({
|
|
701
880
|
phase: 'layer-fallback',
|
|
702
881
|
installer: installerName,
|
|
@@ -717,14 +896,16 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
717
896
|
}
|
|
718
897
|
}
|
|
719
898
|
else {
|
|
720
|
-
// Non-npm installer:
|
|
899
|
+
// Non-npm installer: run SERIALLY (concurrency 1) — see rationale above:
|
|
900
|
+
// spawn(stdio:'inherit') children would interleave terminal output and
|
|
901
|
+
// race the global lastCommandError. Restores the pre-B serial behavior.
|
|
721
902
|
const parallelResult = await parallelInstall(installEntries, async (entry) => {
|
|
722
903
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
723
904
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
724
|
-
const success = runCommand(command);
|
|
905
|
+
const success = await runCommand(command);
|
|
725
906
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
726
907
|
}, {
|
|
727
|
-
concurrency:
|
|
908
|
+
concurrency: 1,
|
|
728
909
|
onProgress: (event) => {
|
|
729
910
|
const { result: r, current, total: t } = event;
|
|
730
911
|
const progress = `[${current}/${t}]`;
|
|
@@ -759,7 +940,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
759
940
|
if (!commandExists(cmdName)) {
|
|
760
941
|
console.log(chalk.yellow(`${indent}glpkg not found. Attempting auto-install...`));
|
|
761
942
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
762
|
-
if (runCommand(installerConfig.hint)) {
|
|
943
|
+
if (await runCommand(installerConfig.hint)) {
|
|
763
944
|
console.log(chalk.green(`${indent} ✓ glpkg installed successfully`));
|
|
764
945
|
}
|
|
765
946
|
else {
|
|
@@ -774,13 +955,15 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
774
955
|
else {
|
|
775
956
|
const legacyEntries = recordToEntries(snapshot.packages);
|
|
776
957
|
const legacyCfg = installerConfig; // capture for closure
|
|
958
|
+
// Legacy glpkg packages: run SERIALLY (concurrency 1) — see rationale above
|
|
959
|
+
// (spawn:'inherit' output interleave + global lastCommandError race).
|
|
777
960
|
const parallelResult = await parallelInstall(legacyEntries, async (entry) => {
|
|
778
961
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
779
962
|
const command = legacyCfg.command.replace('{pkg}', pkgSpec);
|
|
780
|
-
const success = runCommand(command);
|
|
963
|
+
const success = await runCommand(command);
|
|
781
964
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
782
965
|
}, {
|
|
783
|
-
concurrency:
|
|
966
|
+
concurrency: 1,
|
|
784
967
|
onProgress: (event) => {
|
|
785
968
|
const { result: r, current, total: t } = event;
|
|
786
969
|
const progress = `[${current}/${t}]`;
|
|
@@ -806,7 +989,8 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
806
989
|
}
|
|
807
990
|
// Legacy: skits (use skit-adapter: lib 우선, CLI fallback)
|
|
808
991
|
if (snapshot.skits && Object.keys(snapshot.skits).length > 0) {
|
|
809
|
-
|
|
992
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
993
|
+
const skitResult = await installSkitPackages(snapshot.skits, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
810
994
|
result.installed.push(...skitResult.installed);
|
|
811
995
|
result.failed.push(...skitResult.failed);
|
|
812
996
|
}
|
|
@@ -846,7 +1030,7 @@ async function collectAllNpmPackages(snapshot, useProxy = true) {
|
|
|
846
1030
|
// Recurse into extends chain
|
|
847
1031
|
if (snapshot.extends) {
|
|
848
1032
|
try {
|
|
849
|
-
const baseSnapshot = await
|
|
1033
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
850
1034
|
const baseNpmPackages = await collectAllNpmPackages(baseSnapshot, useProxy);
|
|
851
1035
|
// Base packages: don't override current snapshot's versions
|
|
852
1036
|
for (const [name, version] of Object.entries(baseNpmPackages)) {
|
|
@@ -883,7 +1067,7 @@ export async function collectAllPackages(snapshot) {
|
|
|
883
1067
|
// Recurse into extends chain
|
|
884
1068
|
if (snapshot.extends) {
|
|
885
1069
|
try {
|
|
886
|
-
const baseSnapshot = await
|
|
1070
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
887
1071
|
const basePackages = await collectAllPackages(baseSnapshot);
|
|
888
1072
|
Object.assign(allPackages, basePackages);
|
|
889
1073
|
}
|
|
@@ -996,7 +1180,7 @@ async function runReplaceMode(snapshotName, newSet, options, installers) {
|
|
|
996
1180
|
continue;
|
|
997
1181
|
}
|
|
998
1182
|
console.log(chalk.gray(` Removing ${item.name}...`));
|
|
999
|
-
if (runCommand(cmd)) {
|
|
1183
|
+
if (await runCommand(cmd)) {
|
|
1000
1184
|
console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
|
|
1001
1185
|
removed.push(item);
|
|
1002
1186
|
}
|
|
@@ -1057,6 +1241,9 @@ async function previewInstall(snapshot, options) {
|
|
|
1057
1241
|
console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
|
|
1058
1242
|
}
|
|
1059
1243
|
export async function installCommand(name, options = {}) {
|
|
1244
|
+
// Start from a clean downgrade-policy context; the pre-install guard publishes
|
|
1245
|
+
// the resolved mode below, and cleanup() clears it on every exit path.
|
|
1246
|
+
setActiveDowngradeMode(undefined);
|
|
1060
1247
|
console.log(chalk.blue.bold(`
|
|
1061
1248
|
╔══════════════════════════════════════════════════════════════╗
|
|
1062
1249
|
║ Snapshot Installer ║
|
|
@@ -1106,11 +1293,90 @@ export async function installCommand(name, options = {}) {
|
|
|
1106
1293
|
console.log(chalk.yellow('⚠') + ' No auth token — anonymous access');
|
|
1107
1294
|
}
|
|
1108
1295
|
const useProxy = true;
|
|
1296
|
+
// ========== Snapshot grant (grant-unify P1, SNAPSHOT_GRANT_MODE) ==========
|
|
1297
|
+
// off (default): nothing below runs (grant stays null) — legacy path.
|
|
1298
|
+
// prefer/require: request ONE istsg_ grant BEFORE fetching snapshot.json, since
|
|
1299
|
+
// the root generic itself is a closure member the proxy may gate. The grant is
|
|
1300
|
+
// always for an EXACT version: none/'latest' is resolved first with the proxy
|
|
1301
|
+
// rule (highest stable, prereleases ignored — same as the legacy path's
|
|
1302
|
+
// 'latest'); license-api's own 'latest' (= newest upload) is never used. prefer drops
|
|
1303
|
+
// the grant on any grant failure and runs the whole install on the legacy
|
|
1304
|
+
// path; require is fail-loud. dry-run + prefer skips the grant (zero side
|
|
1305
|
+
// effects); dry-run + require still needs it to fetch the snapshot.
|
|
1306
|
+
activeGrantToken = null;
|
|
1307
|
+
const grantMode = readGrantMode(process.env, (m) => console.log(chalk.yellow('⚠ ' + m)));
|
|
1308
|
+
const { name: snapshotBaseName, version: requestedVersion } = splitSnapshotSpec(name);
|
|
1309
|
+
let grant = null;
|
|
1310
|
+
let grantEnvActive = false;
|
|
1311
|
+
/** Grant failure policy: require → exit 1; prefer → warn, drop grant, legacy path. */
|
|
1312
|
+
const onGrantFailure = (err) => {
|
|
1313
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1314
|
+
if (grantMode === 'require') {
|
|
1315
|
+
console.log(chalk.red(`⛔ Snapshot grant failed (SNAPSHOT_GRANT_MODE=require): ${msg}`));
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
console.log(chalk.yellow(`⚠ Snapshot grant unavailable — continuing on the legacy path: ${msg}`));
|
|
1319
|
+
grant = null;
|
|
1320
|
+
activeGrantToken = null;
|
|
1321
|
+
};
|
|
1322
|
+
if (grantMode !== 'off' && !(options.dryRun && grantMode === 'prefer')) {
|
|
1323
|
+
const timeoutMs = readGrantTimeoutMs();
|
|
1324
|
+
let grantVersion = null;
|
|
1325
|
+
try {
|
|
1326
|
+
grantVersion = await resolveInstallVersion(snapshotBaseName, requestedVersion, {
|
|
1327
|
+
registryUrl: REGISTRY_PROXY_URL,
|
|
1328
|
+
timeoutMs,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
onGrantFailure(err);
|
|
1333
|
+
}
|
|
1334
|
+
if (grantVersion) {
|
|
1335
|
+
grant = new GrantState({ snapshot: snapshotBaseName, version: grantVersion, authToken }, {
|
|
1336
|
+
request: (req) => requestSnapshotGrant(req, { licenseApiUrl: LICENSE_API_URL, timeoutMs }),
|
|
1337
|
+
onToken: (token) => {
|
|
1338
|
+
activeGrantToken = token;
|
|
1339
|
+
if (grantEnvActive)
|
|
1340
|
+
process.env.IST_SESSION_TOKEN = token;
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
console.log(chalk.gray(`Requesting snapshot grant (${grantMode}) for ${snapshotBaseName}@${grantVersion}...`));
|
|
1344
|
+
try {
|
|
1345
|
+
const g = await grant.issue();
|
|
1346
|
+
// The closure root (when present) must be exactly the version we asked for.
|
|
1347
|
+
const rootVersion = findRootMemberVersion(g.members, snapshotBaseName);
|
|
1348
|
+
if (rootVersion && rootVersion !== grantVersion) {
|
|
1349
|
+
throw new GrantError('version-mismatch', `grant closure root ${snapshotBaseName}@${rootVersion} differs from the requested ${grantVersion}`);
|
|
1350
|
+
}
|
|
1351
|
+
console.log(chalk.green('✓') + ' ' + formatGrantLog(grantMode, g));
|
|
1352
|
+
}
|
|
1353
|
+
catch (err) {
|
|
1354
|
+
onGrantFailure(err);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
// ==========================================================================
|
|
1109
1359
|
// Fetch snapshot first — access check depends on snapshot.json content
|
|
1110
1360
|
console.log(chalk.gray('Fetching snapshot...'));
|
|
1111
1361
|
let snapshot;
|
|
1112
1362
|
try {
|
|
1113
|
-
|
|
1363
|
+
if (grant) {
|
|
1364
|
+
// Grant mode: fetch exactly the granted version with the grant token.
|
|
1365
|
+
const grantVersion = grant.version;
|
|
1366
|
+
try {
|
|
1367
|
+
snapshot = await fetchSnapshotFromRepo(`${snapshotBaseName}@${grantVersion}`, activeGrantToken);
|
|
1368
|
+
if (snapshot.version !== grantVersion) {
|
|
1369
|
+
throw new GrantError('version-mismatch', `snapshot ${snapshotBaseName}@${snapshot.version} does not match the granted version ${grantVersion}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
onGrantFailure(err);
|
|
1374
|
+
snapshot = await fetchSnapshotFromRepo(name, authToken);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
snapshot = await fetchSnapshotFromRepo(name, authToken);
|
|
1379
|
+
}
|
|
1114
1380
|
}
|
|
1115
1381
|
catch (error) {
|
|
1116
1382
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -1180,8 +1446,13 @@ export async function installCommand(name, options = {}) {
|
|
|
1180
1446
|
const isAuthenticatedAccess = accessField === 'authenticated';
|
|
1181
1447
|
const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
|
|
1182
1448
|
// --- IST Login Check ---
|
|
1183
|
-
|
|
1184
|
-
|
|
1449
|
+
// Single auth verdict: isLoggedIn() = the same credential SSOT that resolved
|
|
1450
|
+
// authToken above (env IST_SESSION_TOKEN or ~/.ist/credentials.json .token,
|
|
1451
|
+
// locally unexpired) and that the 'IST Credentials' prerequisite uses. The old
|
|
1452
|
+
// check read credentials.json .userId, so a token-authenticated install could
|
|
1453
|
+
// print "✓ Auth: IST token" AND "⚠ No IST login" in the same run.
|
|
1454
|
+
const istCredentials = getIstCredentials(); // telemetry identity only (userId/email)
|
|
1455
|
+
if (isLoggedIn()) {
|
|
1185
1456
|
console.log(chalk.green('✓') + ' IST login verified');
|
|
1186
1457
|
}
|
|
1187
1458
|
else if (isPublicAccess) {
|
|
@@ -1242,9 +1513,39 @@ export async function installCommand(name, options = {}) {
|
|
|
1242
1513
|
console.log(chalk.green('✓') + ` License verified for ${productId}`);
|
|
1243
1514
|
}
|
|
1244
1515
|
// =======================================================================
|
|
1516
|
+
// ========== Grant closure coverage (grant mode only) ==========
|
|
1517
|
+
// members[] is the ALLOWED set: every exact pin we will install (flattened
|
|
1518
|
+
// across the extends chain, --pin applied) plus the extends ancestors must be
|
|
1519
|
+
// a closure member, or the proxy would 401 mid-install. A --pin outside the
|
|
1520
|
+
// closure is refused up front (both modes, logged in or not). A manifest pin
|
|
1521
|
+
// outside it is a grant/index inconsistency: prefer → legacy path, require → exit.
|
|
1522
|
+
if (grant) {
|
|
1523
|
+
const g = grant.current;
|
|
1524
|
+
const expectedPins = applyPinOverrides(await collectAllPackages(snapshot), overrides);
|
|
1525
|
+
let ancestors = [];
|
|
1526
|
+
try {
|
|
1527
|
+
ancestors = await collectExtendsChain(snapshot);
|
|
1528
|
+
}
|
|
1529
|
+
catch (err) {
|
|
1530
|
+
onGrantFailure(err);
|
|
1531
|
+
}
|
|
1532
|
+
if (grant) {
|
|
1533
|
+
const coverage = checkGrantCoverage([...Object.entries(expectedPins).map(([n, v]) => ({ name: n, version: v })), ...ancestors], g.members);
|
|
1534
|
+
if (!coverage.ok) {
|
|
1535
|
+
const pinViolations = coverage.missing.filter((spec) => Object.entries(overrides).some(([n, v]) => spec === `${n}@${v}`));
|
|
1536
|
+
if (pinViolations.length > 0) {
|
|
1537
|
+
console.log(chalk.red(`⛔ --pin outside the snapshot grant closure: ${pinViolations.join(', ')}`));
|
|
1538
|
+
console.log(chalk.gray(' A pinned version must be part of this snapshot version\'s frozen closure.'));
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
onGrantFailure(new GrantError('invalid-response', `grant closure does not cover ${coverage.missing.length} manifest pin(s): ${coverage.missing.join(', ')}`));
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1245
1545
|
// ========== Authorize Session with Registry Proxy ==========
|
|
1246
1546
|
let sessionId;
|
|
1247
|
-
|
|
1547
|
+
// Grant mode: the istsg_ grant replaces authorizeSession (one transport token).
|
|
1548
|
+
if (authToken && !grant) {
|
|
1248
1549
|
// Collect all packages for session authorization
|
|
1249
1550
|
const sessionPackages = [];
|
|
1250
1551
|
// From snapshot.install
|
|
@@ -1268,7 +1569,7 @@ export async function installCommand(name, options = {}) {
|
|
|
1268
1569
|
console.log(chalk.gray(` Auth type: ${authType}`));
|
|
1269
1570
|
try {
|
|
1270
1571
|
sessionId = await authorizeSession(authToken, name, sessionPackages);
|
|
1271
|
-
console.log(chalk.green('✓') + ' Session authorized: ' + sessionId);
|
|
1572
|
+
console.log(chalk.green('✓') + ' Session authorized: ' + maskSessionId(sessionId));
|
|
1272
1573
|
}
|
|
1273
1574
|
catch (error) {
|
|
1274
1575
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -1286,7 +1587,15 @@ export async function installCommand(name, options = {}) {
|
|
|
1286
1587
|
if (useProxy) {
|
|
1287
1588
|
const scopes = extractScopes(allPackages);
|
|
1288
1589
|
if (scopes.length > 0) {
|
|
1289
|
-
if (
|
|
1590
|
+
if (grant) {
|
|
1591
|
+
// Grant mode: the istsg_ token is the proxy transport token for npm and
|
|
1592
|
+
// skit (kit npm deps read ${IST_SESSION_TOKEN}). Re-issues update it.
|
|
1593
|
+
process.env.IST_SESSION_TOKEN = grant.current.token;
|
|
1594
|
+
grantEnvActive = true;
|
|
1595
|
+
console.log(chalk.gray(`Configuring registry-proxy (snapshot grant) for scopes: ${scopes.join(', ')}`));
|
|
1596
|
+
npmrcBackup = configureNpmrcForProxy(scopes);
|
|
1597
|
+
}
|
|
1598
|
+
else if (authToken) {
|
|
1290
1599
|
// 환경변수에 설정 (npm이 .npmrc의 ${IST_SESSION_TOKEN}을 해석)
|
|
1291
1600
|
process.env.IST_SESSION_TOKEN = authToken;
|
|
1292
1601
|
console.log(chalk.gray(`Configuring registry-proxy for scopes: ${scopes.join(', ')}`));
|
|
@@ -1316,6 +1625,11 @@ export async function installCommand(name, options = {}) {
|
|
|
1316
1625
|
console.log(chalk.gray('Restored .npmrc'));
|
|
1317
1626
|
}
|
|
1318
1627
|
delete process.env.IST_SESSION_TOKEN;
|
|
1628
|
+
activeGrantToken = null;
|
|
1629
|
+
grantEnvActive = false;
|
|
1630
|
+
// Clear the threaded downgrade mode so it never leaks to a later install
|
|
1631
|
+
// (the exec boundary self-resolves again when no context is published).
|
|
1632
|
+
setActiveDowngradeMode(undefined);
|
|
1319
1633
|
};
|
|
1320
1634
|
const onSignal = (signal) => {
|
|
1321
1635
|
console.log(chalk.yellow(`\nReceived ${signal} — restoring .npmrc before exit...`));
|
|
@@ -1326,6 +1640,28 @@ export async function installCommand(name, options = {}) {
|
|
|
1326
1640
|
};
|
|
1327
1641
|
process.once('SIGINT', onSignal);
|
|
1328
1642
|
process.once('SIGTERM', onSignal);
|
|
1643
|
+
// grant-unify P1 — re-issue the snapshot grant before a long phase when its
|
|
1644
|
+
// remaining TTL is low (GrantState.ensureFresh). The new token reaches npm and
|
|
1645
|
+
// skit through IST_SESSION_TOKEN (onToken). A re-issue failure: require →
|
|
1646
|
+
// throw (fail-loud; callers inside the install try/finally clean up), prefer →
|
|
1647
|
+
// warn and keep the current token. No-op without an active grant (off mode).
|
|
1648
|
+
const refreshGrant = async (phase) => {
|
|
1649
|
+
if (!grant)
|
|
1650
|
+
return;
|
|
1651
|
+
const st = grant;
|
|
1652
|
+
try {
|
|
1653
|
+
if (await st.ensureFresh()) {
|
|
1654
|
+
console.log(chalk.gray(` ↻ Grant re-issued before ${phase}: ${formatGrantLog(grantMode, st.current)}`));
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
catch (err) {
|
|
1658
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1659
|
+
if (grantMode === 'require') {
|
|
1660
|
+
throw new Error(`Snapshot grant re-issue failed before ${phase}: ${msg}`);
|
|
1661
|
+
}
|
|
1662
|
+
console.log(chalk.yellow(` ⚠ Snapshot grant re-issue failed before ${phase} — keeping the current grant: ${msg}`));
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1329
1665
|
// Determine which installers to use
|
|
1330
1666
|
const effectiveInstallers = { ...DEFAULT_INSTALLERS };
|
|
1331
1667
|
if (useProxy) {
|
|
@@ -1351,11 +1687,56 @@ export async function installCommand(name, options = {}) {
|
|
|
1351
1687
|
console.log(chalk.yellow(`⚠ --pin ignored for package(s) not in this snapshot: ${unmatched.join(', ')}`));
|
|
1352
1688
|
}
|
|
1353
1689
|
const npmEntries = Object.entries(effectiveNpmMap);
|
|
1690
|
+
// ========== Downgrade guard (dev-isolation) ==========
|
|
1691
|
+
// A snapshot bundles OLDER stable versions. On a dev machine whose global
|
|
1692
|
+
// tooling is already NEWER, installing would `npm i -g {pkg}@{pinned}` and
|
|
1693
|
+
// DOWNGRADE it, breaking the environment. Detect at THIS boundary — the map is
|
|
1694
|
+
// now flattened across the whole extends chain (effectiveNpmMap), and NO npm
|
|
1695
|
+
// exec has happened yet (the pre-install batch is below). A clean Docker
|
|
1696
|
+
// sandbox has nothing installed globally → zero offenders → auto-pass, no
|
|
1697
|
+
// config needed. Policy precedence (highest wins): CLI --allow-downgrade / env
|
|
1698
|
+
// SNAPSHOT_ALLOW_LOCAL=1 → machine config → snapshot.policy.onDowngrade →
|
|
1699
|
+
// default 'refuse'. Side effects live in @microwiseai/snapshot-downgrade-guard
|
|
1700
|
+
// via injection (readVersion), keeping that package pure.
|
|
1701
|
+
{
|
|
1702
|
+
const { wouldDowngrade, offenders } = detectDowngrades(effectiveNpmMap, {
|
|
1703
|
+
readVersion: readInstalledNpmVersion,
|
|
1704
|
+
});
|
|
1705
|
+
// Resolve the policy ONCE with full context (incl. snapshot.json policy) and
|
|
1706
|
+
// publish it so the downstream npm-exec boundary (runCommand/runCommandSync)
|
|
1707
|
+
// REUSES it instead of recomputing — one decision for the whole install, and
|
|
1708
|
+
// the boundary won't re-report the same downgrade.
|
|
1709
|
+
const mode = resolveDowngradePolicy({
|
|
1710
|
+
override: readOneShotOverride({
|
|
1711
|
+
env: process.env,
|
|
1712
|
+
allowDowngradeFlag: options.allowDowngrade === true,
|
|
1713
|
+
}),
|
|
1714
|
+
machine: getMachineDowngradeMode(),
|
|
1715
|
+
snapshot: snapshot.policy?.onDowngrade,
|
|
1716
|
+
});
|
|
1717
|
+
setActiveDowngradeMode(mode);
|
|
1718
|
+
if (wouldDowngrade) {
|
|
1719
|
+
if (mode === 'refuse') {
|
|
1720
|
+
// Nothing installed yet — restore .npmrc / unset token, then fail-loud
|
|
1721
|
+
// with a non-zero exit and ZERO partial install.
|
|
1722
|
+
cleanup();
|
|
1723
|
+
console.log('');
|
|
1724
|
+
console.log(chalk.red(formatDowngradeReport(offenders, mode)));
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
else if (mode === 'warn') {
|
|
1728
|
+
console.log('');
|
|
1729
|
+
console.log(chalk.yellow(formatDowngradeReport(offenders, mode)));
|
|
1730
|
+
}
|
|
1731
|
+
// mode === 'allow' → proceed silently.
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1354
1734
|
if (npmEntries.length > 0) {
|
|
1355
1735
|
console.log('');
|
|
1356
1736
|
console.log(chalk.cyan(`► Pre-installing ${npmEntries.length} npm packages from entire extends chain...`));
|
|
1357
1737
|
// Filter out already-installed packages
|
|
1358
1738
|
const toInstall = [];
|
|
1739
|
+
const endProbe = timingStart(`pre-install already-installed probes (${npmEntries.length})`);
|
|
1359
1740
|
for (const [name, version] of npmEntries) {
|
|
1360
1741
|
if (isNpmGlobalInstalled(name, version)) {
|
|
1361
1742
|
console.log(` ${chalk.gray('skip')} ${name}@${version}${chalk.green(' (already installed)')}`);
|
|
@@ -1364,11 +1745,23 @@ export async function installCommand(name, options = {}) {
|
|
|
1364
1745
|
toInstall.push({ name, version });
|
|
1365
1746
|
}
|
|
1366
1747
|
}
|
|
1748
|
+
endProbe();
|
|
1367
1749
|
if (toInstall.length > 0) {
|
|
1368
1750
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
1369
1751
|
const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
|
|
1370
|
-
|
|
1371
|
-
|
|
1752
|
+
try {
|
|
1753
|
+
await refreshGrant('pre-install batch');
|
|
1754
|
+
}
|
|
1755
|
+
catch (err) {
|
|
1756
|
+
// Outside the install try/finally: restore .npmrc / token, then fail loud.
|
|
1757
|
+
cleanup();
|
|
1758
|
+
console.log(chalk.red(`⛔ ${err instanceof Error ? err.message : String(err)}`));
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
const endBatch = timingStart(`pre-install batch (${specs.length})`);
|
|
1762
|
+
console.log(chalk.cyan(` ► Installing ${specs.length} npm package(s) in one batch… (수십초 걸릴 수 있음, npm 진행이 아래에 표시됩니다)`));
|
|
1763
|
+
const batchSuccess = await runCommand(npmCommand);
|
|
1764
|
+
endBatch();
|
|
1372
1765
|
// S3 — record the up-front batch (the single step where the beta npm tree
|
|
1373
1766
|
// is actually resolved). 'fallback' means it failed and we retried below.
|
|
1374
1767
|
recorder.record({
|
|
@@ -1393,7 +1786,7 @@ export async function installCommand(name, options = {}) {
|
|
|
1393
1786
|
for (const entry of toInstall) {
|
|
1394
1787
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
1395
1788
|
const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
|
|
1396
|
-
const ok = runCommand(cmd);
|
|
1789
|
+
const ok = await runCommand(cmd);
|
|
1397
1790
|
recorder.record({
|
|
1398
1791
|
phase: 'pre-install-fallback',
|
|
1399
1792
|
installer: 'npm-proxy',
|
|
@@ -1430,33 +1823,44 @@ export async function installCommand(name, options = {}) {
|
|
|
1430
1823
|
try {
|
|
1431
1824
|
// Auto-detect 모드: snapshot.install이 없으면 자동 판별
|
|
1432
1825
|
const autoDetect = !snapshot.install;
|
|
1826
|
+
const endLayers = timingStart('installSnapshot (layers + kits)');
|
|
1433
1827
|
const installResult = await installSnapshot(snapshot, 0, effectiveInstallers, {
|
|
1434
1828
|
useProxy,
|
|
1435
1829
|
autoDetect,
|
|
1436
1830
|
sessionId,
|
|
1437
1831
|
recorder,
|
|
1438
1832
|
overrides,
|
|
1833
|
+
...(grant ? { beforePhase: refreshGrant, skitSessionId: () => activeGrantToken ?? undefined } : {}),
|
|
1439
1834
|
});
|
|
1440
1835
|
result = installResult.result;
|
|
1836
|
+
endLayers();
|
|
1441
1837
|
// ===== S1/FB — verify (and optionally converge) BEFORE cleanup =====
|
|
1442
1838
|
// SSOT enforcement: the manifest pins (with FB overrides applied) are the
|
|
1443
1839
|
// truth. Check the disk actually matches. This MUST run before the finally
|
|
1444
1840
|
// restores .npmrc, because re-pinning via `npm install -g` needs the
|
|
1445
1841
|
// registry-proxy config + token still in place.
|
|
1446
1842
|
if (Object.keys(effectiveNpmMap).length > 0) {
|
|
1447
|
-
// Re-pin
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1843
|
+
// Re-pin ALL diverged packages in a round with a SINGLE `npm install -g`
|
|
1844
|
+
// (batched) and record it as one ledger step carrying every spec. This
|
|
1845
|
+
// collapses N sequential global installs per round into one npm spawn.
|
|
1846
|
+
const repinMany = (phase) => (pkgs) => {
|
|
1847
|
+
if (pkgs.length === 0)
|
|
1848
|
+
return;
|
|
1849
|
+
const specs = pkgs.map((p) => `${p.name}@${p.version}`);
|
|
1850
|
+
const cmd = `npm install -g ${specs.join(' ')}`;
|
|
1851
|
+
console.log(chalk.gray(` ► Re-pinning ${specs.length} package(s) in one batch (npm install -g … — 수십초 걸릴 수 있음)`));
|
|
1852
|
+
// Sync streaming: invoked from the pure/sync runConverge callback.
|
|
1853
|
+
const ok = runCommandSync(cmd);
|
|
1854
|
+
recorder.record({ phase, installer: 'npm-proxy', command: cmd, specs, result: ok ? 'ok' : 'failed' });
|
|
1453
1855
|
};
|
|
1856
|
+
await refreshGrant('verify/converge');
|
|
1857
|
+
const endVerify = timingStart('verify/converge');
|
|
1454
1858
|
if (converge) {
|
|
1455
1859
|
// Default: re-pin EVERY package that diverged from its pin (self-healing).
|
|
1456
1860
|
console.log('');
|
|
1457
1861
|
console.log(chalk.cyan('► Verifying & converging install to manifest pins...'));
|
|
1458
1862
|
verification = runConverge(allNpmPackages, overrides, {
|
|
1459
|
-
|
|
1863
|
+
installMany: repinMany('converge'),
|
|
1460
1864
|
readVersion: readInstalledNpmVersion,
|
|
1461
1865
|
maxRounds: 3,
|
|
1462
1866
|
});
|
|
@@ -1471,13 +1875,14 @@ export async function installCommand(name, options = {}) {
|
|
|
1471
1875
|
console.log('');
|
|
1472
1876
|
console.log(chalk.cyan(`► Holding ${Object.keys(pinTargets).length} pinned package(s) to the override version...`));
|
|
1473
1877
|
runConverge(pinTargets, {}, {
|
|
1474
|
-
|
|
1878
|
+
installMany: repinMany('pin-converge'),
|
|
1475
1879
|
readVersion: readInstalledNpmVersion,
|
|
1476
1880
|
maxRounds: 3,
|
|
1477
1881
|
});
|
|
1478
1882
|
}
|
|
1479
1883
|
verification = verifyInstallMap(effectiveNpmMap, { readVersion: readInstalledNpmVersion });
|
|
1480
1884
|
}
|
|
1885
|
+
endVerify();
|
|
1481
1886
|
}
|
|
1482
1887
|
}
|
|
1483
1888
|
catch (err) {
|
|
@@ -1569,6 +1974,16 @@ export async function installCommand(name, options = {}) {
|
|
|
1569
1974
|
// verification shape { ok, verifiedCount, total, mismatches[{name,expected,got}] }.
|
|
1570
1975
|
if (verification)
|
|
1571
1976
|
ledger.verification = verification;
|
|
1977
|
+
// grant-unify P1 — grant provenance (no token). Absent in off mode.
|
|
1978
|
+
if (grant) {
|
|
1979
|
+
const st = grant;
|
|
1980
|
+
ledger.grant = {
|
|
1981
|
+
mode: grantMode,
|
|
1982
|
+
closureHash: st.current.closureHash.slice(0, 8),
|
|
1983
|
+
memberCount: st.current.members.length,
|
|
1984
|
+
reissues: st.reissues,
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1572
1987
|
// AC1.4 — fold the core-floor verdict too.
|
|
1573
1988
|
ledger.coreFloor = {
|
|
1574
1989
|
ok: coreFloor.ok,
|