@microwiseai/snapshot 0.3.46 → 0.3.57

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.
Files changed (53) hide show
  1. package/dist/commands/access.js +1 -1
  2. package/dist/commands/access.js.map +1 -1
  3. package/dist/commands/doctor.d.ts +2 -0
  4. package/dist/commands/doctor.d.ts.map +1 -0
  5. package/dist/commands/doctor.js +37 -0
  6. package/dist/commands/doctor.js.map +1 -0
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +335 -112
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/setup.d.ts +5 -0
  11. package/dist/commands/setup.d.ts.map +1 -0
  12. package/dist/commands/setup.js +52 -0
  13. package/dist/commands/setup.js.map +1 -0
  14. package/dist/index.js +16 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/access-sync.d.ts.map +1 -1
  17. package/dist/lib/access-sync.js +2 -0
  18. package/dist/lib/access-sync.js.map +1 -1
  19. package/dist/lib/config.d.ts +15 -0
  20. package/dist/lib/config.d.ts.map +1 -1
  21. package/dist/lib/config.js +96 -0
  22. package/dist/lib/config.js.map +1 -1
  23. package/dist/lib/gitlab.js +3 -3
  24. package/dist/lib/gitlab.js.map +1 -1
  25. package/dist/lib/prerequisites.d.ts +33 -0
  26. package/dist/lib/prerequisites.d.ts.map +1 -0
  27. package/dist/lib/prerequisites.js +109 -0
  28. package/dist/lib/prerequisites.js.map +1 -0
  29. package/dist/lib/session.d.ts +6 -8
  30. package/dist/lib/session.d.ts.map +1 -1
  31. package/dist/lib/session.js +10 -12
  32. package/dist/lib/session.js.map +1 -1
  33. package/dist/lib/types.d.ts +1 -1
  34. package/dist/lib/types.d.ts.map +1 -1
  35. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.d.ts +108 -0
  36. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.d.ts.map +1 -0
  37. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.js +196 -0
  38. package/node_modules/@microwiseai/snapshot-dedup/dist/dedup.js.map +1 -0
  39. package/node_modules/@microwiseai/snapshot-dedup/dist/index.d.ts +21 -0
  40. package/node_modules/@microwiseai/snapshot-dedup/dist/index.d.ts.map +1 -0
  41. package/node_modules/@microwiseai/snapshot-dedup/dist/index.js +27 -0
  42. package/node_modules/@microwiseai/snapshot-dedup/dist/index.js.map +1 -0
  43. package/node_modules/@microwiseai/snapshot-dedup/package.json +41 -0
  44. package/node_modules/@microwiseai/snapshot-parallel/dist/index.d.ts +9 -0
  45. package/node_modules/@microwiseai/snapshot-parallel/dist/index.d.ts.map +1 -0
  46. package/node_modules/@microwiseai/snapshot-parallel/dist/index.js +8 -0
  47. package/node_modules/@microwiseai/snapshot-parallel/dist/index.js.map +1 -0
  48. package/node_modules/@microwiseai/snapshot-parallel/dist/parallel-installer.d.ts +86 -0
  49. package/node_modules/@microwiseai/snapshot-parallel/dist/parallel-installer.d.ts.map +1 -0
  50. package/node_modules/@microwiseai/snapshot-parallel/dist/parallel-installer.js +159 -0
  51. package/node_modules/@microwiseai/snapshot-parallel/dist/parallel-installer.js.map +1 -0
  52. package/node_modules/@microwiseai/snapshot-parallel/package.json +41 -0
  53. package/package.json +7 -3
@@ -5,7 +5,7 @@ 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, REGISTRY_PROXY_URL, VENDOR_NAME, PACKAGE_NAME, getIstCredentials, LICENSE_API_URL, verifyLicenseKey, PUBLIC_PACKAGES } from '../lib/config.js';
8
+ import { getLicenseKey, getProxyAuthToken, exchangeSessionToken, REGISTRY_PROXY_URL, VENDOR_NAME, PACKAGE_NAME, getIstCredentials, LICENSE_API_URL, verifyLicenseKey, PUBLIC_PACKAGES } from '../lib/config.js';
9
9
  import { resolveSnapshotDependencies } from '../lib/package-resolver.js';
10
10
  // transitive-resolver removed — npm handles dependency resolution automatically
11
11
  import { installSkitPackage } from '../lib/skit-adapter.js';
@@ -79,7 +79,7 @@ const DEFAULT_INSTALLERS = {
79
79
  /**
80
80
  * Configure .npmrc for registry-proxy with session token
81
81
  */
82
- function configureNpmrcForProxy(scopes, licenseKey) {
82
+ function configureNpmrcForProxy(scopes) {
83
83
  const npmrcPath = join(homedir(), '.npmrc');
84
84
  let originalContent = null;
85
85
  // Backup existing .npmrc
@@ -105,8 +105,8 @@ function configureNpmrcForProxy(scopes, licenseKey) {
105
105
  for (const scope of scopes) {
106
106
  lines.push(`${scope}:registry=${REGISTRY_PROXY_URL}/`);
107
107
  }
108
- // Add auth token for proxy
109
- lines.push(`//${proxyHost}/:_authToken=${licenseKey}`);
108
+ // 환경변수 참조 토큰 직접 안 넣음 (npm이 ${ENV_VAR} 구문을 해석)
109
+ lines.push(`//${proxyHost}/:_authToken=\${IST_SESSION_TOKEN}`);
110
110
  writeFileSync(npmrcPath, lines.join('\n') + '\n');
111
111
  return { path: npmrcPath, content: originalContent };
112
112
  }
@@ -324,33 +324,73 @@ async function installPackagesByType(packages, installerName, installers, snapsh
324
324
  return result;
325
325
  }
326
326
  }
327
- // Install packages in parallel
327
+ // Install packages
328
328
  const isNpmInstaller = installerName === 'npm-proxy' || installerConfig.command.includes('npm install');
329
329
  const entries = recordToEntries(packages);
330
- const parallelResult = await parallelInstall(entries, async (entry) => {
331
- // Skip if already installed with same version (for npm global packages)
332
- if (isNpmInstaller && isNpmGlobalInstalled(entry.name, entry.version)) {
333
- return { success: true };
334
- }
335
- const pkgSpec = `${entry.name}@${entry.version}`;
336
- const command = installerConfig.command.replace('{pkg}', pkgSpec);
337
- const success = runCommand(command);
338
- return { success, error: success ? undefined : `Command failed: ${command}` };
339
- }, {
340
- concurrency: 4,
341
- onProgress: (event) => {
342
- const { result: r, current, total: t } = event;
343
- const progress = `[${current}/${t}]`;
344
- if (r.success) {
345
- console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.green(' ✓')}`);
330
+ if (isNpmInstaller) {
331
+ // Batch install: collect all not-yet-installed packages and run a single npm install -g
332
+ const toInstall = [];
333
+ for (const entry of entries) {
334
+ if (isNpmGlobalInstalled(entry.name, entry.version)) {
335
+ console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
336
+ result.installed.push(`${entry.name}@${entry.version}`);
346
337
  }
347
338
  else {
348
- console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.red(' ✗')}`);
339
+ toInstall.push(entry);
349
340
  }
350
- },
351
- });
352
- result.installed.push(...parallelResult.installed);
353
- result.failed.push(...parallelResult.failed);
341
+ }
342
+ if (toInstall.length > 0) {
343
+ const specs = toInstall.map(e => `${e.name}@${e.version}`);
344
+ const batchCommand = installerConfig.command.replace('{pkg}', specs.join(' '));
345
+ console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
346
+ const success = runCommand(batchCommand);
347
+ if (success) {
348
+ for (const spec of specs) {
349
+ console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
350
+ }
351
+ result.installed.push(...specs);
352
+ }
353
+ else {
354
+ console.log(chalk.red(`${indent} Batch install failed, falling back to individual installs...`));
355
+ for (const entry of toInstall) {
356
+ const pkgSpec = `${entry.name}@${entry.version}`;
357
+ const command = installerConfig.command.replace('{pkg}', pkgSpec);
358
+ const ok = runCommand(command);
359
+ if (ok) {
360
+ console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
361
+ result.installed.push(pkgSpec);
362
+ }
363
+ else {
364
+ console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.red(' ✗')}`);
365
+ result.failed.push(pkgSpec);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ else {
372
+ // Non-npm installer: use parallel install as before
373
+ const parallelResult = await parallelInstall(entries, async (entry) => {
374
+ const pkgSpec = `${entry.name}@${entry.version}`;
375
+ const command = installerConfig.command.replace('{pkg}', pkgSpec);
376
+ const success = runCommand(command);
377
+ return { success, error: success ? undefined : `Command failed: ${command}` };
378
+ }, {
379
+ concurrency: 4,
380
+ onProgress: (event) => {
381
+ const { result: r, current, total: t } = event;
382
+ const progress = `[${current}/${t}]`;
383
+ if (r.success) {
384
+ console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.green(' ✓')}`);
385
+ }
386
+ else {
387
+ console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.red(' ✗')}`);
388
+ }
389
+ },
390
+ });
391
+ result.installed.push(...parallelResult.installed);
392
+ result.failed.push(...parallelResult.failed);
393
+ }
354
394
  return result;
355
395
  }
356
396
  export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}, options = {}) {
@@ -539,29 +579,74 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
539
579
  continue;
540
580
  }
541
581
  }
542
- // Install packages in parallel
582
+ // Install packages
543
583
  const installEntries = recordToEntries(packages);
544
584
  const installCfg = installerConfig; // capture for closure
545
- const parallelResult = await parallelInstall(installEntries, async (entry) => {
546
- const pkgSpec = `${entry.name}@${entry.version}`;
547
- const command = installCfg.command.replace('{pkg}', pkgSpec);
548
- const success = runCommand(command);
549
- return { success, error: success ? undefined : `Command failed: ${command}` };
550
- }, {
551
- concurrency: 4,
552
- onProgress: (event) => {
553
- const { result: r, current, total: t } = event;
554
- const progress = `[${current}/${t}]`;
555
- if (r.success) {
556
- console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.green(' ✓')}`);
585
+ const isNpmType = installerName === 'npm-proxy' || installCfg.command.includes('npm install');
586
+ if (isNpmType) {
587
+ // Batch install: collect all not-yet-installed packages and run a single npm install -g
588
+ const toInstall = [];
589
+ for (const entry of installEntries) {
590
+ if (isNpmGlobalInstalled(entry.name, entry.version)) {
591
+ console.log(`${indent}${chalk.gray('skip')} ${entry.name}@${entry.version}${chalk.green(' (already installed)')}`);
592
+ result.installed.push(`${entry.name}@${entry.version}`);
557
593
  }
558
594
  else {
559
- console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.red(' ✗')}`);
595
+ toInstall.push(entry);
560
596
  }
561
- },
562
- });
563
- result.installed.push(...parallelResult.installed);
564
- result.failed.push(...parallelResult.failed);
597
+ }
598
+ if (toInstall.length > 0) {
599
+ const specs = toInstall.map(e => `${e.name}@${e.version}`);
600
+ const batchCommand = installCfg.command.replace('{pkg}', specs.join(' '));
601
+ console.log(chalk.gray(`${indent} Running batch: npm install -g (${specs.length} packages)`));
602
+ const success = runCommand(batchCommand);
603
+ if (success) {
604
+ for (const spec of specs) {
605
+ console.log(`${indent}${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
606
+ }
607
+ result.installed.push(...specs);
608
+ }
609
+ else {
610
+ console.log(chalk.red(`${indent} Batch install failed, falling back to individual installs...`));
611
+ for (const entry of toInstall) {
612
+ const pkgSpec = `${entry.name}@${entry.version}`;
613
+ const command = installCfg.command.replace('{pkg}', pkgSpec);
614
+ const ok = runCommand(command);
615
+ if (ok) {
616
+ console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
617
+ result.installed.push(pkgSpec);
618
+ }
619
+ else {
620
+ console.log(`${indent}${chalk.blue('fallback')} ${pkgSpec}...${chalk.red(' ✗')}`);
621
+ result.failed.push(pkgSpec);
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ else {
628
+ // Non-npm installer: use parallel install as before
629
+ const parallelResult = await parallelInstall(installEntries, async (entry) => {
630
+ const pkgSpec = `${entry.name}@${entry.version}`;
631
+ const command = installCfg.command.replace('{pkg}', pkgSpec);
632
+ const success = runCommand(command);
633
+ return { success, error: success ? undefined : `Command failed: ${command}` };
634
+ }, {
635
+ concurrency: 4,
636
+ onProgress: (event) => {
637
+ const { result: r, current, total: t } = event;
638
+ const progress = `[${current}/${t}]`;
639
+ if (r.success) {
640
+ console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.green(' ✓')}`);
641
+ }
642
+ else {
643
+ console.log(`${indent}${chalk.blue(progress)} ${r.spec}...${chalk.red(' ✗')}`);
644
+ }
645
+ },
646
+ });
647
+ result.installed.push(...parallelResult.installed);
648
+ result.failed.push(...parallelResult.failed);
649
+ }
565
650
  }
566
651
  }
567
652
  // Legacy: packages (use glpkg) - parallel
@@ -619,6 +704,55 @@ export async function installSnapshot(snapshot, depth = 0, parentInstallers = {}
619
704
  }
620
705
  return { result, installers };
621
706
  }
707
+ /**
708
+ * Recursively collect all npm-type packages from snapshot and its extends chain.
709
+ * npm-type = installer is 'npm-proxy' OR command contains 'npm install'.
710
+ * When useProxy=true, glpkg/gitlab-install are also treated as npm (effectiveInstallers override).
711
+ * Skips skit packages. Returns Record<name, version>.
712
+ */
713
+ async function collectAllNpmPackages(snapshot, useProxy = true) {
714
+ const npmPackages = {};
715
+ // Helper: is this installer name npm-type?
716
+ const isNpmInstaller = (installerName) => {
717
+ if (installerName === 'skit')
718
+ return false;
719
+ if (installerName === 'npm-proxy')
720
+ return true;
721
+ if (useProxy && (installerName === 'glpkg' || installerName === 'gitlab-install'))
722
+ return true;
723
+ const cfg = snapshot.installers?.[installerName] || DEFAULT_INSTALLERS[installerName];
724
+ return cfg ? cfg.command.includes('npm install') : false;
725
+ };
726
+ // Collect from snapshot.install
727
+ if (snapshot.install) {
728
+ for (const [installerName, packages] of Object.entries(snapshot.install)) {
729
+ if (isNpmInstaller(installerName)) {
730
+ Object.assign(npmPackages, packages);
731
+ }
732
+ }
733
+ }
734
+ // Legacy: snapshot.packages uses glpkg → npm when useProxy
735
+ if (useProxy && snapshot.packages) {
736
+ Object.assign(npmPackages, snapshot.packages);
737
+ }
738
+ // Recurse into extends chain
739
+ if (snapshot.extends) {
740
+ try {
741
+ const baseSnapshot = await fetchSnapshotFromRepo(snapshot.extends);
742
+ const baseNpmPackages = await collectAllNpmPackages(baseSnapshot, useProxy);
743
+ // Base packages: don't override current snapshot's versions
744
+ for (const [name, version] of Object.entries(baseNpmPackages)) {
745
+ if (!(name in npmPackages)) {
746
+ npmPackages[name] = version;
747
+ }
748
+ }
749
+ }
750
+ catch {
751
+ // If base snapshot fetch fails, continue with what we have.
752
+ }
753
+ }
754
+ return npmPackages;
755
+ }
622
756
  /**
623
757
  * Recursively collect all packages from snapshot and its extends chain.
624
758
  * This ensures .npmrc is configured with ALL scopes before installation begins,
@@ -658,51 +792,6 @@ export async function installCommand(name) {
658
792
  ║ Snapshot Installer ║
659
793
  ╚══════════════════════════════════════════════════════════════╝
660
794
  `));
661
- // ========== IST Login Check ==========
662
- // Public packages can be installed without login (defined in config.ts)
663
- // Strip version from name for comparison (e.g., @ist/minimal@0.6.9 -> @ist/minimal)
664
- const packageNameOnly = name.includes('@', 1) ? name.substring(0, name.lastIndexOf('@')) : name;
665
- const isPublicPackage = PUBLIC_PACKAGES.includes(packageNameOnly);
666
- const istCredentials = getIstCredentials();
667
- if (istCredentials?.userId) {
668
- console.log(chalk.green('✓') + ' IST login verified');
669
- }
670
- else if (isPublicPackage) {
671
- console.log(chalk.yellow('⚠') + ' No IST login - installing public package');
672
- }
673
- else {
674
- console.log(chalk.red('⛔ IST login required for non-public packages'));
675
- console.log(chalk.gray(' Run: ist auth login'));
676
- console.log(chalk.gray(' Or install @ist/minimal first (no login required)'));
677
- process.exit(1);
678
- }
679
- // ======================================
680
- // ========== License Check ==========
681
- // Use local license key storage (from 'snapshot license activate')
682
- const storedLicenseKey = getLicenseKey();
683
- let licenseValid = false;
684
- if (storedLicenseKey) {
685
- try {
686
- const verifyResult = await verifyLicenseKey(storedLicenseKey);
687
- if (verifyResult.valid) {
688
- licenseValid = true;
689
- markLicenseChecked(PACKAGE_NAME);
690
- console.log(chalk.green('✓') + ' License verified');
691
- }
692
- else {
693
- console.log(chalk.yellow('⚠') + ' License invalid - trying anonymous access...');
694
- }
695
- }
696
- catch {
697
- console.log(chalk.yellow('⚠') + ' License check failed - trying anonymous access...');
698
- }
699
- }
700
- else {
701
- // No license key - try anonymous access (public packages will work via registry-proxy)
702
- console.log(chalk.yellow('⚠') + ' No license - trying anonymous access...');
703
- console.log(chalk.gray(' (Public packages will install without license)'));
704
- }
705
- // ====================================
706
795
  // Validate name
707
796
  let repoName;
708
797
  try {
@@ -719,16 +808,32 @@ export async function installCommand(name) {
719
808
  }
720
809
  console.log(chalk.gray(`Snapshot: ${name}`));
721
810
  console.log(chalk.gray(`Repo: ${repoName}`));
722
- // Check auth: license key for registry-proxy
811
+ // Check auth: exchange fresh session token
723
812
  const licenseKey = getLicenseKey();
724
- const useProxy = true; // Always use registry-proxy
813
+ let sessionToken = null;
725
814
  if (licenseKey) {
726
- console.log(chalk.green('✓') + ' Using registry-proxy with license key');
815
+ sessionToken = licenseKey;
816
+ console.log(chalk.green('✓') + ' Using license key');
727
817
  }
728
818
  else {
729
- console.log(chalk.yellow('⚠') + ' No license key - trying anonymous access');
819
+ // Fresh token 교환 시도
820
+ sessionToken = await exchangeSessionToken();
821
+ if (sessionToken) {
822
+ console.log(chalk.green('✓') + ' Session token acquired');
823
+ }
824
+ else {
825
+ // 기존 토큰 폴백
826
+ sessionToken = getProxyAuthToken();
827
+ if (sessionToken) {
828
+ console.log(chalk.green('✓') + ' Using existing session token');
829
+ }
830
+ else {
831
+ console.log(chalk.yellow('⚠') + ' No auth token - trying anonymous access');
832
+ }
833
+ }
730
834
  }
731
- // Fetch snapshot
835
+ const useProxy = true;
836
+ // Fetch snapshot first — access check depends on snapshot.json content
732
837
  console.log(chalk.gray('Fetching snapshot...'));
733
838
  let snapshot;
734
839
  try {
@@ -744,15 +849,64 @@ export async function installCommand(name) {
744
849
  if (snapshot.description) {
745
850
  console.log(chalk.gray(` ${snapshot.description}`));
746
851
  }
747
- // ========== Transitive Resolution REMOVED ==========
748
- // npm install -g handles dependency resolution automatically.
749
- // Transitive resolution was incorrectly installing internal deps
750
- // as separate global packages (e.g., 7 CLI packages → 37 packages).
751
- // Now we only install the packages explicitly listed in snapshot.json.
752
- // =====================================================
753
- // ========== Snapshot Access Check ==========
754
- if (snapshot.access && typeof snapshot.access === 'object' && snapshot.access.type === 'licensed') {
755
- const productId = snapshot.access.productId;
852
+ // ========== Access Check (based on snapshot.json access field) ==========
853
+ // Determine access type from snapshot.json:
854
+ // - access: "public" → no login/license required
855
+ // - access: "authenticated" → IST login required, no license needed
856
+ // - access: { type: "licensed", productId } license required
857
+ // - access field absent → fallback to PUBLIC_PACKAGES for backward compat
858
+ const packageNameOnly = name.includes('@', 1) ? name.substring(0, name.lastIndexOf('@')) : name;
859
+ const accessField = snapshot.access;
860
+ const isPublicAccess = accessField === 'public'
861
+ || (!accessField && PUBLIC_PACKAGES.includes(packageNameOnly)); // fallback for old snapshots without access field
862
+ const isAuthenticatedAccess = accessField === 'authenticated';
863
+ const isLicensedAccess = !!accessField && typeof accessField === 'object' && accessField.type === 'licensed';
864
+ // --- IST Login Check ---
865
+ const istCredentials = getIstCredentials();
866
+ if (istCredentials?.userId) {
867
+ console.log(chalk.green('✓') + ' IST login verified');
868
+ }
869
+ else if (isPublicAccess) {
870
+ console.log(chalk.yellow('⚠') + ' No IST login - installing public package');
871
+ }
872
+ else {
873
+ console.log(chalk.red('⛔ IST login required for non-public packages'));
874
+ console.log(chalk.gray(' Run: ist auth login'));
875
+ console.log(chalk.gray(' Or install a public snapshot first (no login required)'));
876
+ process.exit(1);
877
+ }
878
+ // --- License Check ---
879
+ const storedLicenseKey = getLicenseKey();
880
+ let licenseValid = false;
881
+ if (isPublicAccess || isAuthenticatedAccess) {
882
+ // Public and authenticated snapshots — skip license check entirely
883
+ const label = isAuthenticatedAccess ? 'Authenticated' : 'Public';
884
+ console.log(chalk.green('✓') + ` ${label} snapshot - no license required`);
885
+ }
886
+ else if (storedLicenseKey) {
887
+ try {
888
+ const verifyResult = await verifyLicenseKey(storedLicenseKey);
889
+ if (verifyResult.valid) {
890
+ licenseValid = true;
891
+ markLicenseChecked(PACKAGE_NAME);
892
+ console.log(chalk.green('✓') + ' License verified');
893
+ }
894
+ else {
895
+ console.log(chalk.yellow('⚠') + ' License invalid - trying anonymous access...');
896
+ }
897
+ }
898
+ catch {
899
+ console.log(chalk.yellow('⚠') + ' License check failed - trying anonymous access...');
900
+ }
901
+ }
902
+ else {
903
+ // No license key - try anonymous access
904
+ console.log(chalk.yellow('⚠') + ' No license - trying anonymous access...');
905
+ console.log(chalk.gray(' (Public packages will install without license)'));
906
+ }
907
+ // --- Licensed Snapshot Gate ---
908
+ if (isLicensedAccess) {
909
+ const productId = accessField.productId;
756
910
  if (!licenseValid) {
757
911
  console.log('');
758
912
  console.log(chalk.red('⛔ This snapshot requires a license'));
@@ -762,11 +916,10 @@ export async function installCommand(name) {
762
916
  }
763
917
  console.log(chalk.green('✓') + ` License verified for ${productId}`);
764
918
  }
765
- // access가 없거나 'public'이면 그냥 통과
766
- // ============================================
919
+ // =======================================================================
767
920
  // ========== Authorize Session with Registry Proxy ==========
768
921
  let sessionId;
769
- if (licenseKey) {
922
+ if (licenseKey || sessionToken) {
770
923
  // Collect all packages for session authorization
771
924
  const sessionPackages = [];
772
925
  // From snapshot.install
@@ -785,17 +938,20 @@ export async function installCommand(name) {
785
938
  }
786
939
  // Transitive deps removed — npm handles dependency resolution automatically
787
940
  if (sessionPackages.length > 0) {
941
+ // Determine auth token: licenseKey takes priority, fallback to sessionToken
942
+ const authToken = licenseKey || sessionToken;
788
943
  console.log(chalk.gray(`Authorizing session for ${sessionPackages.length} packages...`));
789
944
  console.log(chalk.gray(` Packages: ${sessionPackages.join(', ')}`));
945
+ console.log(chalk.gray(` Auth type: ${licenseKey ? 'license key' : 'session token'}`));
790
946
  try {
791
- sessionId = await authorizeSession(licenseKey, name, sessionPackages);
947
+ sessionId = await authorizeSession(authToken, name, sessionPackages);
792
948
  console.log(chalk.green('✓') + ' Session authorized: ' + sessionId);
793
949
  }
794
950
  catch (error) {
795
951
  const errMsg = error instanceof Error ? error.message : String(error);
796
- // Session authorization failure is not fatal - fallback to license key auth
952
+ // Session authorization failure is not fatal - fallback to token auth
797
953
  console.log(chalk.yellow('⚠') + ` Session authorization failed: ${errMsg}`);
798
- console.log(chalk.gray(' Falling back to license key authentication...'));
954
+ console.log(chalk.gray(' Falling back to token authentication...'));
799
955
  }
800
956
  }
801
957
  }
@@ -807,12 +963,14 @@ export async function installCommand(name) {
807
963
  if (useProxy) {
808
964
  const scopes = extractScopes(allPackages);
809
965
  if (scopes.length > 0) {
810
- if (licenseKey) {
966
+ if (sessionToken) {
967
+ // 환경변수에 설정 (npm이 .npmrc의 ${IST_SESSION_TOKEN}을 해석)
968
+ process.env.IST_SESSION_TOKEN = sessionToken;
811
969
  console.log(chalk.gray(`Configuring registry-proxy for scopes: ${scopes.join(', ')}`));
812
- npmrcBackup = configureNpmrcForProxy(scopes, licenseKey);
970
+ npmrcBackup = configureNpmrcForProxy(scopes);
813
971
  }
814
972
  else {
815
- // Anonymous mode - configure proxy without auth token
973
+ // No auth token at all anonymous mode (public packages only)
816
974
  console.log(chalk.gray(`Configuring registry-proxy (anonymous) for scopes: ${scopes.join(', ')}`));
817
975
  npmrcBackup = configureNpmrcForProxyAnonymous(scopes);
818
976
  }
@@ -825,7 +983,58 @@ export async function installCommand(name) {
825
983
  effectiveInstallers['glpkg'] = DEFAULT_INSTALLERS['npm-proxy'];
826
984
  effectiveInstallers['gitlab-install'] = DEFAULT_INSTALLERS['npm-proxy'];
827
985
  }
986
+ // ========== Pre-install: batch all npm packages from extends chain ==========
987
+ // Collect all npm-type packages across the entire extends chain and install
988
+ // them in a single `npm install -g` call, instead of one per snapshot layer.
989
+ const allNpmPackages = await collectAllNpmPackages(snapshot, useProxy);
990
+ const npmEntries = Object.entries(allNpmPackages);
991
+ if (npmEntries.length > 0) {
992
+ console.log('');
993
+ console.log(chalk.cyan(`► Pre-installing ${npmEntries.length} npm packages from entire extends chain...`));
994
+ // Filter out already-installed packages
995
+ const toInstall = [];
996
+ for (const [name, version] of npmEntries) {
997
+ if (isNpmGlobalInstalled(name, version)) {
998
+ console.log(` ${chalk.gray('skip')} ${name}@${version}${chalk.green(' (already installed)')}`);
999
+ }
1000
+ else {
1001
+ toInstall.push({ name, version });
1002
+ }
1003
+ }
1004
+ if (toInstall.length > 0) {
1005
+ const specs = toInstall.map(e => `${e.name}@${e.version}`);
1006
+ const npmCommand = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', specs.join(' '));
1007
+ console.log(chalk.gray(` Running single batch: npm install -g (${specs.length} packages)`));
1008
+ const batchSuccess = runCommand(npmCommand);
1009
+ if (batchSuccess) {
1010
+ for (const spec of specs) {
1011
+ console.log(` ${chalk.blue('batch')} ${spec}...${chalk.green(' ✓')}`);
1012
+ }
1013
+ console.log(chalk.green(`✓ Pre-installed ${specs.length} npm packages in one batch`));
1014
+ }
1015
+ else {
1016
+ // Batch failed - fall back to individual installs
1017
+ console.log(chalk.yellow(' Batch install failed, falling back to individual installs...'));
1018
+ for (const entry of toInstall) {
1019
+ const pkgSpec = `${entry.name}@${entry.version}`;
1020
+ const cmd = DEFAULT_INSTALLERS['npm-proxy'].command.replace('{pkg}', pkgSpec);
1021
+ const ok = runCommand(cmd);
1022
+ if (ok) {
1023
+ console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.green(' ✓')}`);
1024
+ }
1025
+ else {
1026
+ console.log(` ${chalk.blue('fallback')} ${pkgSpec}...${chalk.red(' ✗')}`);
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ else {
1032
+ console.log(chalk.green('✓ All npm packages already installed'));
1033
+ }
1034
+ }
1035
+ // =============================================================================
828
1036
  // Install (handles extends recursively)
1037
+ // installSnapshot's internal isNpmGlobalInstalled checks will skip already-installed packages
829
1038
  let installError = null;
830
1039
  let result = { installed: [], failed: [] };
831
1040
  try {
@@ -847,6 +1056,8 @@ export async function installCommand(name) {
847
1056
  restoreNpmrc(npmrcBackup);
848
1057
  console.log(chalk.gray('Restored .npmrc'));
849
1058
  }
1059
+ // 환경변수 정리
1060
+ delete process.env.IST_SESSION_TOKEN;
850
1061
  }
851
1062
  if (installError) {
852
1063
  console.log(chalk.red(`Error during installation: ${installError.message}`));
@@ -879,6 +1090,18 @@ export async function installCommand(name) {
879
1090
  console.log('');
880
1091
  console.log(chalk.green('✓') + ' Snapshot info saved');
881
1092
  }
1093
+ // Post-install: run doctor to check prerequisites
1094
+ console.log('');
1095
+ const { checkPrerequisites, MINIMAL_PREREQUISITES } = await import('../lib/prerequisites.js');
1096
+ const prereqResults = checkPrerequisites(MINIMAL_PREREQUISITES);
1097
+ const missingPrereqs = prereqResults.filter(r => !r.installed);
1098
+ if (missingPrereqs.length > 0) {
1099
+ console.log(chalk.yellow(`\n⚠ ${missingPrereqs.length} prerequisite(s) not satisfied:`));
1100
+ for (const r of missingPrereqs) {
1101
+ console.log(` ${chalk.red('❌')} ${r.name} — required by ${r.requiredBy.join(', ')}`);
1102
+ }
1103
+ console.log(chalk.gray(`\nRun 'snapshot setup' to install, or 'snapshot doctor' for details.`));
1104
+ }
882
1105
  // Send telemetry (fire and forget)
883
1106
  sendInstallTelemetry({
884
1107
  package: snapshot.name,