@jagilber-org/index-server 1.26.5 → 1.26.11

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.
@@ -20,6 +20,7 @@
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
22
  import os from 'os';
23
+ import { execFileSync } from 'child_process';
23
24
  import { fileURLToPath } from 'url';
24
25
  import { select, input, confirm, checkbox } from '@inquirer/prompts';
25
26
 
@@ -27,12 +28,90 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
28
  const ROOT = path.resolve(__dirname, '..');
28
29
  const IS_WINDOWS = process.platform === 'win32';
29
30
 
31
+ // --------------------------------------------------------------------------
32
+ // Launch spec resolver — determines how to invoke index-server at runtime.
33
+ //
34
+ // Returns { command, args, cwd, source } where source indicates the mode:
35
+ // 'local' — dist/ found at config.root (dev checkout)
36
+ // 'packaged' — dist/ found in the package ROOT but not config.root (npx install)
37
+ // 'npx' — fallback when no dist/ found anywhere
38
+ // --------------------------------------------------------------------------
39
+ function resolveServerLaunch(config) {
40
+ const entryRelative = 'dist/server/index-server.js';
41
+ const localEntry = path.join(config.root, entryRelative);
42
+ const packagedEntry = path.join(ROOT, entryRelative);
43
+
44
+ // Case 1: config.root is the repo checkout with dist/ present
45
+ if (fs.existsSync(localEntry)) {
46
+ return {
47
+ command: 'node',
48
+ args: [entryRelative],
49
+ cwd: config.root,
50
+ source: 'local',
51
+ };
52
+ }
53
+
54
+ // Case 2: dist/ exists in the package root (npx cache) but not in config.root
55
+ if (fs.existsSync(packagedEntry)) {
56
+ return {
57
+ command: 'node',
58
+ args: [fwd(packagedEntry)],
59
+ cwd: config.root,
60
+ source: 'packaged',
61
+ };
62
+ }
63
+
64
+ // Case 3: no dist/ anywhere — use npx as last resort
65
+ let pkgName = '@jagilber-org/index-server';
66
+ let pkgVersion = '';
67
+ try {
68
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
69
+ if (pkg.name) pkgName = pkg.name;
70
+ if (pkg.version) pkgVersion = `@${pkg.version}`;
71
+ } catch {
72
+ console.warn('⚠ Could not read package.json — npx will use latest published version');
73
+ }
74
+
75
+ return {
76
+ command: 'npx',
77
+ args: ['-y', `${pkgName}${pkgVersion}`],
78
+ cwd: config.root,
79
+ source: 'npx',
80
+ };
81
+ }
82
+
30
83
  // --------------------------------------------------------------------------
31
84
  // Path helpers
32
85
  // --------------------------------------------------------------------------
33
86
  /** Normalize to forward slashes for mcp.json compatibility. */
34
87
  function fwd(p) { return p.replace(/\\/g, '/'); }
35
88
 
89
+ /** Locate the npm CLI script for execFileSync(node, [npmCli, ...]). Returns null if not found. */
90
+ function findNpmCli() {
91
+ // npm_execpath is set when running via npm/npx
92
+ if (process.env.npm_execpath) return process.env.npm_execpath;
93
+ // Resolve npm relative to the Node.js installation
94
+ const candidates = [
95
+ path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
96
+ path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
97
+ ];
98
+ for (const c of candidates) {
99
+ if (fs.existsSync(c)) return c;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /** Run an npm command. Uses npm CLI script if found, otherwise shells out to `npm` on PATH. */
105
+ function runNpm(args, opts = {}) {
106
+ const npmCli = findNpmCli();
107
+ if (npmCli) {
108
+ return execFileSync(process.execPath, [npmCli, ...args], opts);
109
+ }
110
+ // Fallback: invoke `npm` directly via execFileSync (uses npm.cmd on Windows).
111
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
112
+ return execFileSync(npmBin, args, opts);
113
+ }
114
+
36
115
  /** Resolve a sub-path under a root, always absolute and forward-slashed. */
37
116
  function resolveUnder(root, ...segments) { return fwd(path.resolve(root, ...segments)); }
38
117
 
@@ -120,6 +199,7 @@ function parseNonInteractiveArgs() {
120
199
  scope: 'repo', // 'global' or 'repo'
121
200
  write: false, // write to real config files
122
201
  preview: true, // show preview before writing
202
+ deploy: true, // deploy runtime to root when needed
123
203
  };
124
204
 
125
205
  for (let i = 0; i < args.length; i++) {
@@ -136,6 +216,7 @@ function parseNonInteractiveArgs() {
136
216
  else if (args[i] === '--scope' && args[i + 1]) config.scope = args[++i];
137
217
  else if (args[i] === '--write') config.write = true;
138
218
  else if (args[i] === '--no-preview') config.preview = false;
219
+ else if (args[i] === '--no-deploy') config.deploy = false;
139
220
  }
140
221
 
141
222
  // Profile overrides
@@ -253,7 +334,7 @@ async function runInteractiveWizard() {
253
334
  default: 'repo',
254
335
  });
255
336
 
256
- return { profile, root, serverName, port, host, mutation, logLevel, generateCerts, targets, scope, write: true, preview: true };
337
+ return { profile, root, serverName, port, host, mutation, logLevel, generateCerts, targets, scope, write: true, preview: true, deploy: true };
257
338
  }
258
339
 
259
340
  // --------------------------------------------------------------------------
@@ -362,15 +443,17 @@ function getEnvCatalog(config, paths) {
362
443
  function generateMcpJson(config, paths) {
363
444
  const catalog = getEnvCatalog(config, paths);
364
445
  const indent = '\t\t\t\t';
446
+ const launch = resolveServerLaunch(config);
447
+ const argsJson = JSON.stringify(launch.args);
365
448
 
366
449
  const lines = [
367
450
  '{',
368
451
  '\t"servers": {',
369
452
  `\t\t"${config.serverName}": {`,
370
453
  '\t\t\t"type": "stdio",',
371
- `\t\t\t"cwd": "${fwd(config.root)}",`,
372
- '\t\t\t"command": "node",',
373
- '\t\t\t"args": ["dist/server/index-server.js"],',
454
+ `\t\t\t"cwd": "${fwd(launch.cwd)}",`,
455
+ `\t\t\t"command": "${launch.command}",`,
456
+ `\t\t\t"args": ${argsJson},`,
374
457
  '\t\t\t"env": {',
375
458
  ];
376
459
 
@@ -412,11 +495,16 @@ function generateCopilotCliJson(config, paths) {
412
495
  if (entry.section) continue;
413
496
  if (entry.active) env[entry.key] = entry.value;
414
497
  }
498
+ const launch = resolveServerLaunch(config);
499
+ // copilot-cli doesn't reliably inherit cwd — use absolute args for local/packaged
500
+ const args = launch.source === 'local'
501
+ ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
502
+ : launch.args;
415
503
  const obj = {
416
504
  mcpServers: {
417
505
  [config.serverName]: {
418
- command: 'node',
419
- args: [path.join(fwd(config.root), 'dist/server/index-server.js')],
506
+ command: launch.command,
507
+ args,
420
508
  env,
421
509
  },
422
510
  },
@@ -434,11 +522,16 @@ function generateClaudeDesktopJson(config, paths) {
434
522
  if (entry.section) continue;
435
523
  if (entry.active) env[entry.key] = entry.value;
436
524
  }
525
+ const launch = resolveServerLaunch(config);
526
+ // Claude Desktop doesn't support cwd — use absolute args for local/packaged
527
+ const args = launch.source === 'local'
528
+ ? [fwd(path.resolve(launch.cwd, launch.args[0]))]
529
+ : launch.args;
437
530
  const obj = {
438
531
  mcpServers: {
439
532
  [config.serverName]: {
440
- command: 'node',
441
- args: [path.join(fwd(config.root), 'dist/server/index-server.js')],
533
+ command: launch.command,
534
+ args,
442
535
  env,
443
536
  },
444
537
  },
@@ -620,6 +713,143 @@ function printFolderSummary(paths, profile) {
620
713
  console.log('└────────────────────┴────────────────────────────────────────────────┘');
621
714
  }
622
715
 
716
+ // --------------------------------------------------------------------------
717
+ // Deploy runtime to target root (when different from package root)
718
+ // --------------------------------------------------------------------------
719
+ async function deployRuntime(config) {
720
+ if (config.deploy === false) return;
721
+
722
+ let sourceRoot, targetRoot;
723
+ try {
724
+ sourceRoot = fs.realpathSync(ROOT);
725
+ targetRoot = fs.realpathSync(config.root);
726
+ } catch {
727
+ sourceRoot = path.resolve(ROOT);
728
+ targetRoot = path.resolve(config.root);
729
+ }
730
+
731
+ // Skip when running from the target directory (dev clone / already deployed)
732
+ if (sourceRoot.toLowerCase() === targetRoot.toLowerCase()) return;
733
+
734
+ const entryPoint = path.join(targetRoot, 'dist', 'server', 'index-server.js');
735
+ const targetPkg = path.join(targetRoot, 'package.json');
736
+
737
+ // Read source version for comparison
738
+ let sourceVersion = 'unknown';
739
+ try {
740
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
741
+ sourceVersion = pkg.version || 'unknown';
742
+ } catch { /* ok */ }
743
+
744
+ // Check if already deployed at this version
745
+ if (fs.existsSync(entryPoint) && fs.existsSync(targetPkg)) {
746
+ try {
747
+ const existing = JSON.parse(fs.readFileSync(targetPkg, 'utf8'));
748
+ if (existing.version === sourceVersion) {
749
+ console.log(`\n✅ Runtime v${sourceVersion} already deployed at ${config.root}`);
750
+ return;
751
+ }
752
+ console.log(`\n📦 Upgrading runtime: ${existing.version} → ${sourceVersion}`);
753
+ } catch { /* ok - redeploy */ }
754
+ } else {
755
+ console.log(`\n📦 Deploying runtime v${sourceVersion} to ${config.root}...`);
756
+ }
757
+
758
+ const pkgName = '@jagilber-org/index-server';
759
+
760
+ // Strategy: npm install the exact package version into the target directory
761
+ // This gives a proper node_modules tree regardless of npx cache layout
762
+ try {
763
+ fs.mkdirSync(targetRoot, { recursive: true });
764
+
765
+ // Write a minimal package.json if none exists (npm install needs it)
766
+ if (!fs.existsSync(targetPkg)) {
767
+ const minPkg = {
768
+ name: 'index-server-runtime',
769
+ version: '1.0.0',
770
+ private: true,
771
+ type: 'commonjs',
772
+ scripts: { start: 'node node_modules/@jagilber-org/index-server/dist/server/index-server.js' },
773
+ };
774
+ fs.writeFileSync(targetPkg, JSON.stringify(minPkg, null, 2), 'utf8');
775
+ }
776
+
777
+ console.log(' Installing package (this may take a moment)...');
778
+
779
+ // Strategy: pack the current package into a tarball, then install it.
780
+ // This works regardless of whether the version is published to npm,
781
+ // and produces a proper self-contained node_modules tree.
782
+ const packOutput = runNpm(
783
+ ['pack', '--pack-destination', targetRoot],
784
+ { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], timeout: 30_000 }
785
+ ).toString().trim();
786
+ const tarballName = packOutput.split('\n').pop();
787
+
788
+ const tarballPath = path.join(targetRoot, tarballName);
789
+
790
+ try {
791
+ // Install from the local tarball
792
+ runNpm(
793
+ ['install', tarballPath, '--omit=dev', '--no-fund', '--no-audit'],
794
+ { cwd: targetRoot, stdio: 'inherit', timeout: 120_000 }
795
+ );
796
+ } finally {
797
+ // Clean up tarball
798
+ try { fs.unlinkSync(tarballPath); } catch { /* ok */ }
799
+ }
800
+
801
+ // Create convenience symlinks/junctions so "dist/" at root resolves
802
+ const installedDist = path.join(targetRoot, 'node_modules', pkgName, 'dist');
803
+ const targetDist = path.join(targetRoot, 'dist');
804
+ if (fs.existsSync(installedDist)) {
805
+ // Remove stale junction/directory before (re)creating — handles broken junctions on Windows
806
+ try { fs.rmSync(targetDist, { recursive: true, force: true }); } catch { /* ok if absent */ }
807
+ try {
808
+ // On Windows, directory junctions don't require elevated privileges
809
+ fs.symlinkSync(installedDist, targetDist, 'junction');
810
+ } catch {
811
+ // Fallback: copy dist recursively
812
+ fs.cpSync(installedDist, targetDist, { recursive: true });
813
+ }
814
+ }
815
+
816
+ // Copy schemas — refresh on redeploy/upgrade
817
+ const installedSchemas = path.join(targetRoot, 'node_modules', pkgName, 'schemas');
818
+ const targetSchemas = path.join(targetRoot, 'schemas');
819
+ if (fs.existsSync(installedSchemas)) {
820
+ try { fs.rmSync(targetSchemas, { recursive: true, force: true }); } catch { /* ok if absent */ }
821
+ fs.cpSync(installedSchemas, targetSchemas, { recursive: true });
822
+ }
823
+
824
+ // Update the runtime package.json with correct version/start script
825
+ try {
826
+ const runtimePkg = JSON.parse(fs.readFileSync(targetPkg, 'utf8'));
827
+ runtimePkg.version = sourceVersion;
828
+ runtimePkg.scripts = runtimePkg.scripts || {};
829
+ runtimePkg.scripts.start = 'node dist/server/index-server.js';
830
+ fs.writeFileSync(targetPkg, JSON.stringify(runtimePkg, null, 2) + '\n', 'utf8');
831
+ } catch { /* ok */ }
832
+
833
+ // Verify the deployed server is actually runnable
834
+ const deployedEntry = path.join(targetRoot, 'dist', 'server', 'index-server.js');
835
+ if (!fs.existsSync(deployedEntry)) {
836
+ throw new Error(
837
+ `dist/server/index-server.js not found after deployment. ` +
838
+ `Build the project first: cd "${ROOT}" && npm run build`
839
+ );
840
+ }
841
+
842
+ console.log(` ✅ Runtime deployed to ${config.root}`);
843
+ } catch (err) {
844
+ console.error(`\n❌ Runtime deployment failed: ${err.message}`);
845
+ console.error(' To deploy manually, run:');
846
+ console.error(` cd "${config.root}" && npm install ${pkgName}@${sourceVersion}`);
847
+ console.error(' Then create a symlink: dist -> node_modules/@jagilber-org/index-server/dist');
848
+ // Exit with error so CI can detect deployment failures
849
+ process.exitCode = 1;
850
+ }
851
+ }
852
+
623
853
  // --------------------------------------------------------------------------
624
854
  // Main
625
855
  // --------------------------------------------------------------------------
@@ -646,7 +876,8 @@ Non-interactive mode:
646
876
  --target <list> Comma-separated targets: vscode,copilot-cli,claude
647
877
  --scope <s> global | repo (default: repo)
648
878
  --write Write directly to real config files (with backup)
649
- --no-preview Skip config preview in non-interactive mode`);
879
+ --no-preview Skip config preview in non-interactive mode
880
+ --no-deploy Skip runtime deployment to target root`);
650
881
  process.exit(0);
651
882
  }
652
883
 
@@ -718,11 +949,13 @@ Non-interactive mode:
718
949
  }
719
950
  }
720
951
 
952
+ // ── Deploy runtime if target root differs from package root ─────────
953
+ await deployRuntime(config);
954
+
721
955
  // ── Generate TLS certs ──────────────────────────────────────────────
722
956
  if (config.generateCerts) {
723
957
  console.log('\n🔐 Generating TLS certificates...');
724
958
  try {
725
- const { execFileSync } = await import('child_process');
726
959
  const certDir = path.join(config.root, 'certs');
727
960
  execFileSync(
728
961
  process.execPath,
@@ -737,16 +970,27 @@ Non-interactive mode:
737
970
 
738
971
  // ── Next steps ──────────────────────────────────────────────────────
739
972
  const proto = (config.profile === 'enhanced' || config.profile === 'experimental') ? 'https' : 'http';
973
+ const launch = resolveServerLaunch(config);
740
974
  console.log('\n╔════════════════════════════════════════════════════════════════╗');
741
975
  console.log('║ Next Steps ║');
742
976
  console.log('╚════════════════════════════════════════════════════════════════╝\n');
743
- console.log(' 1. Build the server:');
744
- console.log(' npm run build\n');
977
+
978
+ let step = 1;
979
+ // Note: `packaged` source means dist/ ships with the wizard package — nothing to
980
+ // build. The packaged-runtime info banner below covers it. Issue #260: do NOT
981
+ // print "npm run build" here because config.root is typically a data-only
982
+ // directory with no package.json (npm run build → ENOENT).
983
+ if (launch.source === 'npx') {
984
+ console.log(` ${step}. The server will be fetched via npx on first start.\n`);
985
+ step++;
986
+ }
745
987
 
746
988
  if (config.write) {
747
- console.log(' 2. Config files have been written. Restart your MCP client.\n');
989
+ console.log(` ${step}. Config files have been written. Restart your MCP client.\n`);
990
+ step++;
748
991
  } else {
749
- console.log(' 2. Copy generated config into your MCP client settings.');
992
+ console.log(` ${step}. Copy generated config into your MCP client settings.`);
993
+
750
994
  for (const ct of configTargets) {
751
995
  const genPath = ct.format === 'vscode' || ct.format === 'vscode-global'
752
996
  ? path.join(config.root, '.vscode', 'mcp.json.generated')
@@ -754,15 +998,18 @@ Non-interactive mode:
754
998
  console.log(` ${ct.target}: ${genPath}`);
755
999
  }
756
1000
  console.log('');
1001
+ step++;
757
1002
  }
758
1003
 
759
- console.log(` 3. Open the dashboard:`);
1004
+ console.log(` ${step}. Open the dashboard:`);
760
1005
  console.log(` ${proto}://localhost:${config.port}\n`);
1006
+ step++;
761
1007
 
762
1008
  if (config.profile === 'enhanced' || config.profile === 'experimental') {
763
- console.log(' 4. First-time semantic search:');
1009
+ console.log(` ${step}. First-time semantic search:`);
764
1010
  console.log(' The MiniLM model (~90MB) will download on first query.');
765
1011
  console.log(` Model cache: ${paths.modelCache}\n`);
1012
+ step++;
766
1013
  }
767
1014
 
768
1015
  if (config.profile === 'experimental') {
@@ -770,6 +1017,11 @@ Non-interactive mode:
770
1017
  console.log(` ${paths.sqliteDb}\n`);
771
1018
  }
772
1019
 
1020
+ if (launch.source === 'packaged') {
1021
+ console.log(' ℹ️ Using packaged runtime from current installation.');
1022
+ console.log(' Rerun without --no-deploy for a self-contained install.\n');
1023
+ }
1024
+
773
1025
  console.log(` Targets: ${(config.targets || ['vscode']).join(', ')} | Scope: ${config.scope || 'repo'}`);
774
1026
  console.log(` Profile: ${config.profile} | Root: ${fwd(config.root)}`);
775
1027
  console.log('');
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/jagilber-org/index-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.26.5",
9
+ "version": "1.26.11",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@jagilber-org/index-server",
14
- "version": "1.26.5",
14
+ "version": "1.26.11",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }