scream-code 0.12.3 → 0.12.5
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/README.md +20 -0
- package/dist/{app-D0r0X4TD.mjs → app-35ccG0J0.mjs} +876 -20
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-DHhRrX3G.mjs → text-input-dialog-BQeV1dTM.mjs} +14 -0
- package/dist/{text-input-dialog-K1S_ir1m.mjs → text-input-dialog-CEUCIznb.mjs} +1 -1
- package/package.json +2 -6
- package/scripts/postinstall/migrate.mjs +0 -351
- package/scripts/postinstall/reach.mjs +0 -457
- package/scripts/postinstall/shortcut.mjs +0 -74
- package/scripts/postinstall/ui.mjs +0 -458
- package/scripts/postinstall.mjs +0 -275
package/dist/main.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
|
|
|
6
6
|
import "./suppress-sqlite-warning-C2VB0doZ.mjs";
|
|
7
7
|
//#region src/main.ts
|
|
8
8
|
try {
|
|
9
|
-
(await import("./app-
|
|
9
|
+
(await import("./app-35ccG0J0.mjs")).main();
|
|
10
10
|
} catch (error) {
|
|
11
11
|
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
12
12
|
process.exit(1);
|
|
@@ -563,6 +563,7 @@ const dictionaries = {
|
|
|
563
563
|
"memory.session_label": "会话: ",
|
|
564
564
|
"memory.pitfall_label": "踩坑: ",
|
|
565
565
|
"memory.experience_label": "经验: ",
|
|
566
|
+
"memory.note_label": "笔记: ",
|
|
566
567
|
"memory.detail_nav_hint": " Enter/Esc 返回 | i 注入 | d 删除",
|
|
567
568
|
"skill.no_session": "请先创建或恢复一个会话,再使用 Skill 中心。",
|
|
568
569
|
"skill.loading": "正在加载 Skill 中心…",
|
|
@@ -1034,6 +1035,9 @@ const dictionaries = {
|
|
|
1034
1035
|
"config.yolo_already_off": "YES 模式已关闭",
|
|
1035
1036
|
"config.ask_already_on": "ASK 模式已开启",
|
|
1036
1037
|
"config.ask_already_off": "ASK 模式已关闭",
|
|
1038
|
+
"config.rlm_on": "RLM 模式已开启 — 持久化 Python 内核可用",
|
|
1039
|
+
"config.rlm_off": "RLM 模式已关闭",
|
|
1040
|
+
"config.rlm_usage": "用法: /rlm (切换) | /rlm on | /rlm off",
|
|
1037
1041
|
"config.auto_already_on": "自动模式已开启",
|
|
1038
1042
|
"config.auto_already_off": "自动模式已关闭",
|
|
1039
1043
|
"settings.title": "设置",
|
|
@@ -1069,6 +1073,8 @@ const dictionaries = {
|
|
|
1069
1073
|
"registry.auto_desc": "切换自动权限模式",
|
|
1070
1074
|
"registry.yolo_desc": "切换至自动批准模式(yolo)",
|
|
1071
1075
|
"registry.ask_desc": "切换至只读问答模式(ask)",
|
|
1076
|
+
"registry.rlm_desc": "RLM 模式:面向长任务的持久 Python 工作环境,支持无限递归子代理(/rlm 切换开关)",
|
|
1077
|
+
"registry.rlm_max_depth_desc": "设置 RLM 递归深度上限(/rlm-max-depth N)",
|
|
1072
1078
|
"registry.wolfpack_desc": "切换群狼协作模式,自动批准+批量并发",
|
|
1073
1079
|
"registry.sessions_desc": "浏览并恢复会话",
|
|
1074
1080
|
"registry.goal_desc": "查看/管理自动目标",
|
|
@@ -1143,6 +1149,7 @@ const dictionaries = {
|
|
|
1143
1149
|
"badge.plan": "计划",
|
|
1144
1150
|
"badge.fusion": "融合计划",
|
|
1145
1151
|
"badge.wolfpack": "群狼",
|
|
1152
|
+
"badge.rlm": "RLM",
|
|
1146
1153
|
"badge.goal": "目标",
|
|
1147
1154
|
"badge.auto": "自动",
|
|
1148
1155
|
"badge.yes": "YES",
|
|
@@ -1618,6 +1625,7 @@ const dictionaries = {
|
|
|
1618
1625
|
"memory.session_label": "Session: ",
|
|
1619
1626
|
"memory.pitfall_label": "Pitfalls: ",
|
|
1620
1627
|
"memory.experience_label": "Experience: ",
|
|
1628
|
+
"memory.note_label": "Note: ",
|
|
1621
1629
|
"memory.detail_nav_hint": " Enter/Esc Back | i Inject | d Delete",
|
|
1622
1630
|
"skill.no_session": "Please create or resume a session before using the Skill center.",
|
|
1623
1631
|
"skill.loading": "Loading Skill center…",
|
|
@@ -2089,6 +2097,9 @@ const dictionaries = {
|
|
|
2089
2097
|
"config.yolo_already_off": "YES mode is already off",
|
|
2090
2098
|
"config.ask_already_on": "ASK mode is already on",
|
|
2091
2099
|
"config.ask_already_off": "ASK mode is already off",
|
|
2100
|
+
"config.rlm_on": "RLM mode enabled — persistent python kernel available",
|
|
2101
|
+
"config.rlm_off": "RLM mode disabled",
|
|
2102
|
+
"config.rlm_usage": "Usage: /rlm (toggle) | /rlm on | /rlm off",
|
|
2092
2103
|
"config.auto_already_on": "Auto mode is already on",
|
|
2093
2104
|
"config.auto_already_off": "Auto mode is already off",
|
|
2094
2105
|
"settings.title": "Settings",
|
|
@@ -2124,6 +2135,8 @@ const dictionaries = {
|
|
|
2124
2135
|
"registry.auto_desc": "Toggle auto permission mode",
|
|
2125
2136
|
"registry.yolo_desc": "Toggle auto-approve mode (yolo)",
|
|
2126
2137
|
"registry.ask_desc": "Toggle read-only Q&A mode (ask)",
|
|
2138
|
+
"registry.rlm_desc": "RLM mode: a persistent Python workspace for long-running tasks with unlimited recursive subagents (/rlm toggles)",
|
|
2139
|
+
"registry.rlm_max_depth_desc": "Set RLM recursion depth limit (/rlm-max-depth N)",
|
|
2127
2140
|
"registry.wolfpack_desc": "Toggle wolfpack mode, auto-approve + batch concurrency",
|
|
2128
2141
|
"registry.sessions_desc": "Browse and restore sessions",
|
|
2129
2142
|
"registry.goal_desc": "View/manage auto goals",
|
|
@@ -2198,6 +2211,7 @@ const dictionaries = {
|
|
|
2198
2211
|
"badge.plan": "Plan",
|
|
2199
2212
|
"badge.fusion": "Fusion",
|
|
2200
2213
|
"badge.wolfpack": "Wolfpack",
|
|
2214
|
+
"badge.rlm": "RLM",
|
|
2201
2215
|
"badge.goal": "Goal",
|
|
2202
2216
|
"badge.auto": "Auto",
|
|
2203
2217
|
"badge.yes": "YES",
|
|
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
|
|
|
3
3
|
import { dirname as __cjsShimDirname } from 'node:path';
|
|
4
4
|
const __filename = __cjsShimFileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = __cjsShimDirname(__filename);
|
|
6
|
-
import { t as TextInputDialogComponent } from "./text-input-dialog-
|
|
6
|
+
import { t as TextInputDialogComponent } from "./text-input-dialog-BQeV1dTM.mjs";
|
|
7
7
|
export { TextInputDialogComponent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scream-code",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "A terminal-native AI agent for builders",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ScreamCli",
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
-
"icon.ico"
|
|
31
|
-
"scripts/postinstall.mjs",
|
|
32
|
-
"scripts/postinstall"
|
|
30
|
+
"icon.ico"
|
|
33
31
|
],
|
|
34
32
|
"type": "module",
|
|
35
33
|
"imports": {
|
|
@@ -53,8 +51,6 @@
|
|
|
53
51
|
"test": "pnpm -w run build:packages && vitest run",
|
|
54
52
|
"e2e": "pnpm -w run build:packages && SCREAM_E2E=1 vitest run test/e2e",
|
|
55
53
|
"e2e:real": "pnpm -w run build:packages && SCREAM_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts",
|
|
56
|
-
"preinstall": "node -e \"console.log('\\n📦 正在安装 scream-code,请稍候...\\n')\"",
|
|
57
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
58
54
|
"prepublishOnly": "node scripts/verify-publish.mjs",
|
|
59
55
|
"smoke": "node dist/main.mjs --version"
|
|
60
56
|
},
|
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detection of the previous Python `scream-cli` shim and the actual
|
|
3
|
-
* filesystem operations that perform (or refuse) the rename.
|
|
4
|
-
*
|
|
5
|
-
* Detection:
|
|
6
|
-
* - {@link detectLegacyShims}: walks the caller-supplied
|
|
7
|
-
* `pathString` and returns every legacy `scream` shim along the
|
|
8
|
-
* way (in PATH order). A shim qualifies when it realpath-resolves
|
|
9
|
-
* outside our own installed package root and its head 4 KiB
|
|
10
|
-
* contains the `scream_cli` module marker — the setuptools
|
|
11
|
-
* entry-point format produced by `uv tool install`,
|
|
12
|
-
* `pipx install`, `pip install`, etc. Returning all hits (not
|
|
13
|
-
* just the first) matters because a user with both uv- and
|
|
14
|
-
* pipx-installed `scream-cli` has two legacy shims in different
|
|
15
|
-
* dirs, and renaming only the earlier one leaves the later one
|
|
16
|
-
* shadowing our new CLI. Callers should pass the `detection`
|
|
17
|
-
* field from `postinstallPaths()` so detection sees the union of
|
|
18
|
-
* the shell PATH and the installer's PATH.
|
|
19
|
-
* - {@link isLegacyShim}: same criterion in standalone form, used to
|
|
20
|
-
* decide whether an existing `scream-legacy` is itself a legacy CLI
|
|
21
|
-
* (safe to consolidate over) or a user-managed file (preserve).
|
|
22
|
-
*
|
|
23
|
-
* Classify + execute (two-phase):
|
|
24
|
-
* - {@link classifyShim}: pre-flight inspection that says what we
|
|
25
|
-
* COULD do to a given shim. Returns one of `renameable`,
|
|
26
|
-
* `consolidate`, `delete-only`, `blocked`. No filesystem writes.
|
|
27
|
-
* - {@link renameInPlace} / {@link deleteShim}: the primitive
|
|
28
|
-
* operations that actually mutate the filesystem. Run after the
|
|
29
|
-
* orchestrator has looked at the full set of classifications and
|
|
30
|
-
* decided to proceed.
|
|
31
|
-
*
|
|
32
|
-
* Splitting classification from execution lets the orchestrator make
|
|
33
|
-
* the abort-or-proceed decision once, against the whole detected set,
|
|
34
|
-
* rather than discovering mid-loop that something failed and ending up
|
|
35
|
-
* with a misleading "scream now launches the new CLI" notice in front of
|
|
36
|
-
* a "permission denied" notice. Uses `fs.lstat` (not `fs.access`) to
|
|
37
|
-
* detect dangling symlinks at the target so we don't clobber them.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
41
|
-
import { delimiter, dirname, extname, join, sep } from 'node:path';
|
|
42
|
-
|
|
43
|
-
const LEGACY_BIN = 'scream';
|
|
44
|
-
const LEGACY_RENAME = 'scream-legacy';
|
|
45
|
-
const PYTHON_MARKER = 'scream_cli';
|
|
46
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
47
|
-
|
|
48
|
-
// Read window for the marker sniff.
|
|
49
|
-
// POSIX: setuptools entry-point scripts are a few hundred bytes —
|
|
50
|
-
// 4 KiB is generous.
|
|
51
|
-
// Windows: `uv tool install` produces a Rust-built launcher .exe
|
|
52
|
-
// (~45 KiB) and the `scream_cli` module name sits embedded
|
|
53
|
-
// near the END of the file (verified offset ≈ 44103 on
|
|
54
|
-
// uv 0.11). Cap reads at 256 KiB so a hostile or
|
|
55
|
-
// unexpectedly large file can't make us hold a lot of
|
|
56
|
-
// memory.
|
|
57
|
-
const SHIM_SNIFF_BYTES_POSIX = 4096;
|
|
58
|
-
const SHIM_SNIFF_BYTES_WINDOWS_MAX = 256 * 1024;
|
|
59
|
-
|
|
60
|
-
function pathEntries(pathString) {
|
|
61
|
-
if (!pathString) return [];
|
|
62
|
-
const seen = new Set();
|
|
63
|
-
const out = [];
|
|
64
|
-
for (const entry of pathString.split(delimiter)) {
|
|
65
|
-
if (!entry || seen.has(entry)) continue;
|
|
66
|
-
seen.add(entry);
|
|
67
|
-
out.push(entry);
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Expand `scream` into the set of filenames that resolve as executables
|
|
74
|
-
* on this platform. POSIX → just `['scream']`. Windows → adds every
|
|
75
|
-
* `PATHEXT` extension (so we find `scream.exe`, `scream.cmd`, etc).
|
|
76
|
-
*/
|
|
77
|
-
function executableCandidates(basename) {
|
|
78
|
-
if (!IS_WINDOWS) return [basename];
|
|
79
|
-
const pathext = (process.env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM')
|
|
80
|
-
.toLowerCase()
|
|
81
|
-
.split(';')
|
|
82
|
-
.map((e) => e.trim())
|
|
83
|
-
.filter(Boolean);
|
|
84
|
-
return [basename, ...pathext.map((ext) => basename + ext)];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function isExecutableFile(filePath) {
|
|
88
|
-
try {
|
|
89
|
-
const info = await fs.stat(filePath);
|
|
90
|
-
if (!info.isFile()) return false;
|
|
91
|
-
// Windows: stat().mode doesn't reflect ACLs in any useful way.
|
|
92
|
-
// Callers already restrict to PATHEXT candidates, so existence
|
|
93
|
-
// suffices.
|
|
94
|
-
if (IS_WINDOWS) return true;
|
|
95
|
-
return (info.mode & 0o111) !== 0;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function readShimHead(filePath) {
|
|
102
|
-
let handle;
|
|
103
|
-
try {
|
|
104
|
-
handle = await fs.open(filePath, 'r');
|
|
105
|
-
const stat = await handle.stat();
|
|
106
|
-
const limit = IS_WINDOWS ? SHIM_SNIFF_BYTES_WINDOWS_MAX : SHIM_SNIFF_BYTES_POSIX;
|
|
107
|
-
const target = Math.min(stat.size, limit);
|
|
108
|
-
const buffer = Buffer.alloc(target);
|
|
109
|
-
const { bytesRead } = await handle.read(buffer, 0, target, 0);
|
|
110
|
-
// `latin1` is a 1-to-1 byte→char mapping; we're searching for an
|
|
111
|
-
// ASCII substring, so we don't want UTF-8 decoding to mangle the
|
|
112
|
-
// bytes around the marker.
|
|
113
|
-
return buffer.subarray(0, bytesRead).toString('latin1');
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
} finally {
|
|
117
|
-
if (handle) await handle.close().catch(() => {});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Walk `pathString` and return every legacy `scream` shim along the
|
|
123
|
-
* way, in PATH order. The orchestrator renames each in turn — a
|
|
124
|
-
* single rename is insufficient when the user has multiple legacy
|
|
125
|
-
* installs (e.g. one from `uv tool install` and another from `pipx
|
|
126
|
-
* install`) in different directories, because the survivor still
|
|
127
|
-
* shadows the new CLI.
|
|
128
|
-
*
|
|
129
|
-
* Each entry has the same shape as the previous single-return
|
|
130
|
-
* value: `{ shimPath, realPath }`. The empty array means
|
|
131
|
-
* "fresh-install / no-op".
|
|
132
|
-
*/
|
|
133
|
-
export async function detectLegacyShims(ownRoot, pathString) {
|
|
134
|
-
const ownRootPrefix = ownRoot ? ownRoot + sep : null;
|
|
135
|
-
const candidates = executableCandidates(LEGACY_BIN);
|
|
136
|
-
const results = [];
|
|
137
|
-
const seenShims = new Set();
|
|
138
|
-
|
|
139
|
-
for (const dir of pathEntries(pathString)) {
|
|
140
|
-
for (const name of candidates) {
|
|
141
|
-
const shimPath = join(dir, name);
|
|
142
|
-
if (seenShims.has(shimPath)) continue;
|
|
143
|
-
if (!(await isExecutableFile(shimPath))) continue;
|
|
144
|
-
|
|
145
|
-
let realPath;
|
|
146
|
-
try {
|
|
147
|
-
realPath = await fs.realpath(shimPath);
|
|
148
|
-
} catch {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Defence-in-depth: never touch a `scream` that resolves into our
|
|
153
|
-
// own installed package. The `scream_cli` marker check below
|
|
154
|
-
// already excludes the manager's generated wrapper today, but
|
|
155
|
-
// this layer keeps us safe if anything in our bundle ever
|
|
156
|
-
// happens to contain the marker substring.
|
|
157
|
-
if (
|
|
158
|
-
ownRootPrefix !== null &&
|
|
159
|
-
(realPath === ownRoot || realPath.startsWith(ownRootPrefix))
|
|
160
|
-
) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const head = await readShimHead(realPath);
|
|
165
|
-
if (!head || !head.includes(PYTHON_MARKER)) continue;
|
|
166
|
-
|
|
167
|
-
seenShims.add(shimPath);
|
|
168
|
-
results.push({ shimPath, realPath });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return results;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Does the file at `p` look like the legacy Python `scream-cli`?
|
|
176
|
-
*
|
|
177
|
-
* Same criterion as {@link detectLegacyShim} uses to recognize the
|
|
178
|
-
* original shim: realpath-resolvable and the first 4 KiB of the
|
|
179
|
-
* resolved file contains the `scream_cli` module name. Used to decide
|
|
180
|
-
* whether an existing `scream-legacy` is itself a legacy shim (safe to
|
|
181
|
-
* drop the duplicate `scream`) or a user-managed file we must not
|
|
182
|
-
* clobber.
|
|
183
|
-
*/
|
|
184
|
-
export async function isLegacyShim(p) {
|
|
185
|
-
let real;
|
|
186
|
-
try {
|
|
187
|
-
real = await fs.realpath(p);
|
|
188
|
-
} catch {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
const head = await readShimHead(real);
|
|
192
|
-
return Boolean(head && head.includes(PYTHON_MARKER));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function pathExists(p) {
|
|
196
|
-
try {
|
|
197
|
-
// lstat (not access/stat) so a dangling symlink at `p` still
|
|
198
|
-
// reports as existing — `fs.access` follows symlinks and would
|
|
199
|
-
// return ENOENT for a broken link, after which `fs.rename` would
|
|
200
|
-
// silently replace it.
|
|
201
|
-
await fs.lstat(p);
|
|
202
|
-
return true;
|
|
203
|
-
} catch {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Compute where `shimPath` should be renamed to. Preserves the file
|
|
210
|
-
* extension so a Windows `C:\…\scream.exe` ends up at `scream-legacy.exe`
|
|
211
|
-
* rather than an extension-less `scream-legacy` that `scream.exe -- legacy`
|
|
212
|
-
* shells won't run.
|
|
213
|
-
*/
|
|
214
|
-
function renameTargetFor(shimPath) {
|
|
215
|
-
const ext = extname(shimPath); // "" on POSIX, ".exe" on Windows
|
|
216
|
-
return join(dirname(shimPath), LEGACY_RENAME + ext);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Is the directory containing `shimPath` a system-managed location
|
|
221
|
-
* the current user can't write to?
|
|
222
|
-
*
|
|
223
|
-
* POSIX: dir owned by uid 0 (root) — captures
|
|
224
|
-
* `sudo pip install scream-cli` → `/usr/local/bin/`.
|
|
225
|
-
*
|
|
226
|
-
* Windows: dir under one of the well-known system roots
|
|
227
|
-
* (`C:\Program Files`, `C:\ProgramData`, `C:\Windows`). uv tool
|
|
228
|
-
* installs land in user space (`%USERPROFILE%\.local\bin`) so this
|
|
229
|
-
* path almost never fires on Windows in practice, but the heuristic
|
|
230
|
-
* correctly classifies the rare admin-prefix install case.
|
|
231
|
-
*
|
|
232
|
-
* The dedicated permission-denied notice (`logPermissionDenied`)
|
|
233
|
-
* uses this to switch from a bare "rename it manually" message to a
|
|
234
|
-
* sudo-aware / admin-aware explanation.
|
|
235
|
-
*/
|
|
236
|
-
async function isSystemOwnedDir(shimPath) {
|
|
237
|
-
if (IS_WINDOWS) {
|
|
238
|
-
const dir = dirname(shimPath).toLowerCase();
|
|
239
|
-
const systemRoots = [
|
|
240
|
-
'c:\\program files',
|
|
241
|
-
'c:\\program files (x86)',
|
|
242
|
-
'c:\\programdata',
|
|
243
|
-
'c:\\windows',
|
|
244
|
-
];
|
|
245
|
-
return systemRoots.some(
|
|
246
|
-
(root) => dir === root || dir.startsWith(root + '\\'),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
const info = await fs.stat(dirname(shimPath));
|
|
251
|
-
return info.uid === 0;
|
|
252
|
-
} catch {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function canWriteDir(dir) {
|
|
258
|
-
try {
|
|
259
|
-
// For `fs.rename` and `fs.unlink` the parent dir needs to be both
|
|
260
|
-
// writable and executable (POSIX `wx`). `fs.access` follows the
|
|
261
|
-
// same semantics. On Windows ACL details are coarse but
|
|
262
|
-
// `fs.access(W_OK)` is a reasonable best-effort.
|
|
263
|
-
await fs.access(dir, fsConstants.W_OK | fsConstants.X_OK);
|
|
264
|
-
return true;
|
|
265
|
-
} catch {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Pre-flight inspection of a single legacy shim. Returns what action
|
|
272
|
-
* we could take WITHOUT executing it. The orchestrator uses these
|
|
273
|
-
* classifications to decide the whole-set strategy (abort vs proceed,
|
|
274
|
-
* which shim becomes scream-legacy) before any filesystem writes
|
|
275
|
-
* happen.
|
|
276
|
-
*
|
|
277
|
-
* Result shapes (all carry `shimPath` and `target` so the renderer
|
|
278
|
-
* has the paths):
|
|
279
|
-
* - `renameable` : can `fs.rename(shim → target)` cleanly;
|
|
280
|
-
* target slot is free.
|
|
281
|
-
* - `consolidate` : target already exists and is itself a legacy
|
|
282
|
-
* shim; we'd `fs.unlink(shim)` and leave the
|
|
283
|
-
* existing `scream-legacy` as the canonical
|
|
284
|
-
* fallback (functionally equivalent — same
|
|
285
|
-
* upstream package).
|
|
286
|
-
* - `delete-only` : target exists but is a user-managed file we
|
|
287
|
-
* won't clobber. We can still `fs.unlink(shim)`
|
|
288
|
-
* to stop the shadowing; the
|
|
289
|
-
* "preserve original scream" invariant fails for
|
|
290
|
-
* THIS dir (we tell the user).
|
|
291
|
-
* - `blocked` : we can't write to the parent dir. Carries
|
|
292
|
-
* `isSystemPath` so the renderer can suggest
|
|
293
|
-
* sudo (POSIX) or admin PowerShell (Windows).
|
|
294
|
-
*/
|
|
295
|
-
export async function classifyShim(shimPath) {
|
|
296
|
-
const target = renameTargetFor(shimPath);
|
|
297
|
-
const dir = dirname(shimPath);
|
|
298
|
-
|
|
299
|
-
if (!(await canWriteDir(dir))) {
|
|
300
|
-
return {
|
|
301
|
-
kind: 'blocked',
|
|
302
|
-
shimPath,
|
|
303
|
-
target,
|
|
304
|
-
isSystemPath: await isSystemOwnedDir(shimPath),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (await pathExists(target)) {
|
|
309
|
-
if (await isLegacyShim(target)) {
|
|
310
|
-
return { kind: 'consolidate', shimPath, target };
|
|
311
|
-
}
|
|
312
|
-
return { kind: 'delete-only', shimPath, target };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return { kind: 'renameable', shimPath, target };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Execute an `fs.rename`. Pre-flight classification establishes
|
|
320
|
-
* whether this is expected to succeed; this primitive just runs the
|
|
321
|
-
* call and wraps the error.
|
|
322
|
-
*/
|
|
323
|
-
export async function renameInPlace(shimPath, target) {
|
|
324
|
-
try {
|
|
325
|
-
await fs.rename(shimPath, target);
|
|
326
|
-
return { success: true };
|
|
327
|
-
} catch (err) {
|
|
328
|
-
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
329
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
330
|
-
return { success: false, code, message };
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Execute an `fs.unlink`. Used when:
|
|
336
|
-
* - we're consolidating onto an existing legacy `scream-legacy`, or
|
|
337
|
-
* - we couldn't rename (foreign target) but can still remove the
|
|
338
|
-
* shadow, or
|
|
339
|
-
* - this is a non-first shim and we just want to clear it out so
|
|
340
|
-
* PATH order resolves to our new CLI.
|
|
341
|
-
*/
|
|
342
|
-
export async function deleteShim(shimPath) {
|
|
343
|
-
try {
|
|
344
|
-
await fs.unlink(shimPath);
|
|
345
|
-
return { success: true };
|
|
346
|
-
} catch (err) {
|
|
347
|
-
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
348
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
349
|
-
return { success: false, code, message };
|
|
350
|
-
}
|
|
351
|
-
}
|