zelari-code 1.0.2 → 1.0.3

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.
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.mjs — Shim health check for global installs.
4
+ *
5
+ * Why this exists:
6
+ * On Windows + npm 10/11, `npm install -g zelari-code@<x>` sometimes
7
+ * fails to (re)create the bin shim `zelari-code.cmd` after an upgrade
8
+ * (e.g. 0.7.x → 1.0.x). The package is correctly installed in
9
+ * `<prefix>/node_modules/zelari-code/`, but `<prefix>/zelari-code.cmd`
10
+ * is missing, so `zelari-code` returns "command not found" on the
11
+ * next shell open. This script runs as `postinstall` and:
12
+ *
13
+ * 1. Detects global installs only (skips local installs to avoid noise).
14
+ * 2. Verifies the expected shim path exists and is current.
15
+ * 3. If missing or stale, prints a clear, actionable warning with the
16
+ * exact fix command. It does NOT auto-repair the shim — auto-repair
17
+ * is risky (shim is a privileged file in the global prefix and a
18
+ * misdirected symlink can shadow other tools).
19
+ * 4. Also warns if the npm global prefix is not on the current PATH,
20
+ * which is a separate-but-related failure mode (npm install
21
+ * succeeds, the shim exists, but the user can't run the command
22
+ * because the prefix isn't reachable from their shell).
23
+ *
24
+ * All output goes to stderr so it doesn't pollute the install log on
25
+ * success. The script is fail-safe: any error here is swallowed and
26
+ * logged, never thrown — a broken postinstall must NEVER fail the
27
+ * install itself.
28
+ *
29
+ * @see scripts/diagnose-path.ps1 — interactive Windows diagnostic
30
+ * @see scripts/fix-path.ps1 — adds npm prefix to user PATH
31
+ */
32
+
33
+ import { execSync } from 'node:child_process';
34
+ import {
35
+ existsSync,
36
+ readFileSync,
37
+ readlinkSync,
38
+ statSync,
39
+ } from 'node:fs';
40
+ import { fileURLToPath } from 'node:url';
41
+ import path from 'node:path';
42
+
43
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
+ const pkgRoot = path.resolve(__dirname, '..');
45
+
46
+ const warn = (msg) => {
47
+ // eslint-disable-next-line no-console
48
+ console.warn(`[zelari-code postinstall] ${msg}`);
49
+ };
50
+
51
+ const note = (msg) => {
52
+ // eslint-disable-next-line no-console
53
+ console.warn(`[zelari-code postinstall] ${msg}`);
54
+ };
55
+
56
+ try {
57
+ // 0. Only run for global installs. Local installs (`npm install zelari-code`)
58
+ // create a `node_modules/.bin/zelari-code` symlink that npm handles
59
+ // correctly, and the user runs the command via `npx` or package.json
60
+ // scripts — so the global-prefix shim is irrelevant.
61
+ //
62
+ // npm sets `npm_config_global=true` for `npm install -g` and for
63
+ // `npm install -g <pkg>` invocations. Some npm versions also set
64
+ // `npm_install_global` — check both for safety.
65
+ const isGlobal =
66
+ process.env.npm_config_global === 'true' ||
67
+ process.env.npm_install_global === 'true' ||
68
+ process.env.NPM_CONFIG_GLOBAL === 'true';
69
+
70
+ if (!isGlobal) {
71
+ process.exit(0);
72
+ }
73
+
74
+ // 1. Resolve the global prefix. Prefer the env var (set by npm), fall
75
+ // back to `npm prefix -g`. If both fail, bail silently — we can't
76
+ // diagnose without knowing where to look.
77
+ let prefix = (process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || '').trim();
78
+ if (!prefix) {
79
+ try {
80
+ prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
81
+ } catch {
82
+ // npm not on PATH, or this is a weirdly-sandboxed install.
83
+ // Either way, we can't help further.
84
+ process.exit(0);
85
+ }
86
+ }
87
+
88
+ const pkgName = process.env.npm_package_name || 'zelari-code';
89
+ const pkgVersion = process.env.npm_package_version || 'unknown';
90
+ const expectedBinScript = path.join(pkgRoot, 'bin', 'zelari-code.js');
91
+ const isWin = process.platform === 'win32';
92
+ const shimName = isWin ? 'zelari-code.cmd' : 'zelari-code';
93
+ const shimPath = path.join(prefix, shimName);
94
+ const expectedNodeModulesShim = isWin
95
+ ? `%~dp0\\node_modules\\${pkgName}\\bin\\zelari-code.js`
96
+ : path.join(prefix, 'node_modules', pkgName, 'bin', 'zelari-code.js');
97
+
98
+ // 2. Check the shim.
99
+ let shimOk = false;
100
+ let shimReason = '';
101
+
102
+ if (!existsSync(shimPath)) {
103
+ shimReason = `shim file not found at ${shimPath}`;
104
+ } else {
105
+ try {
106
+ const st = statSync(shimPath);
107
+ if (isWin) {
108
+ // Windows: .cmd shim. Validate the content references the current
109
+ // package name + bin path. The exact text varies slightly across
110
+ // npm versions, so we just check for the package name + the
111
+ // bin/zelari-code.js substring.
112
+ const content = readFileSync(shimPath, 'utf8');
113
+ if (content.includes(`${pkgName}\\bin\\zelari-code.js`) || content.includes(`${pkgName}/bin/zelari-code.js`)) {
114
+ shimOk = true;
115
+ } else {
116
+ shimReason = `shim at ${shimPath} does not reference ${pkgName}/bin/zelari-code.js — it may belong to a different install`;
117
+ }
118
+ } else {
119
+ // POSIX: symlink (or hardlink). readlinkSync throws for non-symlinks.
120
+ let target;
121
+ try {
122
+ target = readlinkSync(shimPath);
123
+ } catch {
124
+ // Not a symlink — could be a hardlink or a copied file. In
125
+ // either case, npm shouldn't have done that. Treat as broken.
126
+ shimReason = `shim at ${shimPath} is not a symlink (npm should create a symlink for global installs on POSIX)`;
127
+ shimOk = false;
128
+ }
129
+ if (target) {
130
+ const resolved = path.resolve(path.dirname(shimPath), target);
131
+ if (resolved === expectedNodeModulesShim) {
132
+ shimOk = true;
133
+ } else {
134
+ shimReason = `shim at ${shimPath} points to ${resolved}, expected ${expectedNodeModulesShim}`;
135
+ }
136
+ }
137
+ }
138
+ // If we reach here with shimOk still false and shimReason still empty,
139
+ // it's a non-symlink non-cmd file — leave shimReason empty so the
140
+ // generic message is shown.
141
+ if (!shimOk && !shimReason) {
142
+ shimReason = `shim at ${shimPath} is not a regular file or symlink (mode=${st.mode})`;
143
+ }
144
+ } catch (err) {
145
+ shimReason = `could not inspect shim at ${shimPath}: ${err.message}`;
146
+ }
147
+ }
148
+
149
+ if (shimOk) {
150
+ note(`shim OK: ${shimPath}`);
151
+ process.exit(0);
152
+ }
153
+
154
+ // 3. Shim is broken or missing. Emit a clear, actionable warning.
155
+ warn('==============================================================');
156
+ warn(' npm install completed, but the `zelari-code` shim is broken.');
157
+ warn('==============================================================');
158
+ warn(` Reason: ${shimReason}`);
159
+ warn('');
160
+ warn(' The package itself is correctly installed in:');
161
+ warn(` ${path.join(prefix, 'node_modules', pkgName)}`);
162
+ warn(' But the bin shim is not where npm should have created it:');
163
+ warn(` ${shimPath}`);
164
+ warn('');
165
+ warn(' Symptom: `zelari-code` returns "command not found" in your');
166
+ warn(' shell, even though `npm ls -g` lists the package.');
167
+ warn('');
168
+ warn(' Fix (try in order, run as Administrator on Windows if needed):');
169
+ warn('');
170
+ warn(` npm install -g ${pkgName}@${pkgVersion} --force`);
171
+ warn('');
172
+ warn(' If the command is still not found after that, your npm');
173
+ warn(' global prefix may not be on your shell PATH. Run:');
174
+ warn('');
175
+ warn(' npm bin -g # shows the expected shim dir');
176
+ warn(' echo "$PATH" # check if it is included');
177
+ warn('');
178
+ warn(' On Windows, scripts/diagnose-path.ps1 and scripts/fix-path.ps1');
179
+ warn(' in the repo can inspect and repair PATH for you.');
180
+ warn('==============================================================');
181
+ process.exit(0); // Never fail the install.
182
+ } catch (err) {
183
+ // Last-resort safety net: log and exit 0.
184
+ warn(`unexpected error (install is not affected): ${err.message}`);
185
+ process.exit(0);
186
+ }