@microwiseai/snapshot 0.8.0 → 0.9.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/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 +494 -145
- 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/proxy-userconfig.d.ts +45 -0
- package/dist/lib/proxy-userconfig.d.ts.map +1 -0
- package/dist/lib/proxy-userconfig.js +94 -0
- package/dist/lib/proxy-userconfig.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,23 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import {
|
|
2
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
3
|
+
import { existsSync, readFileSync } 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
|
+
import { activateProxyUserconfig } from '../lib/proxy-userconfig.js';
|
|
11
12
|
import { installSkitPackage } from '../lib/skit-adapter.js';
|
|
12
13
|
import { markLicenseChecked } from '@microwiseai/license';
|
|
13
|
-
import { authorizeSession, packagesToSpecs } from '../lib/session.js';
|
|
14
|
+
import { authorizeSession, packagesToSpecs, maskSessionId } from '../lib/session.js';
|
|
15
|
+
import { readGrantMode, readGrantTimeoutMs, requestSnapshotGrant, GrantState, GrantError, checkGrantCoverage, findRootMemberVersion, formatGrantLog, splitSnapshotSpec, resolveInstallVersion, } from '../lib/grant.js';
|
|
14
16
|
import { parallelInstall, recordToEntries } from '@microwiseai/snapshot-parallel';
|
|
17
|
+
import { detectDowngrades, resolveDowngradePolicy, readOneShotOverride, getMachineDowngradeMode, formatDowngradeReport, extractGlobalInstallSpecs, } from '@microwiseai/snapshot-downgrade-guard';
|
|
15
18
|
import { dedupWithReport, dedupInstallBlock, recordInstalledSpecs, recordReferences, computeReplacePlan, applyReplaceHistory, } from '@microwiseai/snapshot-dedup';
|
|
16
19
|
import { createInterface } from 'readline';
|
|
20
|
+
import { timingStart } from '../lib/timing.js';
|
|
17
21
|
import { StepRecorder, buildLedger, writeLedger, queryDependencyTree, } from '../lib/ledger.js';
|
|
18
22
|
import { coreFloorAssert, formatCoreFloorOffenders, DEFAULT_CORE_FLOOR_PKG, DEFAULT_CORE_FLOOR_MIN_VERSION, } from '../lib/core-floor.js';
|
|
19
23
|
import { verifyInstallMap, runConverge, readInstalledNpmVersion, formatVerificationFailure, } from '../lib/install-verify.js';
|
|
@@ -89,86 +93,19 @@ const DEFAULT_INSTALLERS = {
|
|
|
89
93
|
uninstall: 'npm uninstall -g {pkg}', // verified: `npm uninstall`
|
|
90
94
|
},
|
|
91
95
|
};
|
|
96
|
+
// ============================================================
|
|
97
|
+
// npm userconfig for registry-proxy (temp file, ~/.npmrc is never written)
|
|
98
|
+
// ============================================================
|
|
92
99
|
/**
|
|
93
|
-
*
|
|
100
|
+
* Point npm (and skit) at a temp userconfig carrying the proxy scope lines and,
|
|
101
|
+
* when authenticated, the env-referenced auth line. See lib/proxy-userconfig.ts.
|
|
94
102
|
*/
|
|
95
103
|
function configureNpmrcForProxy(scopes) {
|
|
96
|
-
|
|
97
|
-
let originalContent = null;
|
|
98
|
-
// Backup existing .npmrc
|
|
99
|
-
if (existsSync(npmrcPath)) {
|
|
100
|
-
originalContent = readFileSync(npmrcPath, 'utf-8');
|
|
101
|
-
}
|
|
102
|
-
// Build new .npmrc content
|
|
103
|
-
const proxyHost = REGISTRY_PROXY_URL.replace('https://', '');
|
|
104
|
-
const lines = [];
|
|
105
|
-
// Keep existing content that's not related to our scopes
|
|
106
|
-
if (originalContent) {
|
|
107
|
-
const existingLines = originalContent.split('\n');
|
|
108
|
-
for (const line of existingLines) {
|
|
109
|
-
// Skip lines related to our scopes or proxy
|
|
110
|
-
const isOurScope = scopes.some(s => line.includes(`${s}:registry`));
|
|
111
|
-
const isProxyAuth = line.includes(proxyHost);
|
|
112
|
-
if (!isOurScope && !isProxyAuth && line.trim()) {
|
|
113
|
-
lines.push(line);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Add proxy registry config for each scope
|
|
118
|
-
for (const scope of scopes) {
|
|
119
|
-
lines.push(`${scope}:registry=${REGISTRY_PROXY_URL}/`);
|
|
120
|
-
}
|
|
121
|
-
// 환경변수 참조 — 토큰 직접 안 넣음 (npm이 ${ENV_VAR} 구문을 해석)
|
|
122
|
-
lines.push(`//${proxyHost}/:_authToken=\${IST_SESSION_TOKEN}`);
|
|
123
|
-
writeFileSync(npmrcPath, lines.join('\n') + '\n');
|
|
124
|
-
return { path: npmrcPath, content: originalContent };
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Restore original .npmrc
|
|
128
|
-
*/
|
|
129
|
-
function restoreNpmrc(backup) {
|
|
130
|
-
if (backup.content === null) {
|
|
131
|
-
// Original didn't exist, remove it
|
|
132
|
-
if (existsSync(backup.path)) {
|
|
133
|
-
unlinkSync(backup.path);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// Restore original content
|
|
138
|
-
writeFileSync(backup.path, backup.content);
|
|
139
|
-
}
|
|
104
|
+
return activateProxyUserconfig({ scopes, proxyUrl: REGISTRY_PROXY_URL, withAuth: true });
|
|
140
105
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Configure .npmrc for registry-proxy WITHOUT auth token (anonymous access)
|
|
143
|
-
*/
|
|
106
|
+
/** Same, WITHOUT the auth line (anonymous access). */
|
|
144
107
|
function configureNpmrcForProxyAnonymous(scopes) {
|
|
145
|
-
|
|
146
|
-
let originalContent = null;
|
|
147
|
-
// Backup existing .npmrc
|
|
148
|
-
if (existsSync(npmrcPath)) {
|
|
149
|
-
originalContent = readFileSync(npmrcPath, 'utf-8');
|
|
150
|
-
}
|
|
151
|
-
// Build new .npmrc content
|
|
152
|
-
const proxyHost = REGISTRY_PROXY_URL.replace('https://', '');
|
|
153
|
-
const lines = [];
|
|
154
|
-
// Keep existing content that's not related to our scopes
|
|
155
|
-
if (originalContent) {
|
|
156
|
-
const existingLines = originalContent.split('\n');
|
|
157
|
-
for (const line of existingLines) {
|
|
158
|
-
// Skip lines related to our scopes or proxy
|
|
159
|
-
const isOurScope = scopes.some(s => line.includes(`${s}:registry`));
|
|
160
|
-
const isProxyAuth = line.includes(proxyHost);
|
|
161
|
-
if (!isOurScope && !isProxyAuth && line.trim()) {
|
|
162
|
-
lines.push(line);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Add proxy registry config for each scope (NO auth token)
|
|
167
|
-
for (const scope of scopes) {
|
|
168
|
-
lines.push(`${scope}:registry=${REGISTRY_PROXY_URL}/`);
|
|
169
|
-
}
|
|
170
|
-
writeFileSync(npmrcPath, lines.join('\n') + '\n');
|
|
171
|
-
return { path: npmrcPath, content: originalContent };
|
|
108
|
+
return activateProxyUserconfig({ scopes, proxyUrl: REGISTRY_PROXY_URL, withAuth: false });
|
|
172
109
|
}
|
|
173
110
|
/**
|
|
174
111
|
* Extract scopes from packages
|
|
@@ -198,27 +135,148 @@ function commandExists(cmd) {
|
|
|
198
135
|
// Captures the failure reason of the most recent runCommand() call so callers
|
|
199
136
|
// can surface it (debugging visibility). Undefined when the last call succeeded.
|
|
200
137
|
let lastCommandError;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
138
|
+
// ============================================================
|
|
139
|
+
// Downgrade guard — npm-exec choke point (defense in depth)
|
|
140
|
+
// ============================================================
|
|
141
|
+
// EVERY global install funnels through runCommand / runCommandSync (the spawn
|
|
142
|
+
// wrappers) — including installCommand's batches, a direct installSnapshot()
|
|
143
|
+
// library call, extends recursion, and parallelInstall's per-spec callback.
|
|
144
|
+
// Detecting downgrades HERE, at that single execution boundary, means NO entry
|
|
145
|
+
// point can bypass the guard (closes the lib.ts public-export bypass). This is a
|
|
146
|
+
// SECOND layer; installCommand's earlier pre-install guard stays as-is.
|
|
147
|
+
//
|
|
148
|
+
// Policy threading: installCommand resolves the mode once and publishes it via
|
|
149
|
+
// setActiveDowngradeMode(); this boundary reuses it (no recompute, no duplicate
|
|
150
|
+
// report). With no upstream context (direct library call), the boundary
|
|
151
|
+
// self-resolves from env (SNAPSHOT_ALLOW_LOCAL) + machine config, defaulting to
|
|
152
|
+
// the safe 'refuse'.
|
|
153
|
+
export class DowngradeRefusedError extends Error {
|
|
154
|
+
constructor(message) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = 'DowngradeRefusedError';
|
|
208
157
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
158
|
+
}
|
|
159
|
+
let activeDowngradeMode;
|
|
160
|
+
/**
|
|
161
|
+
* Publish (or clear) the downgrade policy resolved upstream by installCommand so
|
|
162
|
+
* the exec boundary reuses it. Exported as a test seam.
|
|
163
|
+
*/
|
|
164
|
+
export function setActiveDowngradeMode(mode) {
|
|
165
|
+
activeDowngradeMode = mode;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Enforce the downgrade policy for a global-install command about to execute.
|
|
169
|
+
* No-op unless the command is a global install carrying versioned specs AND at
|
|
170
|
+
* least one spec would downgrade an already-installed global package. On a
|
|
171
|
+
* 'refuse' verdict it throws DowngradeRefusedError BEFORE spawn (zero partial
|
|
172
|
+
* install); 'warn' logs and proceeds; 'allow' proceeds silently. Pure detection
|
|
173
|
+
* is delegated to @microwiseai/snapshot-downgrade-guard.
|
|
174
|
+
*/
|
|
175
|
+
function assertNoRefusedDowngrade(command) {
|
|
176
|
+
const specs = extractGlobalInstallSpecs(command);
|
|
177
|
+
if (Object.keys(specs).length === 0)
|
|
178
|
+
return;
|
|
179
|
+
const { wouldDowngrade, offenders } = detectDowngrades(specs, {
|
|
180
|
+
readVersion: readInstalledNpmVersion,
|
|
181
|
+
});
|
|
182
|
+
if (!wouldDowngrade)
|
|
183
|
+
return;
|
|
184
|
+
// Reuse the upstream-resolved mode when present (installCommand path); else
|
|
185
|
+
// self-resolve for the context-free direct library-call path.
|
|
186
|
+
const threaded = activeDowngradeMode !== undefined;
|
|
187
|
+
const mode = threaded
|
|
188
|
+
? activeDowngradeMode
|
|
189
|
+
: resolveDowngradePolicy({
|
|
190
|
+
override: readOneShotOverride({ env: process.env }),
|
|
191
|
+
machine: getMachineDowngradeMode(),
|
|
192
|
+
// snapshot.json policy is not available at this boundary; the default
|
|
193
|
+
// 'refuse' keeps the direct-call path on the safe side.
|
|
194
|
+
});
|
|
195
|
+
if (mode === 'refuse') {
|
|
196
|
+
// Only the self-resolved (direct-call) path reports here — the installCommand
|
|
197
|
+
// path already printed upstream and cannot reach a refuse at this boundary
|
|
198
|
+
// (it exits during pre-install), so this avoids duplicate output.
|
|
199
|
+
if (!threaded) {
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(chalk.red(formatDowngradeReport(offenders, mode)));
|
|
219
202
|
}
|
|
203
|
+
throw new DowngradeRefusedError(formatDowngradeReport(offenders, mode));
|
|
204
|
+
}
|
|
205
|
+
if (mode === 'warn' && !threaded) {
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(chalk.yellow(formatDowngradeReport(offenders, mode)));
|
|
208
|
+
}
|
|
209
|
+
// mode === 'allow' → proceed.
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Run a command with FULLY LIVE output (spawn, async).
|
|
213
|
+
*
|
|
214
|
+
* Why spawn+async (not execSync): npm writes its progress bar to STDERR, and the
|
|
215
|
+
* previous `execSync(stdio:['inherit','inherit','pipe'])` piped stderr — which
|
|
216
|
+
* (a) hid npm's live progress (looked "frozen" for tens of seconds) and
|
|
217
|
+
* (b) blocked the event loop so no JS spinner could ever turn. Inheriting all
|
|
218
|
+
* three streams lets npm's own progress render live to the terminal while the
|
|
219
|
+
* event loop stays free. See install-speed-visibility work (B).
|
|
220
|
+
*
|
|
221
|
+
* On failure we can no longer capture the stderr text (it went straight to the
|
|
222
|
+
* terminal, so the user already SAW it) — `lastCommandError` carries the exit
|
|
223
|
+
* code instead.
|
|
224
|
+
*/
|
|
225
|
+
// Exported as a test seam (unit-tested in install.runcommand.test.ts).
|
|
226
|
+
export function runCommand(command) {
|
|
227
|
+
// Depth-defense: abort BEFORE spawn if this global install would downgrade and
|
|
228
|
+
// policy is refuse. Throws DowngradeRefusedError (no partial install).
|
|
229
|
+
assertNoRefusedDowngrade(command);
|
|
230
|
+
lastCommandError = undefined;
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
// shell:true so the plain command string (e.g. "npm install -g a@1 b@2")
|
|
233
|
+
// is parsed exactly as it was under execSync.
|
|
234
|
+
const child = spawn(command, { stdio: 'inherit', shell: true });
|
|
235
|
+
child.on('error', (err) => {
|
|
236
|
+
lastCommandError = `spawn error — ${err.message}`;
|
|
237
|
+
console.log(chalk.red(` Command failed to start: ${command}`));
|
|
238
|
+
console.log(chalk.red(err.message));
|
|
239
|
+
resolve(false);
|
|
240
|
+
});
|
|
241
|
+
child.on('close', (code) => {
|
|
242
|
+
if (code === 0) {
|
|
243
|
+
resolve(true);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
lastCommandError = `exit code ${code ?? 'unknown'}`;
|
|
247
|
+
console.log(chalk.red(` Command failed (exit code ${code ?? 'unknown'}): ${command}`));
|
|
248
|
+
resolve(false);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Synchronous streaming variant — used ONLY by the converge re-pin callback,
|
|
254
|
+
* which is invoked from the pure/sync `runConverge` (lib/install-verify.ts).
|
|
255
|
+
* Keeping this sync avoids making `runConverge` async (which would ripple into
|
|
256
|
+
* its unit-test suite and break its pure-function contract). `spawnSync` with
|
|
257
|
+
* inherited stdio still streams npm's live progress to the terminal (the child
|
|
258
|
+
* writes directly to the inherited fd), so the converge phase no longer looks
|
|
259
|
+
* frozen either — it only blocks the JS event loop for its (batched, rare)
|
|
260
|
+
* duration.
|
|
261
|
+
*/
|
|
262
|
+
// Exported as a test seam (unit-tested in install.runcommand.test.ts).
|
|
263
|
+
export function runCommandSync(command) {
|
|
264
|
+
// Depth-defense: abort BEFORE spawn if this global install would downgrade and
|
|
265
|
+
// policy is refuse. Throws DowngradeRefusedError (no partial install).
|
|
266
|
+
assertNoRefusedDowngrade(command);
|
|
267
|
+
lastCommandError = undefined;
|
|
268
|
+
const res = spawnSync(command, { stdio: 'inherit', shell: true });
|
|
269
|
+
if (res.error) {
|
|
270
|
+
lastCommandError = `spawn error — ${res.error.message}`;
|
|
271
|
+
console.log(chalk.red(` Command failed to start: ${command}`));
|
|
272
|
+
console.log(chalk.red(res.error.message));
|
|
220
273
|
return false;
|
|
221
274
|
}
|
|
275
|
+
if (res.status === 0)
|
|
276
|
+
return true;
|
|
277
|
+
lastCommandError = `exit code ${res.status ?? 'unknown'}`;
|
|
278
|
+
console.log(chalk.red(` Command failed (exit code ${res.status ?? 'unknown'}): ${command}`));
|
|
279
|
+
return false;
|
|
222
280
|
}
|
|
223
281
|
/**
|
|
224
282
|
* Check if a skit package is already installed with the specified version
|
|
@@ -264,8 +322,23 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId, re
|
|
|
264
322
|
const total = packageEntries.length;
|
|
265
323
|
console.log('');
|
|
266
324
|
console.log(chalk.yellow(`${indent}=== ${snapshotName}: ${total} skits ===`));
|
|
267
|
-
// Prepare install options with sessionId if available
|
|
268
|
-
|
|
325
|
+
// Prepare install options with sessionId if available. Forward onProgress so
|
|
326
|
+
// the skit lib's live install messages reach the terminal (skit-adapter.ts
|
|
327
|
+
// already forwards options.onProgress into lib.installSkit) — otherwise each
|
|
328
|
+
// skit install is silent until it completes.
|
|
329
|
+
// grant-unify P1: a function source is read PER KIT, right before the skit
|
|
330
|
+
// call, so a re-issued grant token is picked up (grant mode passes the istsg_
|
|
331
|
+
// token as the skit session → X-Session-Id on the kit tarball download).
|
|
332
|
+
// A plain string (legacy authorizeSession id) behaves exactly as before.
|
|
333
|
+
const skitOptionsFor = () => {
|
|
334
|
+
const sid = typeof sessionId === 'function' ? sessionId() : sessionId;
|
|
335
|
+
return {
|
|
336
|
+
...(sid ? { sessionId: sid } : {}),
|
|
337
|
+
onProgress: (message) => {
|
|
338
|
+
console.log(chalk.gray(`${indent} ${message}`));
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
};
|
|
269
342
|
// Install packages in parallel using snapshot-parallel
|
|
270
343
|
const entries = recordToEntries(packages);
|
|
271
344
|
const parallelResult = await parallelInstall(entries, async (entry) => {
|
|
@@ -273,7 +346,11 @@ async function installSkitPackages(packages, snapshotName, indent, sessionId, re
|
|
|
273
346
|
if (isSkitInstalled(entry.name, entry.version)) {
|
|
274
347
|
return { success: true, skipped: true };
|
|
275
348
|
}
|
|
276
|
-
|
|
349
|
+
// Per-skit start log so a slow single install doesn't look frozen.
|
|
350
|
+
console.log(chalk.gray(`${indent} installing ${entry.name}@${entry.version}…`));
|
|
351
|
+
const endKit = timingStart(`kit ${entry.name}@${entry.version}`);
|
|
352
|
+
const installResult = await installSkitPackage(entry.name, entry.version, skitOptionsFor());
|
|
353
|
+
endKit();
|
|
277
354
|
return {
|
|
278
355
|
success: installResult.success,
|
|
279
356
|
error: installResult.error,
|
|
@@ -333,7 +410,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
333
410
|
if (!commandExists(cmdName)) {
|
|
334
411
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
335
412
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
336
|
-
if (runCommand(installerConfig.hint)) {
|
|
413
|
+
if (await runCommand(installerConfig.hint)) {
|
|
337
414
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
338
415
|
}
|
|
339
416
|
else {
|
|
@@ -353,6 +430,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
353
430
|
if (isNpmInstaller) {
|
|
354
431
|
// Batch install: collect all not-yet-installed packages and run a single npm install -g
|
|
355
432
|
const toInstall = [];
|
|
433
|
+
const endProbe = timingStart(`layer already-installed probes (${entries.length})`);
|
|
356
434
|
for (const entry of entries) {
|
|
357
435
|
if (isNpmGlobalInstalled(entry.name, entry.version)) {
|
|
358
436
|
console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
|
|
@@ -362,11 +440,12 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
362
440
|
toInstall.push(entry);
|
|
363
441
|
}
|
|
364
442
|
}
|
|
443
|
+
endProbe();
|
|
365
444
|
if (toInstall.length > 0) {
|
|
366
445
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
367
446
|
const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
|
|
368
447
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
369
|
-
const success = runCommand(batchCommand);
|
|
448
|
+
const success = await runCommand(batchCommand);
|
|
370
449
|
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
371
450
|
recorder?.record({
|
|
372
451
|
phase: 'layer-batch',
|
|
@@ -389,7 +468,7 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
389
468
|
for (const entry of toInstall) {
|
|
390
469
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
391
470
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
392
|
-
const ok = runCommand(command);
|
|
471
|
+
const ok = await runCommand(command);
|
|
393
472
|
recorder?.record({
|
|
394
473
|
phase: 'layer-fallback',
|
|
395
474
|
installer: installerName,
|
|
@@ -410,14 +489,18 @@ async function installPackagesByType(packages, installerName, installers, snapsh
|
|
|
410
489
|
}
|
|
411
490
|
}
|
|
412
491
|
else {
|
|
413
|
-
// Non-npm installer:
|
|
492
|
+
// Non-npm installer: run SERIALLY (concurrency 1). runCommand now spawns with
|
|
493
|
+
// stdio:'inherit', so concurrent children would interleave output on the same
|
|
494
|
+
// terminal AND race the module-global lastCommandError (mis-attributing the
|
|
495
|
+
// failure reason). Before the B change this path used sync execSync and was
|
|
496
|
+
// therefore effectively serial; concurrency:1 restores that (zero regression).
|
|
414
497
|
const parallelResult = await parallelInstall(entries, async (entry) => {
|
|
415
498
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
416
499
|
const command = installerConfig.command.replace('{pkg}', pkgSpec);
|
|
417
|
-
const success = runCommand(command);
|
|
500
|
+
const success = await runCommand(command);
|
|
418
501
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
419
502
|
}, {
|
|
420
|
-
concurrency:
|
|
503
|
+
concurrency: 1,
|
|
421
504
|
onProgress: (event) => {
|
|
422
505
|
const { result: r, current, total: t } = event;
|
|
423
506
|
const progress = `[${current}/${t}]`;
|
|
@@ -463,6 +546,32 @@ function applySnapshotOverrides(snapshot, overrides) {
|
|
|
463
546
|
}
|
|
464
547
|
return next;
|
|
465
548
|
}
|
|
549
|
+
// ============================================================
|
|
550
|
+
// Grant transport token (grant-unify P1, SNAPSHOT_GRANT_MODE)
|
|
551
|
+
// ============================================================
|
|
552
|
+
// When a snapshot grant is active, EVERY registry-proxy snapshot fetch (incl.
|
|
553
|
+
// extends bases) carries the istsg_ grant token explicitly — it takes precedence
|
|
554
|
+
// over the license key / IST token resolution inside fetchSnapshotFromRepo.
|
|
555
|
+
// null (always, in SNAPSHOT_GRANT_MODE=off) → the legacy call shape is kept.
|
|
556
|
+
let activeGrantToken = null;
|
|
557
|
+
/** Fetch an extends base snapshot (grant token when a grant is active). */
|
|
558
|
+
function fetchBaseSnapshot(spec) {
|
|
559
|
+
return activeGrantToken ? fetchSnapshotFromRepo(spec, activeGrantToken) : fetchSnapshotFromRepo(spec);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Collect the extends chain of a snapshot as exact {name, version} specs (the
|
|
563
|
+
* ancestor generics). Grant mode only — used for the closure coverage check.
|
|
564
|
+
*/
|
|
565
|
+
async function collectExtendsChain(snapshot) {
|
|
566
|
+
const chain = [];
|
|
567
|
+
let cur = snapshot;
|
|
568
|
+
for (let depth = 0; cur.extends && depth < 16; depth++) {
|
|
569
|
+
const base = await fetchBaseSnapshot(cur.extends);
|
|
570
|
+
chain.push({ name: base.name, version: base.version });
|
|
571
|
+
cur = base;
|
|
572
|
+
}
|
|
573
|
+
return chain;
|
|
574
|
+
}
|
|
466
575
|
/**
|
|
467
576
|
* FB — the subset of pin overrides that actually apply to this install (i.e. the
|
|
468
577
|
* overridden package IS declared in the effective npm map). These are the
|
|
@@ -502,7 +611,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
502
611
|
console.log('');
|
|
503
612
|
console.log(chalk.cyan(`${indent}► Extends: ${snapshot.extends}`));
|
|
504
613
|
try {
|
|
505
|
-
const baseSnapshot = await
|
|
614
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
506
615
|
console.log(chalk.gray(`${indent} Installing base snapshot...`));
|
|
507
616
|
const baseResult = await installSnapshot(baseSnapshot, depth + 1, installers, options);
|
|
508
617
|
result.installed.push(...baseResult.result.installed);
|
|
@@ -589,21 +698,22 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
589
698
|
const resolved = await resolveSnapshotDependencies(snapshot, { skipRegistry: true });
|
|
590
699
|
// skit 패키지 설치 (skit-adapter 사용: lib 우선, CLI fallback)
|
|
591
700
|
if (Object.keys(resolved.skit).length > 0) {
|
|
592
|
-
|
|
701
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
702
|
+
const skitResult = await installSkitPackages(resolved.skit, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
593
703
|
result.installed.push(...skitResult.installed);
|
|
594
704
|
result.failed.push(...skitResult.failed);
|
|
595
705
|
}
|
|
596
706
|
// npm 패키지 설치 (proxy 사용 시 npm-proxy, 아니면 glpkg)
|
|
597
707
|
if (Object.keys(resolved.npm).length > 0) {
|
|
598
708
|
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);
|
|
709
|
+
const npmResult = await installPackagesByType(resolved.npm, npmInstaller, installers, snapshot.name, indent, 'npm packages', (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
600
710
|
result.installed.push(...npmResult.installed);
|
|
601
711
|
result.failed.push(...npmResult.failed);
|
|
602
712
|
}
|
|
603
713
|
// generic 패키지 설치 (glpkg)
|
|
604
714
|
if (Object.keys(resolved.generic).length > 0) {
|
|
605
715
|
const genericInstaller = options.useProxy ? 'npm-proxy' : 'glpkg';
|
|
606
|
-
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', options.sessionId, options.recorder);
|
|
716
|
+
const genericResult = await installPackagesByType(resolved.generic, genericInstaller, installers, snapshot.name, indent, 'packages', (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
607
717
|
result.installed.push(...genericResult.installed);
|
|
608
718
|
result.failed.push(...genericResult.failed);
|
|
609
719
|
}
|
|
@@ -620,7 +730,8 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
620
730
|
continue;
|
|
621
731
|
// Skit packages: use skit-adapter (lib 우선, CLI fallback)
|
|
622
732
|
if (installerName === 'skit') {
|
|
623
|
-
|
|
733
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
734
|
+
const skitResult = await installSkitPackages(packages, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
624
735
|
result.installed.push(...skitResult.installed);
|
|
625
736
|
result.failed.push(...skitResult.failed);
|
|
626
737
|
continue;
|
|
@@ -639,7 +750,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
639
750
|
if (!commandExists(cmdName)) {
|
|
640
751
|
console.log(chalk.yellow(`${indent}${installerName} not found. Attempting auto-install...`));
|
|
641
752
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
642
|
-
if (runCommand(installerConfig.hint)) {
|
|
753
|
+
if (await runCommand(installerConfig.hint)) {
|
|
643
754
|
console.log(chalk.green(`${indent} ✓ ${installerName} installed successfully`));
|
|
644
755
|
}
|
|
645
756
|
else {
|
|
@@ -660,6 +771,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
660
771
|
if (isNpmType) {
|
|
661
772
|
// Batch install: collect all not-yet-installed packages and run a single npm install -g
|
|
662
773
|
const toInstall = [];
|
|
774
|
+
const endProbe = timingStart(`layer already-installed probes (${installEntries.length})`);
|
|
663
775
|
for (const entry of installEntries) {
|
|
664
776
|
if (isNpmGlobalInstalled(entry.name, entry.version)) {
|
|
665
777
|
console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
|
|
@@ -669,11 +781,12 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
669
781
|
toInstall.push(entry);
|
|
670
782
|
}
|
|
671
783
|
}
|
|
784
|
+
endProbe();
|
|
672
785
|
if (toInstall.length > 0) {
|
|
673
786
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
674
787
|
const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
|
|
675
788
|
console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
|
|
676
|
-
const success = runCommand(batchCommand);
|
|
789
|
+
const success = await runCommand(batchCommand);
|
|
677
790
|
// S3 — record the batch step (ok, or 'fallback' if it failed → retried below).
|
|
678
791
|
options.recorder?.record({
|
|
679
792
|
phase: 'layer-batch',
|
|
@@ -696,7 +809,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
696
809
|
for (const entry of toInstall) {
|
|
697
810
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
698
811
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
699
|
-
const ok = runCommand(command);
|
|
812
|
+
const ok = await runCommand(command);
|
|
700
813
|
options.recorder?.record({
|
|
701
814
|
phase: 'layer-fallback',
|
|
702
815
|
installer: installerName,
|
|
@@ -717,14 +830,16 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
717
830
|
}
|
|
718
831
|
}
|
|
719
832
|
else {
|
|
720
|
-
// Non-npm installer:
|
|
833
|
+
// Non-npm installer: run SERIALLY (concurrency 1) — see rationale above:
|
|
834
|
+
// spawn(stdio:'inherit') children would interleave terminal output and
|
|
835
|
+
// race the global lastCommandError. Restores the pre-B serial behavior.
|
|
721
836
|
const parallelResult = await parallelInstall(installEntries, async (entry) => {
|
|
722
837
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
723
838
|
const command = installCfg.command.replace('{pkg}', pkgSpec);
|
|
724
|
-
const success = runCommand(command);
|
|
839
|
+
const success = await runCommand(command);
|
|
725
840
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
726
841
|
}, {
|
|
727
|
-
concurrency:
|
|
842
|
+
concurrency: 1,
|
|
728
843
|
onProgress: (event) => {
|
|
729
844
|
const { result: r, current, total: t } = event;
|
|
730
845
|
const progress = `[${current}/${t}]`;
|
|
@@ -759,7 +874,7 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
759
874
|
if (!commandExists(cmdName)) {
|
|
760
875
|
console.log(chalk.yellow(`${indent}glpkg not found. Attempting auto-install...`));
|
|
761
876
|
console.log(chalk.gray(`${indent} Running: ${installerConfig.hint}`));
|
|
762
|
-
if (runCommand(installerConfig.hint)) {
|
|
877
|
+
if (await runCommand(installerConfig.hint)) {
|
|
763
878
|
console.log(chalk.green(`${indent} ✓ glpkg installed successfully`));
|
|
764
879
|
}
|
|
765
880
|
else {
|
|
@@ -774,13 +889,15 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
774
889
|
else {
|
|
775
890
|
const legacyEntries = recordToEntries(snapshot.packages);
|
|
776
891
|
const legacyCfg = installerConfig; // capture for closure
|
|
892
|
+
// Legacy glpkg packages: run SERIALLY (concurrency 1) — see rationale above
|
|
893
|
+
// (spawn:'inherit' output interleave + global lastCommandError race).
|
|
777
894
|
const parallelResult = await parallelInstall(legacyEntries, async (entry) => {
|
|
778
895
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
779
896
|
const command = legacyCfg.command.replace('{pkg}', pkgSpec);
|
|
780
|
-
const success = runCommand(command);
|
|
897
|
+
const success = await runCommand(command);
|
|
781
898
|
return { success, error: success ? undefined : `Command failed: ${command}` };
|
|
782
899
|
}, {
|
|
783
|
-
concurrency:
|
|
900
|
+
concurrency: 1,
|
|
784
901
|
onProgress: (event) => {
|
|
785
902
|
const { result: r, current, total: t } = event;
|
|
786
903
|
const progress = `[${current}/${t}]`;
|
|
@@ -806,7 +923,8 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
|
|
|
806
923
|
}
|
|
807
924
|
// Legacy: skits (use skit-adapter: lib 우선, CLI fallback)
|
|
808
925
|
if (snapshot.skits && Object.keys(snapshot.skits).length > 0) {
|
|
809
|
-
|
|
926
|
+
await options.beforePhase?.(`kit:${snapshot.name}`);
|
|
927
|
+
const skitResult = await installSkitPackages(snapshot.skits, snapshot.name, indent, (options.skitSessionId ?? options.sessionId), options.recorder);
|
|
810
928
|
result.installed.push(...skitResult.installed);
|
|
811
929
|
result.failed.push(...skitResult.failed);
|
|
812
930
|
}
|
|
@@ -846,7 +964,7 @@ async function collectAllNpmPackages(snapshot, useProxy = true) {
|
|
|
846
964
|
// Recurse into extends chain
|
|
847
965
|
if (snapshot.extends) {
|
|
848
966
|
try {
|
|
849
|
-
const baseSnapshot = await
|
|
967
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
850
968
|
const baseNpmPackages = await collectAllNpmPackages(baseSnapshot, useProxy);
|
|
851
969
|
// Base packages: don't override current snapshot's versions
|
|
852
970
|
for (const [name, version] of Object.entries(baseNpmPackages)) {
|
|
@@ -883,7 +1001,7 @@ export async function collectAllPackages(snapshot) {
|
|
|
883
1001
|
// Recurse into extends chain
|
|
884
1002
|
if (snapshot.extends) {
|
|
885
1003
|
try {
|
|
886
|
-
const baseSnapshot = await
|
|
1004
|
+
const baseSnapshot = await fetchBaseSnapshot(snapshot.extends);
|
|
887
1005
|
const basePackages = await collectAllPackages(baseSnapshot);
|
|
888
1006
|
Object.assign(allPackages, basePackages);
|
|
889
1007
|
}
|
|
@@ -996,7 +1114,7 @@ async function runReplaceMode(snapshotName, newSet, options, installers) {
|
|
|
996
1114
|
continue;
|
|
997
1115
|
}
|
|
998
1116
|
console.log(chalk.gray(` Removing ${item.name}...`));
|
|
999
|
-
if (runCommand(cmd)) {
|
|
1117
|
+
if (await runCommand(cmd)) {
|
|
1000
1118
|
console.log(` ${item.name}${chalk.green(' ✓ removed')}`);
|
|
1001
1119
|
removed.push(item);
|
|
1002
1120
|
}
|
|
@@ -1057,6 +1175,9 @@ async function previewInstall(snapshot, options) {
|
|
|
1057
1175
|
console.log(chalk.gray('Dry-run complete: no installs, no uninstalls, no .npmrc or history changes.'));
|
|
1058
1176
|
}
|
|
1059
1177
|
export async function installCommand(name, options = {}) {
|
|
1178
|
+
// Start from a clean downgrade-policy context; the pre-install guard publishes
|
|
1179
|
+
// the resolved mode below, and cleanup() clears it on every exit path.
|
|
1180
|
+
setActiveDowngradeMode(undefined);
|
|
1060
1181
|
console.log(chalk.blue.bold(`
|
|
1061
1182
|
╔══════════════════════════════════════════════════════════════╗
|
|
1062
1183
|
║ Snapshot Installer ║
|
|
@@ -1106,11 +1227,90 @@ export async function installCommand(name, options = {}) {
|
|
|
1106
1227
|
console.log(chalk.yellow('⚠') + ' No auth token — anonymous access');
|
|
1107
1228
|
}
|
|
1108
1229
|
const useProxy = true;
|
|
1230
|
+
// ========== Snapshot grant (grant-unify P1, SNAPSHOT_GRANT_MODE) ==========
|
|
1231
|
+
// off (default): nothing below runs (grant stays null) — legacy path.
|
|
1232
|
+
// prefer/require: request ONE istsg_ grant BEFORE fetching snapshot.json, since
|
|
1233
|
+
// the root generic itself is a closure member the proxy may gate. The grant is
|
|
1234
|
+
// always for an EXACT version: none/'latest' is resolved first with the proxy
|
|
1235
|
+
// rule (highest stable, prereleases ignored — same as the legacy path's
|
|
1236
|
+
// 'latest'); license-api's own 'latest' (= newest upload) is never used. prefer drops
|
|
1237
|
+
// the grant on any grant failure and runs the whole install on the legacy
|
|
1238
|
+
// path; require is fail-loud. dry-run + prefer skips the grant (zero side
|
|
1239
|
+
// effects); dry-run + require still needs it to fetch the snapshot.
|
|
1240
|
+
activeGrantToken = null;
|
|
1241
|
+
const grantMode = readGrantMode(process.env, (m) => console.log(chalk.yellow('⚠ ' + m)));
|
|
1242
|
+
const { name: snapshotBaseName, version: requestedVersion } = splitSnapshotSpec(name);
|
|
1243
|
+
let grant = null;
|
|
1244
|
+
let grantEnvActive = false;
|
|
1245
|
+
/** Grant failure policy: require → exit 1; prefer → warn, drop grant, legacy path. */
|
|
1246
|
+
const onGrantFailure = (err) => {
|
|
1247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1248
|
+
if (grantMode === 'require') {
|
|
1249
|
+
console.log(chalk.red(`⛔ Snapshot grant failed (SNAPSHOT_GRANT_MODE=require): ${msg}`));
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
console.log(chalk.yellow(`⚠ Snapshot grant unavailable — continuing on the legacy path: ${msg}`));
|
|
1253
|
+
grant = null;
|
|
1254
|
+
activeGrantToken = null;
|
|
1255
|
+
};
|
|
1256
|
+
if (grantMode !== 'off' && !(options.dryRun && grantMode === 'prefer')) {
|
|
1257
|
+
const timeoutMs = readGrantTimeoutMs();
|
|
1258
|
+
let grantVersion = null;
|
|
1259
|
+
try {
|
|
1260
|
+
grantVersion = await resolveInstallVersion(snapshotBaseName, requestedVersion, {
|
|
1261
|
+
registryUrl: REGISTRY_PROXY_URL,
|
|
1262
|
+
timeoutMs,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
catch (err) {
|
|
1266
|
+
onGrantFailure(err);
|
|
1267
|
+
}
|
|
1268
|
+
if (grantVersion) {
|
|
1269
|
+
grant = new GrantState({ snapshot: snapshotBaseName, version: grantVersion, authToken }, {
|
|
1270
|
+
request: (req) => requestSnapshotGrant(req, { licenseApiUrl: LICENSE_API_URL, timeoutMs }),
|
|
1271
|
+
onToken: (token) => {
|
|
1272
|
+
activeGrantToken = token;
|
|
1273
|
+
if (grantEnvActive)
|
|
1274
|
+
process.env.IST_SESSION_TOKEN = token;
|
|
1275
|
+
},
|
|
1276
|
+
});
|
|
1277
|
+
console.log(chalk.gray(`Requesting snapshot grant (${grantMode}) for ${snapshotBaseName}@${grantVersion}...`));
|
|
1278
|
+
try {
|
|
1279
|
+
const g = await grant.issue();
|
|
1280
|
+
// The closure root (when present) must be exactly the version we asked for.
|
|
1281
|
+
const rootVersion = findRootMemberVersion(g.members, snapshotBaseName);
|
|
1282
|
+
if (rootVersion && rootVersion !== grantVersion) {
|
|
1283
|
+
throw new GrantError('version-mismatch', `grant closure root ${snapshotBaseName}@${rootVersion} differs from the requested ${grantVersion}`);
|
|
1284
|
+
}
|
|
1285
|
+
console.log(chalk.green('✓') + ' ' + formatGrantLog(grantMode, g));
|
|
1286
|
+
}
|
|
1287
|
+
catch (err) {
|
|
1288
|
+
onGrantFailure(err);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
// ==========================================================================
|
|
1109
1293
|
// Fetch snapshot first — access check depends on snapshot.json content
|
|
1110
1294
|
console.log(chalk.gray('Fetching snapshot...'));
|
|
1111
1295
|
let snapshot;
|
|
1112
1296
|
try {
|
|
1113
|
-
|
|
1297
|
+
if (grant) {
|
|
1298
|
+
// Grant mode: fetch exactly the granted version with the grant token.
|
|
1299
|
+
const grantVersion = grant.version;
|
|
1300
|
+
try {
|
|
1301
|
+
snapshot = await fetchSnapshotFromRepo(`${snapshotBaseName}@${grantVersion}`, activeGrantToken);
|
|
1302
|
+
if (snapshot.version !== grantVersion) {
|
|
1303
|
+
throw new GrantError('version-mismatch', `snapshot ${snapshotBaseName}@${snapshot.version} does not match the granted version ${grantVersion}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
catch (err) {
|
|
1307
|
+
onGrantFailure(err);
|
|
1308
|
+
snapshot = await fetchSnapshotFromRepo(name, authToken);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
snapshot = await fetchSnapshotFromRepo(name, authToken);
|
|
1313
|
+
}
|
|
1114
1314
|
}
|
|
1115
1315
|
catch (error) {
|
|
1116
1316
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -1180,8 +1380,13 @@ export async function installCommand(name, options = {}) {
|
|
|
1180
1380
|
const isAuthenticatedAccess = accessField === 'authenticated';
|
|
1181
1381
|
const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
|
|
1182
1382
|
// --- IST Login Check ---
|
|
1183
|
-
|
|
1184
|
-
|
|
1383
|
+
// Single auth verdict: isLoggedIn() = the same credential SSOT that resolved
|
|
1384
|
+
// authToken above (env IST_SESSION_TOKEN or ~/.ist/credentials.json .token,
|
|
1385
|
+
// locally unexpired) and that the 'IST Credentials' prerequisite uses. The old
|
|
1386
|
+
// check read credentials.json .userId, so a token-authenticated install could
|
|
1387
|
+
// print "✓ Auth: IST token" AND "⚠ No IST login" in the same run.
|
|
1388
|
+
const istCredentials = getIstCredentials(); // telemetry identity only (userId/email)
|
|
1389
|
+
if (isLoggedIn()) {
|
|
1185
1390
|
console.log(chalk.green('✓') + ' IST login verified');
|
|
1186
1391
|
}
|
|
1187
1392
|
else if (isPublicAccess) {
|
|
@@ -1242,9 +1447,39 @@ export async function installCommand(name, options = {}) {
|
|
|
1242
1447
|
console.log(chalk.green('✓') + ` License verified for ${productId}`);
|
|
1243
1448
|
}
|
|
1244
1449
|
// =======================================================================
|
|
1450
|
+
// ========== Grant closure coverage (grant mode only) ==========
|
|
1451
|
+
// members[] is the ALLOWED set: every exact pin we will install (flattened
|
|
1452
|
+
// across the extends chain, --pin applied) plus the extends ancestors must be
|
|
1453
|
+
// a closure member, or the proxy would 401 mid-install. A --pin outside the
|
|
1454
|
+
// closure is refused up front (both modes, logged in or not). A manifest pin
|
|
1455
|
+
// outside it is a grant/index inconsistency: prefer → legacy path, require → exit.
|
|
1456
|
+
if (grant) {
|
|
1457
|
+
const g = grant.current;
|
|
1458
|
+
const expectedPins = applyPinOverrides(await collectAllPackages(snapshot), overrides);
|
|
1459
|
+
let ancestors = [];
|
|
1460
|
+
try {
|
|
1461
|
+
ancestors = await collectExtendsChain(snapshot);
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
onGrantFailure(err);
|
|
1465
|
+
}
|
|
1466
|
+
if (grant) {
|
|
1467
|
+
const coverage = checkGrantCoverage([...Object.entries(expectedPins).map(([n, v]) => ({ name: n, version: v })), ...ancestors], g.members);
|
|
1468
|
+
if (!coverage.ok) {
|
|
1469
|
+
const pinViolations = coverage.missing.filter((spec) => Object.entries(overrides).some(([n, v]) => spec === `${n}@${v}`));
|
|
1470
|
+
if (pinViolations.length > 0) {
|
|
1471
|
+
console.log(chalk.red(`⛔ --pin outside the snapshot grant closure: ${pinViolations.join(', ')}`));
|
|
1472
|
+
console.log(chalk.gray(' A pinned version must be part of this snapshot version\'s frozen closure.'));
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
onGrantFailure(new GrantError('invalid-response', `grant closure does not cover ${coverage.missing.length} manifest pin(s): ${coverage.missing.join(', ')}`));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1245
1479
|
// ========== Authorize Session with Registry Proxy ==========
|
|
1246
1480
|
let sessionId;
|
|
1247
|
-
|
|
1481
|
+
// Grant mode: the istsg_ grant replaces authorizeSession (one transport token).
|
|
1482
|
+
if (authToken && !grant) {
|
|
1248
1483
|
// Collect all packages for session authorization
|
|
1249
1484
|
const sessionPackages = [];
|
|
1250
1485
|
// From snapshot.install
|
|
@@ -1268,7 +1503,7 @@ export async function installCommand(name, options = {}) {
|
|
|
1268
1503
|
console.log(chalk.gray(` Auth type: ${authType}`));
|
|
1269
1504
|
try {
|
|
1270
1505
|
sessionId = await authorizeSession(authToken, name, sessionPackages);
|
|
1271
|
-
console.log(chalk.green('✓') + ' Session authorized: ' + sessionId);
|
|
1506
|
+
console.log(chalk.green('✓') + ' Session authorized: ' + maskSessionId(sessionId));
|
|
1272
1507
|
}
|
|
1273
1508
|
catch (error) {
|
|
1274
1509
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -1281,12 +1516,20 @@ export async function installCommand(name, options = {}) {
|
|
|
1281
1516
|
// =============================================================
|
|
1282
1517
|
// Collect all packages from entire extends chain to determine scopes
|
|
1283
1518
|
const allPackages = await collectAllPackages(snapshot);
|
|
1284
|
-
//
|
|
1519
|
+
// Temp npm userconfig for the proxy (with or without auth); ~/.npmrc untouched
|
|
1285
1520
|
let npmrcBackup = null;
|
|
1286
1521
|
if (useProxy) {
|
|
1287
1522
|
const scopes = extractScopes(allPackages);
|
|
1288
1523
|
if (scopes.length > 0) {
|
|
1289
|
-
if (
|
|
1524
|
+
if (grant) {
|
|
1525
|
+
// Grant mode: the istsg_ token is the proxy transport token for npm and
|
|
1526
|
+
// skit (kit npm deps read ${IST_SESSION_TOKEN}). Re-issues update it.
|
|
1527
|
+
process.env.IST_SESSION_TOKEN = grant.current.token;
|
|
1528
|
+
grantEnvActive = true;
|
|
1529
|
+
console.log(chalk.gray(`Configuring registry-proxy (snapshot grant) for scopes: ${scopes.join(', ')}`));
|
|
1530
|
+
npmrcBackup = configureNpmrcForProxy(scopes);
|
|
1531
|
+
}
|
|
1532
|
+
else if (authToken) {
|
|
1290
1533
|
// 환경변수에 설정 (npm이 .npmrc의 ${IST_SESSION_TOKEN}을 해석)
|
|
1291
1534
|
process.env.IST_SESSION_TOKEN = authToken;
|
|
1292
1535
|
console.log(chalk.gray(`Configuring registry-proxy for scopes: ${scopes.join(', ')}`));
|
|
@@ -1312,13 +1555,18 @@ export async function installCommand(name, options = {}) {
|
|
|
1312
1555
|
return;
|
|
1313
1556
|
cleanupDone = true;
|
|
1314
1557
|
if (npmrcBackup) {
|
|
1315
|
-
|
|
1316
|
-
console.log(chalk.gray('
|
|
1558
|
+
npmrcBackup.cleanup();
|
|
1559
|
+
console.log(chalk.gray('Removed temp npm userconfig'));
|
|
1317
1560
|
}
|
|
1318
1561
|
delete process.env.IST_SESSION_TOKEN;
|
|
1562
|
+
activeGrantToken = null;
|
|
1563
|
+
grantEnvActive = false;
|
|
1564
|
+
// Clear the threaded downgrade mode so it never leaks to a later install
|
|
1565
|
+
// (the exec boundary self-resolves again when no context is published).
|
|
1566
|
+
setActiveDowngradeMode(undefined);
|
|
1319
1567
|
};
|
|
1320
1568
|
const onSignal = (signal) => {
|
|
1321
|
-
console.log(chalk.yellow(`\nReceived ${signal} — restoring
|
|
1569
|
+
console.log(chalk.yellow(`\nReceived ${signal} — restoring npm config before exit...`));
|
|
1322
1570
|
cleanup();
|
|
1323
1571
|
// Conventional exit code for signal termination: 128 + signal number
|
|
1324
1572
|
// (SIGINT=2 → 130, SIGTERM=15 → 143).
|
|
@@ -1326,6 +1574,28 @@ export async function installCommand(name, options = {}) {
|
|
|
1326
1574
|
};
|
|
1327
1575
|
process.once('SIGINT', onSignal);
|
|
1328
1576
|
process.once('SIGTERM', onSignal);
|
|
1577
|
+
// grant-unify P1 — re-issue the snapshot grant before a long phase when its
|
|
1578
|
+
// remaining TTL is low (GrantState.ensureFresh). The new token reaches npm and
|
|
1579
|
+
// skit through IST_SESSION_TOKEN (onToken). A re-issue failure: require →
|
|
1580
|
+
// throw (fail-loud; callers inside the install try/finally clean up), prefer →
|
|
1581
|
+
// warn and keep the current token. No-op without an active grant (off mode).
|
|
1582
|
+
const refreshGrant = async (phase) => {
|
|
1583
|
+
if (!grant)
|
|
1584
|
+
return;
|
|
1585
|
+
const st = grant;
|
|
1586
|
+
try {
|
|
1587
|
+
if (await st.ensureFresh()) {
|
|
1588
|
+
console.log(chalk.gray(` ↻ Grant re-issued before ${phase}: ${formatGrantLog(grantMode, st.current)}`));
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
catch (err) {
|
|
1592
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1593
|
+
if (grantMode === 'require') {
|
|
1594
|
+
throw new Error(`Snapshot grant re-issue failed before ${phase}: ${msg}`);
|
|
1595
|
+
}
|
|
1596
|
+
console.log(chalk.yellow(` ⚠ Snapshot grant re-issue failed before ${phase} — keeping the current grant: ${msg}`));
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1329
1599
|
// Determine which installers to use
|
|
1330
1600
|
const effectiveInstallers = { ...DEFAULT_INSTALLERS };
|
|
1331
1601
|
if (useProxy) {
|
|
@@ -1351,11 +1621,56 @@ export async function installCommand(name, options = {}) {
|
|
|
1351
1621
|
console.log(chalk.yellow(`⚠ --pin ignored for package(s) not in this snapshot: ${unmatched.join(', ')}`));
|
|
1352
1622
|
}
|
|
1353
1623
|
const npmEntries = Object.entries(effectiveNpmMap);
|
|
1624
|
+
// ========== Downgrade guard (dev-isolation) ==========
|
|
1625
|
+
// A snapshot bundles OLDER stable versions. On a dev machine whose global
|
|
1626
|
+
// tooling is already NEWER, installing would `npm i -g {pkg}@{pinned}` and
|
|
1627
|
+
// DOWNGRADE it, breaking the environment. Detect at THIS boundary — the map is
|
|
1628
|
+
// now flattened across the whole extends chain (effectiveNpmMap), and NO npm
|
|
1629
|
+
// exec has happened yet (the pre-install batch is below). A clean Docker
|
|
1630
|
+
// sandbox has nothing installed globally → zero offenders → auto-pass, no
|
|
1631
|
+
// config needed. Policy precedence (highest wins): CLI --allow-downgrade / env
|
|
1632
|
+
// SNAPSHOT_ALLOW_LOCAL=1 → machine config → snapshot.policy.onDowngrade →
|
|
1633
|
+
// default 'refuse'. Side effects live in @microwiseai/snapshot-downgrade-guard
|
|
1634
|
+
// via injection (readVersion), keeping that package pure.
|
|
1635
|
+
{
|
|
1636
|
+
const { wouldDowngrade, offenders } = detectDowngrades(effectiveNpmMap, {
|
|
1637
|
+
readVersion: readInstalledNpmVersion,
|
|
1638
|
+
});
|
|
1639
|
+
// Resolve the policy ONCE with full context (incl. snapshot.json policy) and
|
|
1640
|
+
// publish it so the downstream npm-exec boundary (runCommand/runCommandSync)
|
|
1641
|
+
// REUSES it instead of recomputing — one decision for the whole install, and
|
|
1642
|
+
// the boundary won't re-report the same downgrade.
|
|
1643
|
+
const mode = resolveDowngradePolicy({
|
|
1644
|
+
override: readOneShotOverride({
|
|
1645
|
+
env: process.env,
|
|
1646
|
+
allowDowngradeFlag: options.allowDowngrade === true,
|
|
1647
|
+
}),
|
|
1648
|
+
machine: getMachineDowngradeMode(),
|
|
1649
|
+
snapshot: snapshot.policy?.onDowngrade,
|
|
1650
|
+
});
|
|
1651
|
+
setActiveDowngradeMode(mode);
|
|
1652
|
+
if (wouldDowngrade) {
|
|
1653
|
+
if (mode === 'refuse') {
|
|
1654
|
+
// Nothing installed yet — restore .npmrc / unset token, then fail-loud
|
|
1655
|
+
// with a non-zero exit and ZERO partial install.
|
|
1656
|
+
cleanup();
|
|
1657
|
+
console.log('');
|
|
1658
|
+
console.log(chalk.red(formatDowngradeReport(offenders, mode)));
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
else if (mode === 'warn') {
|
|
1662
|
+
console.log('');
|
|
1663
|
+
console.log(chalk.yellow(formatDowngradeReport(offenders, mode)));
|
|
1664
|
+
}
|
|
1665
|
+
// mode === 'allow' → proceed silently.
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1354
1668
|
if (npmEntries.length > 0) {
|
|
1355
1669
|
console.log('');
|
|
1356
1670
|
console.log(chalk.cyan(`► Pre-installing ${npmEntries.length} npm packages from entire extends chain...`));
|
|
1357
1671
|
// Filter out already-installed packages
|
|
1358
1672
|
const toInstall = [];
|
|
1673
|
+
const endProbe = timingStart(`pre-install already-installed probes (${npmEntries.length})`);
|
|
1359
1674
|
for (const [name, version] of npmEntries) {
|
|
1360
1675
|
if (isNpmGlobalInstalled(name, version)) {
|
|
1361
1676
|
console.log(` ${chalk.gray('skip')} ${name}@${version}${chalk.green(' (already installed)')}`);
|
|
@@ -1364,11 +1679,23 @@ export async function installCommand(name, options = {}) {
|
|
|
1364
1679
|
toInstall.push({ name, version });
|
|
1365
1680
|
}
|
|
1366
1681
|
}
|
|
1682
|
+
endProbe();
|
|
1367
1683
|
if (toInstall.length > 0) {
|
|
1368
1684
|
const specs = toInstall.map(e => `${e.name}@${e.version}`);
|
|
1369
1685
|
const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
|
|
1370
|
-
|
|
1371
|
-
|
|
1686
|
+
try {
|
|
1687
|
+
await refreshGrant('pre-install batch');
|
|
1688
|
+
}
|
|
1689
|
+
catch (err) {
|
|
1690
|
+
// Outside the install try/finally: restore .npmrc / token, then fail loud.
|
|
1691
|
+
cleanup();
|
|
1692
|
+
console.log(chalk.red(`⛔ ${err instanceof Error ? err.message : String(err)}`));
|
|
1693
|
+
process.exit(1);
|
|
1694
|
+
}
|
|
1695
|
+
const endBatch = timingStart(`pre-install batch (${specs.length})`);
|
|
1696
|
+
console.log(chalk.cyan(` ► Installing ${specs.length} npm package(s) in one batch… (수십초 걸릴 수 있음, npm 진행이 아래에 표시됩니다)`));
|
|
1697
|
+
const batchSuccess = await runCommand(npmCommand);
|
|
1698
|
+
endBatch();
|
|
1372
1699
|
// S3 — record the up-front batch (the single step where the beta npm tree
|
|
1373
1700
|
// is actually resolved). 'fallback' means it failed and we retried below.
|
|
1374
1701
|
recorder.record({
|
|
@@ -1393,7 +1720,7 @@ export async function installCommand(name, options = {}) {
|
|
|
1393
1720
|
for (const entry of toInstall) {
|
|
1394
1721
|
const pkgSpec = `${entry.name}@${entry.version}`;
|
|
1395
1722
|
const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
|
|
1396
|
-
const ok = runCommand(cmd);
|
|
1723
|
+
const ok = await runCommand(cmd);
|
|
1397
1724
|
recorder.record({
|
|
1398
1725
|
phase: 'pre-install-fallback',
|
|
1399
1726
|
installer: 'npm-proxy',
|
|
@@ -1430,33 +1757,44 @@ export async function installCommand(name, options = {}) {
|
|
|
1430
1757
|
try {
|
|
1431
1758
|
// Auto-detect 모드: snapshot.install이 없으면 자동 판별
|
|
1432
1759
|
const autoDetect = !snapshot.install;
|
|
1760
|
+
const endLayers = timingStart('installSnapshot (layers + kits)');
|
|
1433
1761
|
const installResult = await installSnapshot(snapshot, 0, effectiveInstallers, {
|
|
1434
1762
|
useProxy,
|
|
1435
1763
|
autoDetect,
|
|
1436
1764
|
sessionId,
|
|
1437
1765
|
recorder,
|
|
1438
1766
|
overrides,
|
|
1767
|
+
...(grant ? { beforePhase: refreshGrant, skitSessionId: () => activeGrantToken ?? undefined } : {}),
|
|
1439
1768
|
});
|
|
1440
1769
|
result = installResult.result;
|
|
1770
|
+
endLayers();
|
|
1441
1771
|
// ===== S1/FB — verify (and optionally converge) BEFORE cleanup =====
|
|
1442
1772
|
// SSOT enforcement: the manifest pins (with FB overrides applied) are the
|
|
1443
1773
|
// truth. Check the disk actually matches. This MUST run before the finally
|
|
1444
1774
|
// restores .npmrc, because re-pinning via `npm install -g` needs the
|
|
1445
1775
|
// registry-proxy config + token still in place.
|
|
1446
1776
|
if (Object.keys(effectiveNpmMap).length > 0) {
|
|
1447
|
-
// Re-pin
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1777
|
+
// Re-pin ALL diverged packages in a round with a SINGLE `npm install -g`
|
|
1778
|
+
// (batched) and record it as one ledger step carrying every spec. This
|
|
1779
|
+
// collapses N sequential global installs per round into one npm spawn.
|
|
1780
|
+
const repinMany = (phase) => (pkgs) => {
|
|
1781
|
+
if (pkgs.length === 0)
|
|
1782
|
+
return;
|
|
1783
|
+
const specs = pkgs.map((p) => `${p.name}@${p.version}`);
|
|
1784
|
+
const cmd = `npm install -g ${specs.join(' ')}`;
|
|
1785
|
+
console.log(chalk.gray(` ► Re-pinning ${specs.length} package(s) in one batch (npm install -g … — 수십초 걸릴 수 있음)`));
|
|
1786
|
+
// Sync streaming: invoked from the pure/sync runConverge callback.
|
|
1787
|
+
const ok = runCommandSync(cmd);
|
|
1788
|
+
recorder.record({ phase, installer: 'npm-proxy', command: cmd, specs, result: ok ? 'ok' : 'failed' });
|
|
1453
1789
|
};
|
|
1790
|
+
await refreshGrant('verify/converge');
|
|
1791
|
+
const endVerify = timingStart('verify/converge');
|
|
1454
1792
|
if (converge) {
|
|
1455
1793
|
// Default: re-pin EVERY package that diverged from its pin (self-healing).
|
|
1456
1794
|
console.log('');
|
|
1457
1795
|
console.log(chalk.cyan('► Verifying & converging install to manifest pins...'));
|
|
1458
1796
|
verification = runConverge(allNpmPackages, overrides, {
|
|
1459
|
-
|
|
1797
|
+
installMany: repinMany('converge'),
|
|
1460
1798
|
readVersion: readInstalledNpmVersion,
|
|
1461
1799
|
maxRounds: 3,
|
|
1462
1800
|
});
|
|
@@ -1471,13 +1809,14 @@ export async function installCommand(name, options = {}) {
|
|
|
1471
1809
|
console.log('');
|
|
1472
1810
|
console.log(chalk.cyan(`► Holding ${Object.keys(pinTargets).length} pinned package(s) to the override version...`));
|
|
1473
1811
|
runConverge(pinTargets, {}, {
|
|
1474
|
-
|
|
1812
|
+
installMany: repinMany('pin-converge'),
|
|
1475
1813
|
readVersion: readInstalledNpmVersion,
|
|
1476
1814
|
maxRounds: 3,
|
|
1477
1815
|
});
|
|
1478
1816
|
}
|
|
1479
1817
|
verification = verifyInstallMap(effectiveNpmMap, { readVersion: readInstalledNpmVersion });
|
|
1480
1818
|
}
|
|
1819
|
+
endVerify();
|
|
1481
1820
|
}
|
|
1482
1821
|
}
|
|
1483
1822
|
catch (err) {
|
|
@@ -1569,6 +1908,16 @@ export async function installCommand(name, options = {}) {
|
|
|
1569
1908
|
// verification shape { ok, verifiedCount, total, mismatches[{name,expected,got}] }.
|
|
1570
1909
|
if (verification)
|
|
1571
1910
|
ledger.verification = verification;
|
|
1911
|
+
// grant-unify P1 — grant provenance (no token). Absent in off mode.
|
|
1912
|
+
if (grant) {
|
|
1913
|
+
const st = grant;
|
|
1914
|
+
ledger.grant = {
|
|
1915
|
+
mode: grantMode,
|
|
1916
|
+
closureHash: st.current.closureHash.slice(0, 8),
|
|
1917
|
+
memberCount: st.current.members.length,
|
|
1918
|
+
reissues: st.reissues,
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1572
1921
|
// AC1.4 — fold the core-floor verdict too.
|
|
1573
1922
|
ledger.coreFloor = {
|
|
1574
1923
|
ok: coreFloor.ok,
|