@liumir/lmcode 0.5.13
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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/app-BrCDSMM8.mjs +139435 -0
- package/dist/assets/tokenizers.win32-x64-msvc-7FuPBfvC.node +0 -0
- package/dist/chunk-apG1qJts.mjs +41 -0
- package/dist/dist-0bMQWc-B.mjs +1258 -0
- package/dist/esm-CmfJNv9s.mjs +8590 -0
- package/dist/from-CKE2n10i.mjs +3849 -0
- package/dist/main.d.mts +2 -0
- package/dist/main.mjs +15 -0
- package/dist/multipart-parser-BxHsVgPe.mjs +299 -0
- package/dist/src-BMbLXrAA.mjs +1182 -0
- package/dist/suppress-sqlite-warning-C2VB0doZ.mjs +52 -0
- package/icon.ico +0 -0
- package/package.json +80 -0
- package/scripts/postinstall/migrate.mjs +351 -0
- package/scripts/postinstall/reach.mjs +457 -0
- package/scripts/postinstall/shortcut.mjs +74 -0
- package/scripts/postinstall/ui.mjs +458 -0
- package/scripts/postinstall.mjs +276 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall hook for @lmcode-cli/lmcode.
|
|
4
|
+
*
|
|
5
|
+
* Goal: when this package is installed globally, ensure typing `lm`
|
|
6
|
+
* invokes the new TypeScript CLI. The npm `package.json` bin field
|
|
7
|
+
* installs a fresh `lm` shim into the global bin dir; this script
|
|
8
|
+
* removes any pre-existing `lm` shim left behind by the previous
|
|
9
|
+
* Python CLI (installed via `uv tool install`, `pipx install`,
|
|
10
|
+
* `pip install`, etc.) that would otherwise shadow ours via PATH
|
|
11
|
+
* ordering. The renamed shim is kept as `scream-legacy` so users can
|
|
12
|
+
* still invoke the old CLI if they want to fall back.
|
|
13
|
+
*
|
|
14
|
+
* ## Hard rules
|
|
15
|
+
*
|
|
16
|
+
* - Only runs for global installs across npm, yarn (classic), and
|
|
17
|
+
* pnpm. Non-global installs (npx, local project deps, workspace
|
|
18
|
+
* bootstraps, `pnpm dlx`) are silent no-ops.
|
|
19
|
+
* - Never fails the install. Any error here is caught and reported,
|
|
20
|
+
* but the script always exits 0.
|
|
21
|
+
* - Does not touch a `lm` we don't recognize as the previous
|
|
22
|
+
* Python CLI (matched by realpath-resolved shim head containing
|
|
23
|
+
* `scream_cli`).
|
|
24
|
+
* - Cross-platform: POSIX and Windows. Windows-specific bits live
|
|
25
|
+
* in the helpers (PATHEXT-aware PATH walking, whole-file marker
|
|
26
|
+
* sniff for uv's Rust launcher .exe, extension-preserving
|
|
27
|
+
* rename target like `scream.exe` → `scream-legacy.exe`).
|
|
28
|
+
*
|
|
29
|
+
* ## Code layout
|
|
30
|
+
*
|
|
31
|
+
* This file is the orchestrator; the actual logic lives in
|
|
32
|
+
* sibling modules to keep each file under a manageable size:
|
|
33
|
+
*
|
|
34
|
+
* - `./postinstall/reach.mjs` — package-manager detection,
|
|
35
|
+
* global-install gate, own-package-root resolution, user-shell
|
|
36
|
+
* PATH lookup, reachability check.
|
|
37
|
+
* - `./postinstall/migrate.mjs` — legacy detection,
|
|
38
|
+
* `lm`-vs-`scream-legacy` classification, the rename / unlink
|
|
39
|
+
* primitives.
|
|
40
|
+
* - `./postinstall/ui.mjs` — `notify()` (with `/dev/tty` fallback),
|
|
41
|
+
* ANSI styling, the fixed-width box, and the five outcome
|
|
42
|
+
* renderers.
|
|
43
|
+
*
|
|
44
|
+
* ## Workflow
|
|
45
|
+
*
|
|
46
|
+
* What runs when a user types `npm install -g @lmcode-cli/lmcode`
|
|
47
|
+
* (or the yarn / pnpm equivalent):
|
|
48
|
+
*
|
|
49
|
+
* 1. The manager extracts the package and runs lifecycle scripts.
|
|
50
|
+
* The `bin.scream` mapping in `package.json` tells the manager to
|
|
51
|
+
* install a `lm` shim under its global bin directory.
|
|
52
|
+
* 2. The manager invokes this script via the `scripts.postinstall`
|
|
53
|
+
* entry — orchestrated by `main` below.
|
|
54
|
+
* 3. Install-context gate: only proceed when this is a global
|
|
55
|
+
* install (`isGlobalInstall` checks `npm_config_global` /
|
|
56
|
+
* `pnpm_config_global` / `npm_config_location`).
|
|
57
|
+
* 4. Probe PATH once via `postinstallPaths()`: detection uses the
|
|
58
|
+
* union of shell PATH + process PATH; reachability uses the
|
|
59
|
+
* shell PATH alone (with a fallback to process PATH if the
|
|
60
|
+
* shell can't be probed). Sharing one probe keeps detection
|
|
61
|
+
* and reachability symmetric and avoids running `$SHELL -l`
|
|
62
|
+
* twice.
|
|
63
|
+
* 5. Detect EVERY previous Python `scream-cli` shim on the detection
|
|
64
|
+
* PATH (`detectLegacyShims`). Returns `[]` for fresh-install /
|
|
65
|
+
* no-op. Multiple results happen when the user has installed
|
|
66
|
+
* `scream-cli` through more than one Python tool (uv + pipx, or
|
|
67
|
+
* sudo-pip + pip-user). PATH order is preserved.
|
|
68
|
+
* 6. Pre-flight classify each shim (`classifyShim`) — pure
|
|
69
|
+
* filesystem inspection, no writes. Each shim ends up
|
|
70
|
+
* `renameable`, `consolidate`, `delete-only`, or `blocked`.
|
|
71
|
+
* 7. Decide abort vs proceed against the WHOLE set:
|
|
72
|
+
* `findFirstResolvableScream` walks PATH treating the actionable
|
|
73
|
+
* shims as gone and reports what wins:
|
|
74
|
+
* - `own` → proceed to execute.
|
|
75
|
+
* - `blocked-legacy` → a legacy we can't remove still wins.
|
|
76
|
+
* Surface `logMigrationBlocked` with sudo / admin
|
|
77
|
+
* instructions; touch nothing.
|
|
78
|
+
* - `foreign` → some `lm` we don't recognize (a user's own
|
|
79
|
+
* file) wins. Surface `logForeignScreamInTheWay` asking the
|
|
80
|
+
* user to delete or rename their own file; touch nothing.
|
|
81
|
+
* - `none` → no `lm` on PATH at all (our shim's bin dir
|
|
82
|
+
* isn't in the shell's PATH). Surface
|
|
83
|
+
* `logNewCliNotOnPath`; touch nothing.
|
|
84
|
+
* 8. Execute. The FIRST classification in PATH order that we can
|
|
85
|
+
* touch becomes `scream-legacy` (preserves what `lm` referred
|
|
86
|
+
* to before this install). Each subsequent shim is `unlink`ed —
|
|
87
|
+
* keeping it as a dormant duplicate adds no value. If the
|
|
88
|
+
* first shim's `scream-legacy` target is already user-managed,
|
|
89
|
+
* we delete `lm` anyway (still achieves takeover) and tell
|
|
90
|
+
* the user we couldn't preserve a fallback. Extension is
|
|
91
|
+
* preserved on Windows (`scream.exe` → `scream-legacy.exe`).
|
|
92
|
+
* 9. One end-of-orchestration notice (`logMigrationDone`)
|
|
93
|
+
* summarizes every action — renames, consolidates,
|
|
94
|
+
* delete-only, deletes, and harmless blocked leftovers. The
|
|
95
|
+
* takeover-success line only fires on this path because Step 7
|
|
96
|
+
* already certified it.
|
|
97
|
+
* 10. The manager completes the install with its usual summary.
|
|
98
|
+
* This script always exits 0; any uncaught error is swallowed
|
|
99
|
+
* by the top-level `catch` so the install never fails because
|
|
100
|
+
* of the migration.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
import {
|
|
104
|
+
detectPackageManager,
|
|
105
|
+
findFirstResolvableScream,
|
|
106
|
+
isGlobalInstall,
|
|
107
|
+
ownPackageRoot,
|
|
108
|
+
postinstallPaths,
|
|
109
|
+
} from './postinstall/reach.mjs';
|
|
110
|
+
import {
|
|
111
|
+
classifyShim,
|
|
112
|
+
deleteShim,
|
|
113
|
+
detectLegacyShims,
|
|
114
|
+
renameInPlace,
|
|
115
|
+
} from './postinstall/migrate.mjs';
|
|
116
|
+
import { createDesktopShortcut } from './postinstall/shortcut.mjs';
|
|
117
|
+
import {
|
|
118
|
+
logForeignScreamInTheWay,
|
|
119
|
+
logMigrationBlocked,
|
|
120
|
+
logMigrationDone,
|
|
121
|
+
logNewCliNotOnPath,
|
|
122
|
+
notify,
|
|
123
|
+
} from './postinstall/ui.mjs';
|
|
124
|
+
|
|
125
|
+
async function main() {
|
|
126
|
+
// Step 1: skip non-global installs (npx, local project deps,
|
|
127
|
+
// workspace bootstraps). Windows is supported natively; the
|
|
128
|
+
// platform-specific bits (PATHEXT-aware PATH walk, whole-file
|
|
129
|
+
// marker sniff for uv's launcher .exe, extension-preserving
|
|
130
|
+
// rename) live in the helpers.
|
|
131
|
+
if (!isGlobalInstall()) return;
|
|
132
|
+
|
|
133
|
+
// Step 2: locate our own installed package root once and share it
|
|
134
|
+
// with both detection (skip files inside our package) and
|
|
135
|
+
// reachability (only count our shim as "found").
|
|
136
|
+
const ownRoot = await ownPackageRoot(import.meta.dirname);
|
|
137
|
+
const pm = detectPackageManager();
|
|
138
|
+
|
|
139
|
+
// Step 3: probe the user's shell PATH once so detection and
|
|
140
|
+
// reachability share a single consistent view. Detection uses the
|
|
141
|
+
// union of shell PATH + process PATH (so we catch a legacy shim
|
|
142
|
+
// visible to either); reachability uses the shell PATH alone (so
|
|
143
|
+
// we don't claim "scream works now" when the shim only sits in the
|
|
144
|
+
// installer's env).
|
|
145
|
+
const paths = await postinstallPaths();
|
|
146
|
+
|
|
147
|
+
// Step 4: detect EVERY previous Python `scream-cli` shim on the
|
|
148
|
+
// detection PATH. A user with both `uv tool install` and `pipx
|
|
149
|
+
// install` would have two; we must address all of them or the
|
|
150
|
+
// survivor still shadows the new CLI.
|
|
151
|
+
const detections = await detectLegacyShims(ownRoot, paths.detection);
|
|
152
|
+
if (detections.length === 0) return;
|
|
153
|
+
|
|
154
|
+
// Step 5: pre-flight classify every shim WITHOUT touching the
|
|
155
|
+
// filesystem yet. The orchestrator decides abort-or-proceed against
|
|
156
|
+
// the whole set rather than discovering mid-loop that we got partway
|
|
157
|
+
// and have to backtrack.
|
|
158
|
+
const classifications = await Promise.all(
|
|
159
|
+
detections.map(async (detection) => {
|
|
160
|
+
const c = await classifyShim(detection.shimPath);
|
|
161
|
+
return { ...c, detection };
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Step 6: figure out what wins PATH resolution once every shim we
|
|
166
|
+
// CAN touch is treated as gone. Three possible blockers:
|
|
167
|
+
// - a legacy shim we couldn't classify as actionable (sudo/admin
|
|
168
|
+
// needed)
|
|
169
|
+
// - an unrelated `lm` we don't recognize (a user's own wrapper
|
|
170
|
+
// script — they own the decision)
|
|
171
|
+
// - nothing resolves (our shim isn't on PATH at all)
|
|
172
|
+
// For each we render a different notice and touch NOTHING. The
|
|
173
|
+
// common-case fourth result is "our shim wins" — we proceed.
|
|
174
|
+
const actionable = classifications.filter((c) => c.kind !== 'blocked');
|
|
175
|
+
const blocked = classifications.filter((c) => c.kind === 'blocked');
|
|
176
|
+
const actionableShimPaths = actionable.map((c) => c.shimPath);
|
|
177
|
+
const allDetectedShimPaths = classifications.map((c) => c.shimPath);
|
|
178
|
+
|
|
179
|
+
const blocker = await findFirstResolvableScream(
|
|
180
|
+
ownRoot,
|
|
181
|
+
paths.reachability,
|
|
182
|
+
actionableShimPaths,
|
|
183
|
+
allDetectedShimPaths,
|
|
184
|
+
);
|
|
185
|
+
if (blocker.kind !== 'own') {
|
|
186
|
+
if (blocker.kind === 'blocked-legacy') {
|
|
187
|
+
logMigrationBlocked(blocked, actionable, pm);
|
|
188
|
+
} else if (blocker.kind === 'foreign') {
|
|
189
|
+
logForeignScreamInTheWay(blocker.path, pm);
|
|
190
|
+
} else {
|
|
191
|
+
// 'none' — our shim isn't on PATH at all.
|
|
192
|
+
logNewCliNotOnPath(detections[0], pm);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Step 7: execute. The FIRST classification in PATH order that
|
|
198
|
+
// we can touch becomes `scream-legacy` (preserves what the user's
|
|
199
|
+
// `lm` used to refer to). Every subsequent shim is just
|
|
200
|
+
// deleted — keeping it as a dormant duplicate adds no value.
|
|
201
|
+
const renames = [];
|
|
202
|
+
const consolidates = [];
|
|
203
|
+
const skippedForeignTarget = [];
|
|
204
|
+
const deletes = [];
|
|
205
|
+
const errors = [];
|
|
206
|
+
let preservedFirst = false;
|
|
207
|
+
|
|
208
|
+
for (const c of classifications) {
|
|
209
|
+
if (c.kind === 'blocked') continue; // already established harmless
|
|
210
|
+
|
|
211
|
+
if (!preservedFirst) {
|
|
212
|
+
preservedFirst = true;
|
|
213
|
+
if (c.kind === 'renameable') {
|
|
214
|
+
const r = await renameInPlace(c.shimPath, c.target);
|
|
215
|
+
if (r.success) {
|
|
216
|
+
renames.push(c);
|
|
217
|
+
} else {
|
|
218
|
+
errors.push({ ...c, ...r });
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (c.kind === 'consolidate') {
|
|
223
|
+
const r = await deleteShim(c.shimPath);
|
|
224
|
+
if (r.success) {
|
|
225
|
+
consolidates.push(c);
|
|
226
|
+
} else {
|
|
227
|
+
errors.push({ ...c, ...r });
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (c.kind === 'delete-only') {
|
|
232
|
+
const r = await deleteShim(c.shimPath);
|
|
233
|
+
if (r.success) {
|
|
234
|
+
skippedForeignTarget.push(c);
|
|
235
|
+
} else {
|
|
236
|
+
errors.push({ ...c, ...r });
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Not the first actionable shim. Just delete it.
|
|
242
|
+
const r = await deleteShim(c.shimPath);
|
|
243
|
+
if (r.success) {
|
|
244
|
+
deletes.push(c);
|
|
245
|
+
} else {
|
|
246
|
+
errors.push({ ...c, ...r });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Step 8: one notice summarizing everything that happened. The
|
|
252
|
+
// takeover-success language is only emitted when we know it's true
|
|
253
|
+
// (we already passed the reachability gate above).
|
|
254
|
+
logMigrationDone(
|
|
255
|
+
{
|
|
256
|
+
renames,
|
|
257
|
+
consolidates,
|
|
258
|
+
skippedForeignTarget,
|
|
259
|
+
deletes,
|
|
260
|
+
blockedHarmless: blocked,
|
|
261
|
+
errors,
|
|
262
|
+
},
|
|
263
|
+
pm,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
createDesktopShortcut();
|
|
269
|
+
} catch {
|
|
270
|
+
// Never fail the install over a shortcut.
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main().catch((err) => {
|
|
274
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
275
|
+
notify(`[lmcode] postinstall warning: ${message}`);
|
|
276
|
+
});
|