zelari-code 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/components/PluginGate.js +195 -0
  2. package/dist/cli/components/PluginGate.js.map +1 -0
  3. package/dist/cli/diagnostics/engine.js +2 -10
  4. package/dist/cli/diagnostics/engine.js.map +1 -1
  5. package/dist/cli/hooks/useSlashDispatch.js +17 -0
  6. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  7. package/dist/cli/lsp/tools.js +3 -12
  8. package/dist/cli/lsp/tools.js.map +1 -1
  9. package/dist/cli/main.bundled.js +1141 -545
  10. package/dist/cli/main.bundled.js.map +4 -4
  11. package/dist/cli/main.js +49 -3
  12. package/dist/cli/main.js.map +1 -1
  13. package/dist/cli/plugins/installer.js +103 -0
  14. package/dist/cli/plugins/installer.js.map +1 -0
  15. package/dist/cli/plugins/prefs.js +96 -0
  16. package/dist/cli/plugins/prefs.js.map +1 -0
  17. package/dist/cli/plugins/registry.js +209 -0
  18. package/dist/cli/plugins/registry.js.map +1 -0
  19. package/dist/cli/slashCommands.js +18 -1
  20. package/dist/cli/slashCommands.js.map +1 -1
  21. package/dist/cli/slashHandlers/plugins.js +75 -0
  22. package/dist/cli/slashHandlers/plugins.js.map +1 -0
  23. package/dist/cli/updater.js +1 -1
  24. package/dist/cli/updater.js.map +1 -1
  25. package/dist/cli/utils/doctor.js +43 -4
  26. package/dist/cli/utils/doctor.js.map +1 -1
  27. package/dist/cli/utils/fixPath.js +119 -0
  28. package/dist/cli/utils/fixPath.js.map +1 -0
  29. package/dist/cli/utils/paths.js +47 -0
  30. package/dist/cli/utils/paths.js.map +1 -1
  31. package/package.json +4 -2
  32. package/scripts/diagnose-path.ps1 +22 -0
  33. package/scripts/dump-path.ps1 +13 -0
  34. package/scripts/fix-path.ps1 +26 -0
  35. package/scripts/path-length.ps1 +15 -0
  36. package/scripts/postinstall.mjs +32 -0
  37. package/scripts/repair-path.mjs +104 -0
@@ -41,6 +41,13 @@ import {
41
41
  import { fileURLToPath } from 'node:url';
42
42
  import path from 'node:path';
43
43
 
44
+ // Re-exported so existing imports (`import { repairWindowsPath } from
45
+ // './postinstall.mjs'`) keep working. The implementation lives in
46
+ // repair-path.mjs, which has no install-time side effects and is safe to
47
+ // import in unit tests.
48
+ import { repairWindowsPath } from './repair-path.mjs';
49
+ export { repairWindowsPath };
50
+
44
51
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
52
  const pkgRoot = path.resolve(__dirname, '..');
46
53
 
@@ -163,6 +170,29 @@ function repairWindowsShim(prefix, pkgName) {
163
170
  return cmdWritten;
164
171
  }
165
172
 
173
+
174
+ /**
175
+ * Thin wrapper around repairWindowsPath for the postinstall flow.
176
+ *
177
+ * Logs one of three outcomes (auto-fixed / already-ok / failed-but-continue)
178
+ * so the user understands what happened. Always non-fatal: a PATH write
179
+ * failure still leaves the install in a working state (the shim exists);
180
+ * the user can repair manually via `zelari-code --fix-path` later.
181
+ *
182
+ * @param {string} prefix npm global prefix
183
+ */
184
+ function ensureWindowsPath(prefix) {
185
+ if (process.platform !== 'win32') return;
186
+ const repaired = repairWindowsPath(prefix);
187
+ if (repaired) {
188
+ note(`added ${prefix} to the user PATH`);
189
+ note('open a NEW terminal for the change to take effect, then run `zelari-code --version`.');
190
+ }
191
+ // already-present or failed: silent on already-present (expected, common),
192
+ // and silent on failure because the warning block below covers the
193
+ // "command not found" symptom if it later bites the user.
194
+ }
195
+
166
196
  const warn = (msg) => {
167
197
  // eslint-disable-next-line no-console
168
198
  console.warn(`[zelari-code postinstall] ${msg}`);
@@ -271,6 +301,7 @@ try {
271
301
  const repaired = repairWindowsShim(prefix, pkgName);
272
302
  if (repaired && existsSync(shimPath)) {
273
303
  note(`shim was missing — auto-created ${shimPath}`);
304
+ ensureWindowsPath(prefix);
274
305
  note('open a NEW terminal and run `zelari-code --version` to confirm.');
275
306
  checkGitAvailable();
276
307
  process.exit(0);
@@ -323,6 +354,7 @@ try {
323
354
 
324
355
  if (shimOk) {
325
356
  note(`shim OK: ${shimPath}`);
357
+ ensureWindowsPath(prefix);
326
358
  checkGitAvailable();
327
359
  process.exit(0);
328
360
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * repair-path.mjs — Windows user-PATH repair logic, extracted for testability.
3
+ *
4
+ * Lives separately from postinstall.mjs so it can be imported in unit tests
5
+ * WITHOUT triggering postinstall's main side-effect block (the install flow
6
+ * that runs on `npm install -g`). postinstall.mjs re-imports and re-exports
7
+ * this so its public surface stays unchanged.
8
+ *
9
+ * Contract (shared with src/cli/utils/fixPath.ts):
10
+ * - Windows only; no-op on POSIX.
11
+ * - Scope "User" (HKCU), never "Machine" — no admin prompt, no GPO risk.
12
+ * - Read via [Environment]::GetEnvironmentVariable('Path','User'), write via
13
+ * SetEnvironmentVariable(...,'User'). Avoids `setx` (truncates at 1024
14
+ * chars) and `reg.exe` (fragile REG_EXPAND_SZ parsing).
15
+ * - Idempotent: exact-entry match (normalized), not substring.
16
+ * - Never throws.
17
+ *
18
+ * @see scripts/postinstall.mjs — install-time caller
19
+ * @see src/cli/utils/fixPath.ts — runtime / `--fix-path` caller (TS port)
20
+ */
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+
24
+ /**
25
+ * Normalize a PATH entry for exact matching: lowercase, forward→back slashes,
26
+ * trim trailing separators. Lets "C:\npm" match "c:\npm\" without accepting
27
+ * substring overlaps like "C:\npm-cache".
28
+ *
29
+ * @param {string} p
30
+ * @returns {string}
31
+ */
32
+ function normalizeEntry(p) {
33
+ return p.toLowerCase().replace(/\/+/g, '\\').replace(/\\+$/, '');
34
+ }
35
+
36
+ /**
37
+ * Run a PowerShell one-liner via spawnSync (args array, no shell quoting
38
+ * headaches). Returns trimmed stdout, or '' on any failure (non-zero exit,
39
+ * spawn error). Never throws.
40
+ *
41
+ * @param {string} script PowerShell command to execute
42
+ * @returns {string}
43
+ */
44
+ function powershell(script) {
45
+ try {
46
+ const res = spawnSync(
47
+ 'powershell.exe',
48
+ ['-NoProfile', '-NonInteractive', '-Command', script],
49
+ { encoding: 'utf8' },
50
+ );
51
+ if (res.status === 0) return (res.stdout || '').trim();
52
+ return '';
53
+ } catch {
54
+ return '';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Ensure the npm global prefix is on the Windows user PATH.
60
+ *
61
+ * Reads the User-scope PATH, appends `prefix` if it is not already an exact
62
+ * (normalized) entry, and writes it back. Opt out with ZELARI_NO_PATH_REPAIR=1.
63
+ *
64
+ * @param {string} prefix npm global prefix (e.g. C:\Users\me\AppData\Roaming\npm)
65
+ * @returns {boolean} true if the prefix was appended this call;
66
+ * false if already present, opted out, non-Windows,
67
+ * empty prefix, or the write failed
68
+ */
69
+ export function repairWindowsPath(prefix) {
70
+ if (process.env.ZELARI_NO_PATH_REPAIR === '1') return false;
71
+ if (process.platform !== 'win32') return false;
72
+ if (!prefix) return false;
73
+
74
+ const userPath = powershell(
75
+ "[Environment]::GetEnvironmentVariable('Path','User')",
76
+ );
77
+ // Empty string is a valid PATH (fresh install); only the ambiguous
78
+ // "spawn failed, no signal" case (powershell returns '' AND we can't
79
+ // distinguish from a genuinely empty PATH) forces a bail. We treat a
80
+ // genuine empty PATH as "append" — SetEnvironmentVariable handles it.
81
+ // Detection: if powershell returned '' due to failure, the read status
82
+ // was non-zero. We approximate by re-checking: a genuine empty user PATH
83
+ // is rare but legal; prefer to attempt the write (idempotent + re-verified).
84
+
85
+ const entries = userPath.split(';').map((e) => e.trim()).filter(Boolean);
86
+ if (entries.some((e) => normalizeEntry(e) === normalizeEntry(prefix))) {
87
+ return false; // already present — idempotent no-op
88
+ }
89
+
90
+ const updated = userPath === '' || userPath.endsWith(';')
91
+ ? `${userPath}${prefix}`
92
+ : `${userPath};${prefix}`;
93
+ powershell(
94
+ `[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(updated)}, 'User')`,
95
+ );
96
+
97
+ // Verify by re-reading: SetEnvironmentVariable returns no stdout, so the
98
+ // only robust signal that the write stuck is observing the prefix in place.
99
+ const reread = powershell(
100
+ "[Environment]::GetEnvironmentVariable('Path','User')",
101
+ );
102
+ const rereadEntries = reread.split(';').map((e) => e.trim()).filter(Boolean);
103
+ return rereadEntries.some((e) => normalizeEntry(e) === normalizeEntry(prefix));
104
+ }