lshed 0.3.0 → 0.4.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.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -4
- package/dist/cli.js +458 -165
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-09-03
|
|
4
|
+
|
|
5
|
+
Hand-configured MCP servers travel with the profile. Secret values do not.
|
|
6
|
+
|
|
7
|
+
- New category `mcp` for Claude Code: each user-scope server in `~/.claude.json` becomes `mcp/<name>.json` in the shed. `restore` writes only `mcpServers.<name>` and leaves the rest of the file (machine ID, session state, servers you added by hand) untouched. The file is replaced atomically.
|
|
8
|
+
- `init` masks values under `env` and `headers` whose key looks like a secret with `${VAR}` (`EXA_API_KEY` → `${EXA_API_KEY}`, `Authorization: Bearer …` → `Bearer ${NOTION_AUTHORIZATION}`). Claude Code expands `${VAR}` from the environment in every scope, so `restore` places the placeholder verbatim and no secret passes through lshed. Verified against the real binary.
|
|
9
|
+
- `restore` and `status` list the variables a profile needs that are not set in the current shell.
|
|
10
|
+
- `diff` shows entry changes by key path; `save` keeps placeholders that still match and masks newly added secret-looking keys.
|
|
11
|
+
- Profile switches remove only the servers lshed placed, backing each up as JSON.
|
|
12
|
+
- Adapter interface: `entries()` returns categories that live as JSON entries instead of files, with `secretKeys` and `expandsEnv` policy. The default root honours `CLAUDE_CONFIG_DIR`.
|
|
13
|
+
- Managed-set paths for entries are written as `mcp:<name>`; a colon cannot appear in a path segment, so they never collide with files.
|
|
14
|
+
|
|
3
15
|
## 0.3.0 — 2026-09-02
|
|
4
16
|
|
|
5
17
|
Claude Code plugins are packages now. On the machine this was built against, the five installed plugins were the only thing a fresh `restore` still left out, and two of them carry MCP servers.
|
package/README.md
CHANGED
|
@@ -64,12 +64,15 @@ components:
|
|
|
64
64
|
instructions:
|
|
65
65
|
- id: base # file:./instructions/base.md
|
|
66
66
|
- id: research-style
|
|
67
|
+
mcp:
|
|
68
|
+
- id: exa # file:./mcp/exa.json — secrets replaced by ${VAR}
|
|
67
69
|
|
|
68
70
|
profiles:
|
|
69
71
|
research:
|
|
70
72
|
skills: [paper-review]
|
|
71
73
|
agents: [reviewer]
|
|
72
74
|
instructions: [base, research-style] # order matters
|
|
75
|
+
mcp: [exa]
|
|
73
76
|
teaching:
|
|
74
77
|
skills: [grading-helper]
|
|
75
78
|
commands: [summarize]
|
|
@@ -77,10 +80,25 @@ profiles:
|
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
- Component `source` accepts `file:<path relative to the shed>`. Package `source` accepts `github:owner/repo@ref` or `git:<url>#ref`.
|
|
80
|
-
- Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`.
|
|
83
|
+
- Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`, `mcp`.
|
|
81
84
|
- `ignore:` at the top level adds to the built-in list of things never copied: `node_modules`, `.git`, `__pycache__`, `.venv`, cache directories, `*.log`. Build output like `dist/` is not ignored by default, since some skills ship it. Add it yourself if your parts rebuild from source.
|
|
82
85
|
- Instructions are not merged. `restore` writes a `CLAUDE.md` that `@`-imports each fragment in order, so a fragment edit shows up without re-running anything. Your original `CLAUDE.md` is backed up the first time.
|
|
83
86
|
|
|
87
|
+
## MCP servers and secrets
|
|
88
|
+
|
|
89
|
+
User-scope MCP servers live in `~/.claude.json`, next to machine IDs and session state. lshed treats each server as a component of category `mcp`: the shed holds `mcp/<name>.json`, and `restore` edits only the `mcpServers.<name>` key of `~/.claude.json`, leaving everything else in that file alone.
|
|
90
|
+
|
|
91
|
+
**No secret value enters the shed.** `init` replaces values under `env` and `headers` whose key looks like a secret (`key`, `token`, `secret`, `pass`, `auth`, `credential`, `cookie`, `session`) with a `${VAR}` placeholder:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{ "type": "stdio", "command": "npx", "args": ["-y", "exa-mcp-server"],
|
|
95
|
+
"env": { "EXA_API_KEY": "${EXA_API_KEY}" } }
|
|
96
|
+
{ "type": "http", "url": "https://mcp.notion.com/mcp",
|
|
97
|
+
"headers": { "Authorization": "Bearer ${NOTION_AUTHORIZATION}" } }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`restore` writes the placeholder as is. Claude Code expands `${VAR}` from the environment when it starts the server, so the value only ever lives in your shell (`export EXA_API_KEY=...` in `~/.zshrc`, or however you manage secrets). `restore` and `status` list the variables the profile needs that are not set. The heuristic is a suggestion: edit the JSON in the shed to add or remove placeholders, and `init` warns when something in `args` or `url` looks like a token. `save` keeps existing placeholders and masks new secret-looking keys, so a rotated key never leaks into the shed by accident.
|
|
101
|
+
|
|
84
102
|
## Three kinds of things
|
|
85
103
|
|
|
86
104
|
A real `~/.claude` mixes three kinds of content, and they need different handling:
|
|
@@ -168,22 +186,24 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
|
|
|
168
186
|
<shed>/
|
|
169
187
|
lshed.yaml
|
|
170
188
|
skills/<id>/ agents/<id>.md commands/<id>.md instructions/<id>.md
|
|
189
|
+
mcp/<id>.json ← secrets as ${VAR}
|
|
171
190
|
|
|
172
191
|
~/.claude/
|
|
173
192
|
skills/ agents/ commands/ CLAUDE.md ← placed by restore
|
|
193
|
+
~/.claude.json mcpServers.<id> ← one key per mcp component; the rest of the file is untouched
|
|
174
194
|
lshed/state.json ← which profile, which paths are managed
|
|
175
195
|
lshed/instructions/<id>.md ← fragments imported by CLAUDE.md
|
|
176
196
|
lshed/backups/<timestamp>/ ← whatever restore replaced
|
|
177
197
|
```
|
|
178
198
|
|
|
179
|
-
`state.json` is per machine and is not part of the shed.
|
|
199
|
+
`state.json` is per machine and is not part of the shed. If `CLAUDE_CONFIG_DIR` is set, lshed uses it as the root and expects `.claude.json` inside it, as Claude Code does.
|
|
180
200
|
|
|
181
201
|
## Not in scope (yet)
|
|
182
202
|
|
|
183
|
-
-
|
|
203
|
+
- Secrets beyond "name the variable". Encrypted values, `op://` references and OS keychains are possible later; today lshed is deliberately no better than dotfiles here.
|
|
204
|
+
- Project-scope MCP servers (`.mcp.json`, `~/.claude.json` `projects.*`). They belong to the project.
|
|
184
205
|
- `settings.json` merging (hooks, permissions).
|
|
185
206
|
- `sync` (a git pull/push wrapper). Use git in the shed directly for now.
|
|
186
|
-
- MCP servers configured by hand in `~/.claude.json`. Plugin-bundled ones are covered.
|
|
187
207
|
- Windows and macOS have not been tested. The code avoids platform-specific paths, but treat 0.1 as Linux/WSL.
|
|
188
208
|
|
|
189
209
|
## Troubleshooting
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import os2 from "os";
|
|
7
|
-
import
|
|
7
|
+
import path17 from "path";
|
|
8
8
|
|
|
9
9
|
// src/adapters/claude-code.ts
|
|
10
|
-
import { promises as
|
|
11
|
-
import
|
|
10
|
+
import { promises as fs4 } from "fs";
|
|
11
|
+
import path4 from "path";
|
|
12
12
|
import os from "os";
|
|
13
13
|
|
|
14
14
|
// src/installers/claude-plugin.ts
|
|
@@ -166,79 +166,14 @@ var pluginInstaller = {
|
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
// src/adapters/claude-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{ name: "agents", root: "agents", kind: "file" },
|
|
173
|
-
{ name: "commands", root: "commands", kind: "file" }
|
|
174
|
-
// instructions 는 단일 파일(CLAUDE.md)이라 스캔 대상이 아니라 생성 대상이다 (§3.3)
|
|
175
|
-
];
|
|
176
|
-
var ClaudeCodeAdapter = class {
|
|
177
|
-
name = "claude-code";
|
|
178
|
-
root;
|
|
179
|
-
constructor(root) {
|
|
180
|
-
this.root = root ?? path2.join(os.homedir(), ".claude");
|
|
181
|
-
}
|
|
182
|
-
categories() {
|
|
183
|
-
return CATEGORIES;
|
|
184
|
-
}
|
|
185
|
-
instructionsStrategy() {
|
|
186
|
-
return "import";
|
|
187
|
-
}
|
|
188
|
-
instructionsFileName() {
|
|
189
|
-
return "CLAUDE.md";
|
|
190
|
-
}
|
|
191
|
-
installers() {
|
|
192
|
-
return [marketplaceInstaller, pluginInstaller];
|
|
193
|
-
}
|
|
194
|
-
async scan() {
|
|
195
|
-
const out = [];
|
|
196
|
-
for (const cat of CATEGORIES) {
|
|
197
|
-
const dir = path2.join(this.root, cat.root);
|
|
198
|
-
let entries;
|
|
199
|
-
try {
|
|
200
|
-
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
201
|
-
} catch {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
for (const e of entries) {
|
|
205
|
-
if (e.name.startsWith(".")) continue;
|
|
206
|
-
const full = path2.join(dir, e.name);
|
|
207
|
-
let st;
|
|
208
|
-
try {
|
|
209
|
-
st = await fs2.stat(full);
|
|
210
|
-
} catch {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (cat.kind === "dir" && st.isDirectory()) {
|
|
214
|
-
out.push({ category: cat.name, id: e.name, path: full });
|
|
215
|
-
} else if (cat.kind === "file" && st.isFile() && e.name.endsWith(".md")) {
|
|
216
|
-
out.push({ category: cat.name, id: e.name.slice(0, -3), path: full });
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return out;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// src/core/context.ts
|
|
225
|
-
import { promises as fs6 } from "fs";
|
|
226
|
-
import path8 from "path";
|
|
227
|
-
import { spawn as spawn2 } from "child_process";
|
|
228
|
-
|
|
229
|
-
// src/installers/git.ts
|
|
230
|
-
import { promises as fs4 } from "fs";
|
|
231
|
-
import path5 from "path";
|
|
232
|
-
|
|
233
|
-
// src/git.ts
|
|
234
|
-
import { execFile, spawn } from "child_process";
|
|
235
|
-
import { promisify } from "util";
|
|
236
|
-
import path4 from "path";
|
|
169
|
+
// src/adapters/claude-mcp.ts
|
|
170
|
+
import { promises as fs3 } from "fs";
|
|
171
|
+
import path3 from "path";
|
|
237
172
|
|
|
238
173
|
// src/fsutil.ts
|
|
239
|
-
import { promises as
|
|
174
|
+
import { promises as fs2 } from "fs";
|
|
240
175
|
import { createHash } from "crypto";
|
|
241
|
-
import
|
|
176
|
+
import path2 from "path";
|
|
242
177
|
|
|
243
178
|
// src/ignore.ts
|
|
244
179
|
var DEFAULT_IGNORE = [
|
|
@@ -263,7 +198,7 @@ function isIgnored(rel, patterns) {
|
|
|
263
198
|
// src/fsutil.ts
|
|
264
199
|
async function exists(p) {
|
|
265
200
|
try {
|
|
266
|
-
await
|
|
201
|
+
await fs2.access(p);
|
|
267
202
|
return true;
|
|
268
203
|
} catch {
|
|
269
204
|
return false;
|
|
@@ -271,7 +206,7 @@ async function exists(p) {
|
|
|
271
206
|
}
|
|
272
207
|
async function isDir(p) {
|
|
273
208
|
try {
|
|
274
|
-
return (await
|
|
209
|
+
return (await fs2.stat(p)).isDirectory();
|
|
275
210
|
} catch {
|
|
276
211
|
return false;
|
|
277
212
|
}
|
|
@@ -280,48 +215,48 @@ async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
|
280
215
|
if (!await exists(root)) return [];
|
|
281
216
|
if (!await isDir(root)) return [""];
|
|
282
217
|
const out = [];
|
|
283
|
-
async function
|
|
284
|
-
const entries = await
|
|
218
|
+
async function walk2(dir, rel) {
|
|
219
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
285
220
|
for (const e of entries) {
|
|
286
221
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
287
222
|
if (isIgnored(r, ignore)) continue;
|
|
288
|
-
const full =
|
|
223
|
+
const full = path2.join(dir, e.name);
|
|
289
224
|
let st;
|
|
290
225
|
try {
|
|
291
|
-
st = await
|
|
226
|
+
st = await fs2.stat(full);
|
|
292
227
|
} catch {
|
|
293
228
|
continue;
|
|
294
229
|
}
|
|
295
|
-
if (st.isDirectory()) await
|
|
230
|
+
if (st.isDirectory()) await walk2(full, r);
|
|
296
231
|
else if (st.isFile()) out.push(r);
|
|
297
232
|
}
|
|
298
233
|
}
|
|
299
|
-
await
|
|
234
|
+
await walk2(root, "");
|
|
300
235
|
return out.sort();
|
|
301
236
|
}
|
|
302
237
|
async function hashFile(p) {
|
|
303
|
-
return createHash("sha256").update(await
|
|
238
|
+
return createHash("sha256").update(await fs2.readFile(p)).digest("hex");
|
|
304
239
|
}
|
|
305
240
|
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
306
241
|
if (!await exists(root)) return null;
|
|
307
242
|
const h = createHash("sha256");
|
|
308
243
|
for (const rel of await listFiles(root, ignore)) {
|
|
309
|
-
h.update(rel).update("\0").update(await
|
|
244
|
+
h.update(rel).update("\0").update(await fs2.readFile(rel ? path2.join(root, rel) : root)).update("\0");
|
|
310
245
|
}
|
|
311
246
|
return h.digest("hex");
|
|
312
247
|
}
|
|
313
248
|
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
314
|
-
await
|
|
315
|
-
await
|
|
316
|
-
const srcRoot =
|
|
317
|
-
await
|
|
249
|
+
await fs2.rm(dst, { recursive: true, force: true });
|
|
250
|
+
await fs2.mkdir(path2.dirname(dst), { recursive: true });
|
|
251
|
+
const srcRoot = path2.resolve(src);
|
|
252
|
+
await fs2.cp(src, dst, {
|
|
318
253
|
recursive: true,
|
|
319
254
|
dereference: true,
|
|
320
255
|
filter: async (from) => {
|
|
321
|
-
const rel =
|
|
256
|
+
const rel = path2.relative(srcRoot, path2.resolve(from)).split(path2.sep).join("/");
|
|
322
257
|
if (isIgnored(rel, ignore)) return false;
|
|
323
258
|
try {
|
|
324
|
-
if ((await
|
|
259
|
+
if ((await fs2.lstat(from)).isSymbolicLink()) await fs2.stat(from);
|
|
325
260
|
} catch {
|
|
326
261
|
return false;
|
|
327
262
|
}
|
|
@@ -330,15 +265,15 @@ async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
|
330
265
|
});
|
|
331
266
|
}
|
|
332
267
|
async function removeTree(p) {
|
|
333
|
-
await
|
|
268
|
+
await fs2.rm(p, { recursive: true, force: true });
|
|
334
269
|
}
|
|
335
270
|
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
336
271
|
const l = new Set(await listFiles(local, ignore));
|
|
337
272
|
const s = new Set(await listFiles(shed, ignore));
|
|
338
273
|
const out = [];
|
|
339
274
|
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
340
|
-
const lp = f ?
|
|
341
|
-
const sp = f ?
|
|
275
|
+
const lp = f ? path2.join(local, f) : local;
|
|
276
|
+
const sp = f ? path2.join(shed, f) : shed;
|
|
342
277
|
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
343
278
|
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
344
279
|
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
@@ -346,13 +281,127 @@ async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
|
346
281
|
return out;
|
|
347
282
|
}
|
|
348
283
|
|
|
284
|
+
// src/adapters/claude-mcp.ts
|
|
285
|
+
var ClaudeMcpEntries = class {
|
|
286
|
+
constructor(root) {
|
|
287
|
+
this.root = root;
|
|
288
|
+
}
|
|
289
|
+
root;
|
|
290
|
+
name = "mcp";
|
|
291
|
+
kind = "entry";
|
|
292
|
+
secretKeys = ["env", "headers"];
|
|
293
|
+
expandsEnv = true;
|
|
294
|
+
/** ~/.claude 의 형제 ~/.claude.json. CLAUDE_CONFIG_DIR 처럼 루트 안에 있으면 그것을 쓴다. */
|
|
295
|
+
async file() {
|
|
296
|
+
const inside = path3.join(this.root, ".claude.json");
|
|
297
|
+
return await exists(inside) ? inside : `${this.root}.json`;
|
|
298
|
+
}
|
|
299
|
+
async load() {
|
|
300
|
+
const p = await this.file();
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(await fs3.readFile(p, "utf8"));
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if (e.code === "ENOENT") return {};
|
|
305
|
+
throw new Error(`${p} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async read() {
|
|
309
|
+
const servers = (await this.load()).mcpServers;
|
|
310
|
+
return servers && typeof servers === "object" ? { ...servers } : {};
|
|
311
|
+
}
|
|
312
|
+
async write(id, value) {
|
|
313
|
+
const p = await this.file();
|
|
314
|
+
const all = await this.load();
|
|
315
|
+
const servers = { ...all.mcpServers ?? {} };
|
|
316
|
+
if (value === null) delete servers[id];
|
|
317
|
+
else servers[id] = value;
|
|
318
|
+
all.mcpServers = servers;
|
|
319
|
+
await fs3.mkdir(path3.dirname(p), { recursive: true });
|
|
320
|
+
const tmp = `${p}.lshed-${process.pid}.tmp`;
|
|
321
|
+
await fs3.writeFile(tmp, JSON.stringify(all, null, 2) + "\n");
|
|
322
|
+
await fs3.rename(tmp, p);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/adapters/claude-code.ts
|
|
327
|
+
var CATEGORIES = [
|
|
328
|
+
{ name: "skills", root: "skills", kind: "dir" },
|
|
329
|
+
{ name: "agents", root: "agents", kind: "file" },
|
|
330
|
+
{ name: "commands", root: "commands", kind: "file" }
|
|
331
|
+
// instructions 는 단일 파일(CLAUDE.md)이라 스캔 대상이 아니라 생성 대상이다 (§3.3)
|
|
332
|
+
];
|
|
333
|
+
var ClaudeCodeAdapter = class {
|
|
334
|
+
name = "claude-code";
|
|
335
|
+
root;
|
|
336
|
+
mcp;
|
|
337
|
+
constructor(root) {
|
|
338
|
+
this.root = root ?? process.env.CLAUDE_CONFIG_DIR ?? path4.join(os.homedir(), ".claude");
|
|
339
|
+
this.mcp = new ClaudeMcpEntries(this.root);
|
|
340
|
+
}
|
|
341
|
+
entries() {
|
|
342
|
+
return [this.mcp];
|
|
343
|
+
}
|
|
344
|
+
categories() {
|
|
345
|
+
return CATEGORIES;
|
|
346
|
+
}
|
|
347
|
+
instructionsStrategy() {
|
|
348
|
+
return "import";
|
|
349
|
+
}
|
|
350
|
+
instructionsFileName() {
|
|
351
|
+
return "CLAUDE.md";
|
|
352
|
+
}
|
|
353
|
+
installers() {
|
|
354
|
+
return [marketplaceInstaller, pluginInstaller];
|
|
355
|
+
}
|
|
356
|
+
async scan() {
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const cat of CATEGORIES) {
|
|
359
|
+
const dir = path4.join(this.root, cat.root);
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const e of entries) {
|
|
367
|
+
if (e.name.startsWith(".")) continue;
|
|
368
|
+
const full = path4.join(dir, e.name);
|
|
369
|
+
let st;
|
|
370
|
+
try {
|
|
371
|
+
st = await fs4.stat(full);
|
|
372
|
+
} catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (cat.kind === "dir" && st.isDirectory()) {
|
|
376
|
+
out.push({ category: cat.name, id: e.name, path: full });
|
|
377
|
+
} else if (cat.kind === "file" && st.isFile() && e.name.endsWith(".md")) {
|
|
378
|
+
out.push({ category: cat.name, id: e.name.slice(0, -3), path: full });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/core/context.ts
|
|
387
|
+
import { promises as fs7 } from "fs";
|
|
388
|
+
import path9 from "path";
|
|
389
|
+
import { spawn as spawn2 } from "child_process";
|
|
390
|
+
|
|
391
|
+
// src/installers/git.ts
|
|
392
|
+
import { promises as fs5 } from "fs";
|
|
393
|
+
import path6 from "path";
|
|
394
|
+
|
|
349
395
|
// src/git.ts
|
|
396
|
+
import { execFile, spawn } from "child_process";
|
|
397
|
+
import { promisify } from "util";
|
|
398
|
+
import path5 from "path";
|
|
350
399
|
var x = promisify(execFile);
|
|
351
400
|
async function git(args, cwd) {
|
|
352
401
|
const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
|
|
353
402
|
return stdout.trim();
|
|
354
403
|
}
|
|
355
|
-
var isRepo = (dir) => exists(
|
|
404
|
+
var isRepo = (dir) => exists(path5.join(dir, ".git"));
|
|
356
405
|
var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
|
|
357
406
|
var head = (dir) => git(["rev-parse", "HEAD"], dir);
|
|
358
407
|
var branch = async (dir) => {
|
|
@@ -373,7 +422,7 @@ function runShell(cmd, cwd) {
|
|
|
373
422
|
// src/installers/git.ts
|
|
374
423
|
function dirOf(ctx, pkg) {
|
|
375
424
|
if (!pkg.into) throw new Error(`package ${pkg.id}: git \uACC4\uC5F4 \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
|
|
376
|
-
return
|
|
425
|
+
return path6.join(ctx.adapter.root, ...pkg.into.split("/"));
|
|
377
426
|
}
|
|
378
427
|
var gitInstaller = {
|
|
379
428
|
name: "git",
|
|
@@ -385,7 +434,7 @@ var gitInstaller = {
|
|
|
385
434
|
if (!await isRepo(f.path)) continue;
|
|
386
435
|
const url = await remoteUrl(f.path);
|
|
387
436
|
if (!url) continue;
|
|
388
|
-
const into =
|
|
437
|
+
const into = path6.relative(ctx.adapter.root, f.path).split(path6.sep).join("/");
|
|
389
438
|
out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), rev: await head(f.path), path: f.path });
|
|
390
439
|
}
|
|
391
440
|
return out;
|
|
@@ -399,7 +448,7 @@ var gitInstaller = {
|
|
|
399
448
|
const dir = dirOf(ctx, pkg);
|
|
400
449
|
if (await exists(dir)) throw new Error(`package ${pkg.id}: ${dir} \uAC00 \uC788\uC9C0\uB9CC git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uCE58\uC6B0\uAC70\uB098 into \uB97C \uBC14\uAFB8\uC138\uC694.`);
|
|
401
450
|
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
402
|
-
await
|
|
451
|
+
await fs5.mkdir(path6.dirname(dir), { recursive: true });
|
|
403
452
|
await clone(url, dir, ref);
|
|
404
453
|
if (locked) {
|
|
405
454
|
await resetHard(dir, locked).catch(() => {
|
|
@@ -506,7 +555,8 @@ ${problems.map((p) => " " + p).join("\n")}`);
|
|
|
506
555
|
return m;
|
|
507
556
|
}
|
|
508
557
|
function effectiveSource(category, c, kind = "dir") {
|
|
509
|
-
|
|
558
|
+
const ext = kind === "file" ? ".md" : kind === "entry" ? ".json" : "";
|
|
559
|
+
return c.source ?? `file:./${category}/${c.id}${ext}`;
|
|
510
560
|
}
|
|
511
561
|
function stringifyManifest(m) {
|
|
512
562
|
const out = { ...m };
|
|
@@ -520,10 +570,10 @@ function packagesOf(m, profile) {
|
|
|
520
570
|
}
|
|
521
571
|
|
|
522
572
|
// src/resolvers/file.ts
|
|
523
|
-
import
|
|
573
|
+
import path7 from "path";
|
|
524
574
|
function resolveSource(shed, raw) {
|
|
525
575
|
const s = parseSource(raw);
|
|
526
|
-
if (s.scheme === "file") return
|
|
576
|
+
if (s.scheme === "file") return path7.resolve(shed, s.path);
|
|
527
577
|
throw new Error(`"${raw}": ${s.scheme}: \uCD9C\uCC98\uB294 v0.2\uC5D0\uC11C \uC9C0\uC6D0\uB429\uB2C8\uB2E4. \uC9C0\uAE08\uC740 file: \uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
528
578
|
}
|
|
529
579
|
function isSaveable(raw) {
|
|
@@ -531,8 +581,8 @@ function isSaveable(raw) {
|
|
|
531
581
|
}
|
|
532
582
|
|
|
533
583
|
// src/state.ts
|
|
534
|
-
import { promises as
|
|
535
|
-
import
|
|
584
|
+
import { promises as fs6 } from "fs";
|
|
585
|
+
import path8 from "path";
|
|
536
586
|
import { z as z2 } from "zod";
|
|
537
587
|
var StateSchema = z2.object({
|
|
538
588
|
profile: z2.string(),
|
|
@@ -543,11 +593,11 @@ var StateSchema = z2.object({
|
|
|
543
593
|
});
|
|
544
594
|
var LSHED_DIR = "lshed";
|
|
545
595
|
function statePath(adapter) {
|
|
546
|
-
return
|
|
596
|
+
return path8.join(adapter.root, LSHED_DIR, "state.json");
|
|
547
597
|
}
|
|
548
598
|
async function readState(adapter) {
|
|
549
599
|
try {
|
|
550
|
-
const raw = JSON.parse(await
|
|
600
|
+
const raw = JSON.parse(await fs6.readFile(statePath(adapter), "utf8"));
|
|
551
601
|
return StateSchema.parse(raw);
|
|
552
602
|
} catch (e) {
|
|
553
603
|
if (e.code === "ENOENT") return null;
|
|
@@ -556,8 +606,8 @@ async function readState(adapter) {
|
|
|
556
606
|
}
|
|
557
607
|
async function writeState(adapter, state) {
|
|
558
608
|
const p = statePath(adapter);
|
|
559
|
-
await
|
|
560
|
-
await
|
|
609
|
+
await fs6.mkdir(path8.dirname(p), { recursive: true });
|
|
610
|
+
await fs6.writeFile(p, JSON.stringify(state, null, 2) + "\n");
|
|
561
611
|
}
|
|
562
612
|
|
|
563
613
|
// src/core/context.ts
|
|
@@ -582,15 +632,15 @@ var MANIFEST_FILE = "lshed.yaml";
|
|
|
582
632
|
var INSTRUCTIONS = "instructions";
|
|
583
633
|
var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
|
|
584
634
|
function manifestPath(ctx) {
|
|
585
|
-
return
|
|
635
|
+
return path9.join(ctx.shed, MANIFEST_FILE);
|
|
586
636
|
}
|
|
587
637
|
function knownCategories(adapter) {
|
|
588
|
-
return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
|
|
638
|
+
return [...adapter.categories().map((c) => c.name), ...adapter.entries().map((e) => e.name), INSTRUCTIONS];
|
|
589
639
|
}
|
|
590
640
|
async function loadManifest(ctx) {
|
|
591
641
|
let text;
|
|
592
642
|
try {
|
|
593
|
-
text = await
|
|
643
|
+
text = await fs7.readFile(manifestPath(ctx), "utf8");
|
|
594
644
|
} catch {
|
|
595
645
|
throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
|
|
596
646
|
\uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
|
|
@@ -604,12 +654,22 @@ function ignoreOf(ctx) {
|
|
|
604
654
|
}
|
|
605
655
|
function targetRel(cat, id) {
|
|
606
656
|
if (cat === INSTRUCTIONS) return `${FRAGMENTS_DIR}/${id}.md`;
|
|
657
|
+
if (cat.kind === "entry") return `${cat.name}:${id}`;
|
|
607
658
|
return cat.kind === "dir" ? `${cat.root}/${id}` : `${cat.root}/${id}.md`;
|
|
608
659
|
}
|
|
660
|
+
function entryOf(ctx, rel) {
|
|
661
|
+
const i = rel.indexOf(":");
|
|
662
|
+
if (i <= 0) return void 0;
|
|
663
|
+
const cat = ctx.adapter.entries().find((e) => e.name === rel.slice(0, i));
|
|
664
|
+
return cat ? { cat, id: rel.slice(i + 1) } : void 0;
|
|
665
|
+
}
|
|
666
|
+
function kindOf(ctx, category) {
|
|
667
|
+
if (category === INSTRUCTIONS) return "file";
|
|
668
|
+
if (ctx.adapter.entries().some((e) => e.name === category)) return "entry";
|
|
669
|
+
return ctx.adapter.categories().find((k) => k.name === category)?.kind ?? "dir";
|
|
670
|
+
}
|
|
609
671
|
function sourcePath(ctx, category, c) {
|
|
610
|
-
|
|
611
|
-
const kind = category === INSTRUCTIONS ? "file" : cat?.kind ?? "dir";
|
|
612
|
-
return resolveSource(ctx.shed, effectiveSource(category, c, kind));
|
|
672
|
+
return resolveSource(ctx.shed, effectiveSource(category, c, kindOf(ctx, category)));
|
|
613
673
|
}
|
|
614
674
|
function findComponent(m, category, id) {
|
|
615
675
|
const c = (m.components[category] ?? []).find((x2) => x2.id === id);
|
|
@@ -625,28 +685,29 @@ function planProfile(ctx, m, profile) {
|
|
|
625
685
|
const items = [];
|
|
626
686
|
for (const [category, ids] of Object.entries(p)) {
|
|
627
687
|
if (category === PACKAGES) continue;
|
|
628
|
-
const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category);
|
|
688
|
+
const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category) ?? ctx.adapter.entries().find((e) => e.name === category);
|
|
629
689
|
if (!cat) throw new Error(`\uD504\uB85C\uD544 "${profile}": \uC5B4\uB311\uD130 ${ctx.adapter.name} \uC740 \uCE74\uD14C\uACE0\uB9AC "${category}" \uB97C \uBAA8\uB985\uB2C8\uB2E4`);
|
|
690
|
+
const entry = cat !== INSTRUCTIONS && cat.kind === "entry" ? cat : void 0;
|
|
630
691
|
for (const id of ids) {
|
|
631
692
|
const component = findComponent(m, category, id);
|
|
632
|
-
items.push({ category, id, rel: targetRel(cat, id), src: sourcePath(ctx, category, component), component });
|
|
693
|
+
items.push({ category, id, rel: targetRel(cat, id), src: sourcePath(ctx, category, component), component, entry });
|
|
633
694
|
}
|
|
634
695
|
}
|
|
635
696
|
return items;
|
|
636
697
|
}
|
|
637
698
|
function abs(ctx, rel) {
|
|
638
|
-
return
|
|
699
|
+
return path9.join(ctx.adapter.root, ...rel.split("/"));
|
|
639
700
|
}
|
|
640
701
|
|
|
641
702
|
// src/core/init.ts
|
|
642
|
-
import { promises as
|
|
643
|
-
import
|
|
703
|
+
import { promises as fs11 } from "fs";
|
|
704
|
+
import path14 from "path";
|
|
644
705
|
|
|
645
706
|
// src/core/instructions.ts
|
|
646
|
-
import
|
|
707
|
+
import path10 from "path";
|
|
647
708
|
var MARKER = "<!-- generated by lshed";
|
|
648
709
|
function instructionsFile(ctx) {
|
|
649
|
-
return
|
|
710
|
+
return path10.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
|
|
650
711
|
}
|
|
651
712
|
function isGenerated(text) {
|
|
652
713
|
return text.trimStart().startsWith(MARKER);
|
|
@@ -665,12 +726,12 @@ ${f.content.trimEnd()}
|
|
|
665
726
|
}
|
|
666
727
|
|
|
667
728
|
// src/core/packages.ts
|
|
668
|
-
import { promises as
|
|
669
|
-
import
|
|
729
|
+
import { promises as fs9 } from "fs";
|
|
730
|
+
import path12 from "path";
|
|
670
731
|
|
|
671
732
|
// src/lock.ts
|
|
672
|
-
import { promises as
|
|
673
|
-
import
|
|
733
|
+
import { promises as fs8 } from "fs";
|
|
734
|
+
import path11 from "path";
|
|
674
735
|
import YAML2 from "yaml";
|
|
675
736
|
import { z as z3 } from "zod";
|
|
676
737
|
var EntrySchema = z3.object({ source: z3.string(), rev: z3.string().optional(), commit: z3.string().optional() }).transform((e) => ({ source: e.source, rev: e.rev ?? e.commit ?? "" }));
|
|
@@ -681,7 +742,7 @@ var LockSchema = z3.object({
|
|
|
681
742
|
var LOCK_FILE = "lshed.lock";
|
|
682
743
|
async function readLock(shed) {
|
|
683
744
|
try {
|
|
684
|
-
return LockSchema.parse(YAML2.parse(await
|
|
745
|
+
return LockSchema.parse(YAML2.parse(await fs8.readFile(path11.join(shed, LOCK_FILE), "utf8")));
|
|
685
746
|
} catch (e) {
|
|
686
747
|
if (e.code === "ENOENT") return { version: 1, packages: {} };
|
|
687
748
|
throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
@@ -689,7 +750,7 @@ async function readLock(shed) {
|
|
|
689
750
|
}
|
|
690
751
|
async function writeLock(shed, lock) {
|
|
691
752
|
const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
|
|
692
|
-
await
|
|
753
|
+
await fs8.writeFile(path11.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
|
|
693
754
|
}
|
|
694
755
|
|
|
695
756
|
// src/core/packages.ts
|
|
@@ -702,26 +763,26 @@ async function detectGenerated(found, pkgs) {
|
|
|
702
763
|
const out = /* @__PURE__ */ new Map();
|
|
703
764
|
const located = pkgs.filter((p) => p.path);
|
|
704
765
|
if (!located.length) return out;
|
|
705
|
-
const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await
|
|
766
|
+
const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await fs9.realpath(p.path) })));
|
|
706
767
|
for (const f of found) {
|
|
707
768
|
if (located.some((p) => p.path === f.path)) continue;
|
|
708
769
|
let entries;
|
|
709
770
|
try {
|
|
710
|
-
if (!(await
|
|
711
|
-
entries = await
|
|
771
|
+
if (!(await fs9.stat(f.path)).isDirectory()) continue;
|
|
772
|
+
entries = await fs9.readdir(f.path);
|
|
712
773
|
} catch {
|
|
713
774
|
continue;
|
|
714
775
|
}
|
|
715
776
|
for (const name of entries) {
|
|
716
|
-
const p =
|
|
777
|
+
const p = path12.join(f.path, name);
|
|
717
778
|
let target;
|
|
718
779
|
try {
|
|
719
|
-
if (!(await
|
|
720
|
-
target = await
|
|
780
|
+
if (!(await fs9.lstat(p)).isSymbolicLink()) continue;
|
|
781
|
+
target = await fs9.realpath(p).catch(async () => path12.resolve(f.path, await fs9.readlink(p)));
|
|
721
782
|
} catch {
|
|
722
783
|
continue;
|
|
723
784
|
}
|
|
724
|
-
const owner = roots.find((r) => target === r.real || target.startsWith(r.real +
|
|
785
|
+
const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path12.sep));
|
|
725
786
|
if (owner) {
|
|
726
787
|
out.set(`${f.category}/${f.id}`, owner.id);
|
|
727
788
|
break;
|
|
@@ -766,7 +827,7 @@ async function ensurePackages(ctx, pkgs, opts = {}) {
|
|
|
766
827
|
async function maybeInstall(ctx, pkg, dir, opts, res) {
|
|
767
828
|
if (!pkg.install) return;
|
|
768
829
|
if (opts.yes) {
|
|
769
|
-
ctx.log(` $ (${
|
|
830
|
+
ctx.log(` $ (${path12.relative(ctx.adapter.root, dir) || "."}) ${pkg.install}`);
|
|
770
831
|
await runShell(pkg.install, dir);
|
|
771
832
|
} else {
|
|
772
833
|
res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
|
|
@@ -807,6 +868,132 @@ async function updatePackages(ctx, pkgs, opts = {}) {
|
|
|
807
868
|
return res;
|
|
808
869
|
}
|
|
809
870
|
|
|
871
|
+
// src/core/entries.ts
|
|
872
|
+
import { promises as fs10 } from "fs";
|
|
873
|
+
import path13 from "path";
|
|
874
|
+
var SECRET_KEY_RE = /key|token|secret|pass|auth|credential|cookie|session/i;
|
|
875
|
+
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
876
|
+
function placeholdersIn(v) {
|
|
877
|
+
const out = /* @__PURE__ */ new Set();
|
|
878
|
+
walk(v, (s) => {
|
|
879
|
+
for (const m of s.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
880
|
+
return s;
|
|
881
|
+
});
|
|
882
|
+
return [...out];
|
|
883
|
+
}
|
|
884
|
+
function walk(v, onString, keyPath = []) {
|
|
885
|
+
if (typeof v === "string") return onString(v, keyPath);
|
|
886
|
+
if (Array.isArray(v)) return v.map((x2, i) => walk(x2, onString, [...keyPath, String(i)]));
|
|
887
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x2]) => [k, walk(x2, onString, [...keyPath, k])]));
|
|
888
|
+
return v;
|
|
889
|
+
}
|
|
890
|
+
var envName = (...parts) => parts.join("_").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
891
|
+
function mask(id, entry, cat) {
|
|
892
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
893
|
+
const out = { ...entry };
|
|
894
|
+
for (const sk of cat.secretKeys) {
|
|
895
|
+
const sect = out[sk];
|
|
896
|
+
if (!sect || typeof sect !== "object" || Array.isArray(sect)) continue;
|
|
897
|
+
const masked = {};
|
|
898
|
+
for (const [k, v] of Object.entries(sect)) {
|
|
899
|
+
if (typeof v !== "string" || !SECRET_KEY_RE.test(k) || PLACEHOLDER_RE.test(v)) {
|
|
900
|
+
masked[k] = v;
|
|
901
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const name = sk === "env" ? envName(k) : envName(id, k);
|
|
905
|
+
const scheme = /^(\w+) \S+$/.exec(v);
|
|
906
|
+
masked[k] = scheme ? `${scheme[1]} \${${name}}` : `\${${name}}`;
|
|
907
|
+
}
|
|
908
|
+
out[sk] = masked;
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
function suspiciousStrings(entry) {
|
|
913
|
+
const out = [];
|
|
914
|
+
walk(entry, (s, kp) => {
|
|
915
|
+
if (/^(sk-|ghp_|github_pat_|xox[abp]-|AKIA|glpat-|ntn_|secret_)[A-Za-z0-9_-]{8,}/.test(s)) out.push(kp.join("."));
|
|
916
|
+
return s;
|
|
917
|
+
});
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
function expand(entry, env = process.env) {
|
|
921
|
+
const missing = /* @__PURE__ */ new Set();
|
|
922
|
+
const value = walk(
|
|
923
|
+
entry,
|
|
924
|
+
(s) => s.replace(PLACEHOLDER_RE, (whole, name, def) => {
|
|
925
|
+
if (env[name] !== void 0) return env[name];
|
|
926
|
+
if (def !== void 0) return def;
|
|
927
|
+
missing.add(name);
|
|
928
|
+
return whole;
|
|
929
|
+
})
|
|
930
|
+
);
|
|
931
|
+
return { value, missing: [...missing] };
|
|
932
|
+
}
|
|
933
|
+
function stringMatches(shed, local) {
|
|
934
|
+
if (shed === local) return true;
|
|
935
|
+
if (!PLACEHOLDER_RE.test(shed)) {
|
|
936
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
940
|
+
const re = "^" + shed.split(PLACEHOLDER_RE).map((part, i) => i % 3 === 0 ? part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : i % 3 === 1 ? ".+" : "").join("") + "$";
|
|
941
|
+
return new RegExp(re).test(local);
|
|
942
|
+
}
|
|
943
|
+
function matches2(shed, local) {
|
|
944
|
+
if (typeof shed === "string" && typeof local === "string") return stringMatches(shed, local);
|
|
945
|
+
if (Array.isArray(shed) && Array.isArray(local)) return shed.length === local.length && shed.every((x2, i) => matches2(x2, local[i]));
|
|
946
|
+
if (shed && local && typeof shed === "object" && typeof local === "object" && !Array.isArray(shed) && !Array.isArray(local)) {
|
|
947
|
+
const a = Object.keys(shed).sort(), b = Object.keys(local).sort();
|
|
948
|
+
return a.length === b.length && a.every((k, i) => k === b[i] && matches2(shed[k], local[k]));
|
|
949
|
+
}
|
|
950
|
+
return shed === local;
|
|
951
|
+
}
|
|
952
|
+
function remask(id, local, shed, cat) {
|
|
953
|
+
const keep = (l, s) => {
|
|
954
|
+
if (typeof l === "string" && typeof s === "string" && stringMatches(s, l)) return s;
|
|
955
|
+
if (Array.isArray(l) && Array.isArray(s)) return l.map((x2, i) => keep(x2, s[i]));
|
|
956
|
+
if (l && s && typeof l === "object" && typeof s === "object" && !Array.isArray(l) && !Array.isArray(s)) {
|
|
957
|
+
return Object.fromEntries(Object.entries(l).map(([k, x2]) => [k, keep(x2, s[k])]));
|
|
958
|
+
}
|
|
959
|
+
return l;
|
|
960
|
+
};
|
|
961
|
+
return mask(id, shed === null ? local : keep(local, shed), cat);
|
|
962
|
+
}
|
|
963
|
+
function diffEntry(shed, local) {
|
|
964
|
+
const out = [];
|
|
965
|
+
const go = (s, l, kp) => {
|
|
966
|
+
if (s === void 0) {
|
|
967
|
+
out.push({ status: "A", file: kp || "(entry)" });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (l === void 0) {
|
|
971
|
+
out.push({ status: "D", file: kp || "(entry)" });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const objs = s && l && typeof s === "object" && typeof l === "object" && !Array.isArray(s) && !Array.isArray(l);
|
|
975
|
+
if (objs) {
|
|
976
|
+
for (const k of (/* @__PURE__ */ new Set([...Object.keys(s), ...Object.keys(l)])).values()) go(s[k], l[k], kp ? `${kp}.${k}` : k);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (!matches2(s, l)) out.push({ status: "M", file: kp || "(entry)" });
|
|
980
|
+
};
|
|
981
|
+
go(shed, local, "");
|
|
982
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
983
|
+
}
|
|
984
|
+
async function readEntryFile(p) {
|
|
985
|
+
try {
|
|
986
|
+
return JSON.parse(await fs10.readFile(p, "utf8"));
|
|
987
|
+
} catch (e) {
|
|
988
|
+
if (e.code === "ENOENT") return null;
|
|
989
|
+
throw new Error(`${p}: JSON \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async function writeEntryFile(p, v) {
|
|
993
|
+
await fs10.mkdir(path13.dirname(p), { recursive: true });
|
|
994
|
+
await fs10.writeFile(p, JSON.stringify(v, null, 2) + "\n");
|
|
995
|
+
}
|
|
996
|
+
|
|
810
997
|
// src/core/init.ts
|
|
811
998
|
async function init(ctx, opts = {}) {
|
|
812
999
|
const profileName = opts.profile ?? "default";
|
|
@@ -841,7 +1028,7 @@ async function init(ctx, opts = {}) {
|
|
|
841
1028
|
m.components[cat.name] = [];
|
|
842
1029
|
m.profiles[profileName][cat.name] = [];
|
|
843
1030
|
for (const f of mine) {
|
|
844
|
-
const dst =
|
|
1031
|
+
const dst = path14.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
845
1032
|
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
846
1033
|
m.components[cat.name].push({ id: f.id });
|
|
847
1034
|
m.profiles[profileName][cat.name].push(f.id);
|
|
@@ -850,23 +1037,50 @@ async function init(ctx, opts = {}) {
|
|
|
850
1037
|
ctx.log(` + ${cat.name}/${f.id}`);
|
|
851
1038
|
}
|
|
852
1039
|
}
|
|
1040
|
+
for (const cat of ctx.adapter.entries()) {
|
|
1041
|
+
const all2 = await cat.read();
|
|
1042
|
+
const ids = Object.keys(all2).sort();
|
|
1043
|
+
for (const id of ids.filter((id2) => isExcluded(cat.name, id2))) {
|
|
1044
|
+
skipped.push(`${cat.name}/${id}`);
|
|
1045
|
+
ctx.log(` - ${cat.name}/${id} (--exclude)`);
|
|
1046
|
+
}
|
|
1047
|
+
const mine = ids.filter((id) => !isExcluded(cat.name, id));
|
|
1048
|
+
if (!mine.length) continue;
|
|
1049
|
+
m.components[cat.name] = [];
|
|
1050
|
+
m.profiles[profileName][cat.name] = [];
|
|
1051
|
+
for (const id of mine) {
|
|
1052
|
+
if (!/^[\w.-]+$/.test(id)) {
|
|
1053
|
+
ctx.log(` ! ${cat.name}/${id}: \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790\uAC00 \uC788\uC5B4 \uAC74\uB108\uB700`);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const masked = mask(id, all2[id], cat);
|
|
1057
|
+
await writeEntryFile(path14.join(ctx.shed, cat.name, `${id}.json`), masked);
|
|
1058
|
+
m.components[cat.name].push({ id });
|
|
1059
|
+
m.profiles[profileName][cat.name].push(id);
|
|
1060
|
+
managed.push(targetRel(cat, id));
|
|
1061
|
+
copied++;
|
|
1062
|
+
const vars = placeholdersIn(masked);
|
|
1063
|
+
ctx.log(` + ${cat.name}/${id}${vars.length ? ` (\uC2DC\uD06C\uB9BF \u2192 ${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1064
|
+
for (const where of suspiciousStrings(masked)) ctx.log(` ! ${where} \uAC00 \uC2DC\uD06C\uB9BF\uCC98\uB7FC \uBCF4\uC785\uB2C8\uB2E4. \uCC3D\uACE0\uC758 ${cat.name}/${id}.json \uC5D0\uC11C \${VAR} \uB85C \uBC14\uAFB8\uC138\uC694`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
853
1067
|
const instr = instructionsFile(ctx);
|
|
854
1068
|
if (await exists(instr)) {
|
|
855
|
-
const text = await
|
|
1069
|
+
const text = await fs11.readFile(instr, "utf8");
|
|
856
1070
|
if (!isGenerated(text)) {
|
|
857
|
-
const dst =
|
|
858
|
-
await
|
|
859
|
-
await
|
|
1071
|
+
const dst = path14.join(ctx.shed, INSTRUCTIONS, "main.md");
|
|
1072
|
+
await fs11.mkdir(path14.dirname(dst), { recursive: true });
|
|
1073
|
+
await fs11.writeFile(dst, text);
|
|
860
1074
|
const fragRel = targetRel(INSTRUCTIONS, "main");
|
|
861
1075
|
await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
|
|
862
1076
|
managed.push(fragRel);
|
|
863
1077
|
m.components[INSTRUCTIONS] = [{ id: "main" }];
|
|
864
1078
|
m.profiles[profileName][INSTRUCTIONS] = ["main"];
|
|
865
1079
|
copied++;
|
|
866
|
-
ctx.log(` + ${INSTRUCTIONS}/main (${
|
|
1080
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path14.basename(instr)})`);
|
|
867
1081
|
}
|
|
868
1082
|
}
|
|
869
|
-
await
|
|
1083
|
+
await fs11.mkdir(ctx.shed, { recursive: true });
|
|
870
1084
|
let yamlText = stringifyManifest(m);
|
|
871
1085
|
for (const p of pkgs) {
|
|
872
1086
|
if (!p.into) continue;
|
|
@@ -875,7 +1089,7 @@ async function init(ctx, opts = {}) {
|
|
|
875
1089
|
# install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
|
|
876
1090
|
`);
|
|
877
1091
|
}
|
|
878
|
-
await
|
|
1092
|
+
await fs11.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
|
|
879
1093
|
` + yamlText);
|
|
880
1094
|
if (pkgs.length) {
|
|
881
1095
|
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
|
|
@@ -892,8 +1106,8 @@ ${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD50
|
|
|
892
1106
|
}
|
|
893
1107
|
|
|
894
1108
|
// src/core/restore.ts
|
|
895
|
-
import { promises as
|
|
896
|
-
import
|
|
1109
|
+
import { promises as fs12 } from "fs";
|
|
1110
|
+
import path15 from "path";
|
|
897
1111
|
async function restore(ctx, profileArg, opts = {}) {
|
|
898
1112
|
const backup = opts.backup ?? true;
|
|
899
1113
|
const state = await readState(ctx.adapter);
|
|
@@ -912,22 +1126,59 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
912
1126
|
const oldManaged = new Set(state?.managed ?? []);
|
|
913
1127
|
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
914
1128
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
915
|
-
const backupDir =
|
|
1129
|
+
const backupDir = path15.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
916
1130
|
const backedUp = [];
|
|
917
1131
|
const placed = [];
|
|
1132
|
+
const missingEnv = [];
|
|
1133
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
1134
|
+
const entriesOf = async (cat) => {
|
|
1135
|
+
if (!localEntries.has(cat.name)) localEntries.set(cat.name, await cat.read());
|
|
1136
|
+
return localEntries.get(cat.name);
|
|
1137
|
+
};
|
|
918
1138
|
async function backUp(rel) {
|
|
1139
|
+
const en = entryOf(ctx, rel);
|
|
1140
|
+
if (en) {
|
|
1141
|
+
const cur = (await entriesOf(en.cat))[en.id];
|
|
1142
|
+
if (cur === void 0) return;
|
|
1143
|
+
backedUp.push(rel);
|
|
1144
|
+
if (opts.dryRun || !backup) return;
|
|
1145
|
+
await writeEntryFile(path15.join(backupDir, en.cat.name, `${en.id}.json`), cur);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
919
1148
|
const from = abs(ctx, rel);
|
|
920
1149
|
if (!await exists(from)) return;
|
|
921
1150
|
backedUp.push(rel);
|
|
922
1151
|
if (opts.dryRun || !backup) return;
|
|
923
|
-
await copyTree(from,
|
|
1152
|
+
await copyTree(from, path15.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
|
|
924
1153
|
}
|
|
925
1154
|
for (const rel of toRemove) {
|
|
926
1155
|
ctx.log(` - ${rel}`);
|
|
927
1156
|
await backUp(rel);
|
|
928
|
-
if (
|
|
1157
|
+
if (opts.dryRun) continue;
|
|
1158
|
+
const en = entryOf(ctx, rel);
|
|
1159
|
+
if (en) await en.cat.write(en.id, null);
|
|
1160
|
+
else await removeTree(abs(ctx, rel));
|
|
929
1161
|
}
|
|
930
1162
|
for (const it of plan) {
|
|
1163
|
+
if (it.entry) {
|
|
1164
|
+
const shed = await readEntryFile(it.src);
|
|
1165
|
+
const local = (await entriesOf(it.entry))[it.id];
|
|
1166
|
+
const vars = placeholdersIn(shed);
|
|
1167
|
+
const ex = expand(shed);
|
|
1168
|
+
if (ex.missing.length) missingEnv.push({ rel: it.rel, vars: ex.missing });
|
|
1169
|
+
const value = it.entry.expandsEnv ? shed : ex.value;
|
|
1170
|
+
const same2 = local !== void 0 && matches2(shed, local);
|
|
1171
|
+
const mark2 = same2 ? "=" : local !== void 0 ? "~" : "+";
|
|
1172
|
+
ctx.log(` ${mark2} ${it.rel}${vars.length ? ` (${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1173
|
+
if (same2) {
|
|
1174
|
+
placed.push(it.rel);
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (local !== void 0) await backUp(it.rel);
|
|
1178
|
+
if (!opts.dryRun) await it.entry.write(it.id, value);
|
|
1179
|
+
placed.push(it.rel);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
931
1182
|
const target = abs(ctx, it.rel);
|
|
932
1183
|
const same = await hashTree(target, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx));
|
|
933
1184
|
const mark = same ? "=" : await exists(target) ? "~" : "+";
|
|
@@ -943,14 +1194,14 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
943
1194
|
const instrPath = instructionsFile(ctx);
|
|
944
1195
|
if (fragments.length) {
|
|
945
1196
|
const contents = [];
|
|
946
|
-
for (const f of fragments) contents.push({ id: f.id, content: await
|
|
1197
|
+
for (const f of fragments) contents.push({ id: f.id, content: await fs12.readFile(f.src, "utf8") });
|
|
947
1198
|
const rendered = renderInstructions(ctx, profile, contents);
|
|
948
|
-
const existing = await exists(instrPath) ? await
|
|
1199
|
+
const existing = await exists(instrPath) ? await fs12.readFile(instrPath, "utf8") : null;
|
|
949
1200
|
if (existing !== rendered) {
|
|
950
1201
|
const mark = existing === null ? "+" : "~";
|
|
951
1202
|
ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
|
|
952
1203
|
if (existing !== null) await backUp(instrRel);
|
|
953
|
-
if (!opts.dryRun) await
|
|
1204
|
+
if (!opts.dryRun) await fs12.writeFile(instrPath, rendered);
|
|
954
1205
|
} else {
|
|
955
1206
|
ctx.log(` = ${instrRel}`);
|
|
956
1207
|
}
|
|
@@ -960,14 +1211,22 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
960
1211
|
ctx.log(`
|
|
961
1212
|
(dry-run) \uBCC0\uACBD \uC5C6\uC74C. \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}, \uBC31\uC5C5 \uC608\uC815 ${backedUp.length}`);
|
|
962
1213
|
reportPending(ctx, pkgRes);
|
|
963
|
-
|
|
1214
|
+
reportMissingEnv(ctx, missingEnv);
|
|
1215
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: null, missingEnv };
|
|
964
1216
|
}
|
|
965
1217
|
await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
966
1218
|
const bdir = backup && backedUp.length ? backupDir : null;
|
|
967
1219
|
ctx.log(`
|
|
968
1220
|
\uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.installed.length ? `, \uD328\uD0A4\uC9C0 \uC124\uCE58 ${pkgRes.installed.length}` : ""}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
|
|
969
1221
|
reportPending(ctx, pkgRes);
|
|
970
|
-
|
|
1222
|
+
reportMissingEnv(ctx, missingEnv);
|
|
1223
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: bdir, missingEnv };
|
|
1224
|
+
}
|
|
1225
|
+
function reportMissingEnv(ctx, missing) {
|
|
1226
|
+
if (!missing.length) return;
|
|
1227
|
+
ctx.log(`
|
|
1228
|
+
\uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uB294 \uD56D\uBAA9\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uC2DC\uD06C\uB9BF \uAC12\uC740 \uCC3D\uACE0\uC5D0 \uB2F4\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uC774 \uAE30\uAE30\uC758 \uC178 \uD658\uACBD\uC5D0 \uB123\uC73C\uC138\uC694 (\uC608: ~/.zshrc \uC758 export):`);
|
|
1229
|
+
for (const m of missing) ctx.log(` ${m.rel}: ${m.vars.join(", ")}`);
|
|
971
1230
|
}
|
|
972
1231
|
|
|
973
1232
|
// src/core/diff.ts
|
|
@@ -976,8 +1235,16 @@ async function diff(ctx) {
|
|
|
976
1235
|
if (!state) throw new Error("\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 'lshed restore <profile>' \uC744 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
977
1236
|
const m = await loadManifest(ctx);
|
|
978
1237
|
const out = [];
|
|
1238
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
979
1239
|
for (const item of planProfile(ctx, m, state.profile)) {
|
|
980
|
-
|
|
1240
|
+
let changes;
|
|
1241
|
+
if (item.entry) {
|
|
1242
|
+
if (!localEntries.has(item.category)) localEntries.set(item.category, await item.entry.read());
|
|
1243
|
+
const shed = await readEntryFile(item.src);
|
|
1244
|
+
changes = shed === null ? [{ status: "A", file: "(entry)" }] : diffEntry(shed, localEntries.get(item.category)[item.id]);
|
|
1245
|
+
} else {
|
|
1246
|
+
changes = await diffTrees(abs(ctx, item.rel), item.src, ignoreOf(ctx));
|
|
1247
|
+
}
|
|
981
1248
|
if (changes.length) out.push({ item, changes });
|
|
982
1249
|
}
|
|
983
1250
|
return out;
|
|
@@ -996,12 +1263,18 @@ function formatDiff(diffs) {
|
|
|
996
1263
|
// src/core/status.ts
|
|
997
1264
|
async function status(ctx) {
|
|
998
1265
|
const state = await readState(ctx.adapter);
|
|
999
|
-
if (!state) return { state: null, drifted: [], packages: [] };
|
|
1266
|
+
if (!state) return { state: null, drifted: [], packages: [], missingEnv: [] };
|
|
1000
1267
|
const d = await diff(ctx);
|
|
1001
1268
|
const m = await loadManifest(ctx);
|
|
1002
1269
|
const lock = await readLock(ctx.shed);
|
|
1003
1270
|
const packages = await Promise.all(packagesOf(m, state.profile).map((p) => packageStatus(ctx, p, lock)));
|
|
1004
|
-
|
|
1271
|
+
const missingEnv = [];
|
|
1272
|
+
for (const it of planProfile(ctx, m, state.profile).filter((p) => p.entry)) {
|
|
1273
|
+
const shed = await readEntryFile(it.src);
|
|
1274
|
+
const missing = shed === null ? [] : expand(shed).missing;
|
|
1275
|
+
if (missing.length) missingEnv.push({ rel: it.rel, vars: missing });
|
|
1276
|
+
}
|
|
1277
|
+
return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages, missingEnv };
|
|
1005
1278
|
}
|
|
1006
1279
|
function formatStatus(s, adapterRoot) {
|
|
1007
1280
|
if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
|
|
@@ -1018,6 +1291,7 @@ function formatStatus(s, adapterRoot) {
|
|
|
1018
1291
|
const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${short2(p.rev)} (\uB77D \uC5C6\uC74C)` : p.rev === p.locked ? `${short2(p.rev)} = lock` : `${short2(p.rev)} \u2260 lock ${short2(p.locked)} \u2192 lshed update`;
|
|
1019
1292
|
lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
|
|
1020
1293
|
}
|
|
1294
|
+
for (const m of s.missingEnv) lines.push(`\uD658\uACBD\uBCC0\uC218 ${m.rel}: ${m.vars.join(", ")} \uC5C6\uC74C \u2192 \uC178\uC5D0\uC11C export \uD558\uC138\uC694`);
|
|
1021
1295
|
return lines.join("\n");
|
|
1022
1296
|
}
|
|
1023
1297
|
|
|
@@ -1037,13 +1311,27 @@ async function save(ctx, ids = []) {
|
|
|
1037
1311
|
});
|
|
1038
1312
|
}
|
|
1039
1313
|
const saved = [];
|
|
1314
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
1040
1315
|
for (const it of plan) {
|
|
1041
|
-
const
|
|
1042
|
-
const src = effectiveSource(it.category, it.component, kind);
|
|
1316
|
+
const src = effectiveSource(it.category, it.component, kindOf(ctx, it.category));
|
|
1043
1317
|
if (!isSaveable(src)) {
|
|
1044
1318
|
ctx.log(` ! ${it.category}/${it.id}: \uC6D0\uACA9 \uCD9C\uCC98(${src})\uB294 save \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
1045
1319
|
continue;
|
|
1046
1320
|
}
|
|
1321
|
+
if (it.entry) {
|
|
1322
|
+
if (!localEntries.has(it.category)) localEntries.set(it.category, await it.entry.read());
|
|
1323
|
+
const local2 = localEntries.get(it.category)[it.id];
|
|
1324
|
+
if (local2 === void 0) {
|
|
1325
|
+
ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const shed = await readEntryFile(it.src);
|
|
1329
|
+
if (shed !== null && matches2(shed, local2)) continue;
|
|
1330
|
+
await writeEntryFile(it.src, remask(it.id, local2, shed, it.entry));
|
|
1331
|
+
saved.push(`${it.category}/${it.id}`);
|
|
1332
|
+
ctx.log(` \u2713 ${it.category}/${it.id} \u2192 \uCC3D\uACE0 (\uC2DC\uD06C\uB9BF\uC740 \uC790\uB9AC\uD45C\uC2DC\uC790\uB85C)`);
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1047
1335
|
const local = abs(ctx, it.rel);
|
|
1048
1336
|
if (!await exists(local)) {
|
|
1049
1337
|
ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
|
|
@@ -1083,8 +1371,8 @@ function formatRows(rows, m) {
|
|
|
1083
1371
|
}
|
|
1084
1372
|
|
|
1085
1373
|
// src/core/remove.ts
|
|
1086
|
-
import { promises as
|
|
1087
|
-
import
|
|
1374
|
+
import { promises as fs13 } from "fs";
|
|
1375
|
+
import path16 from "path";
|
|
1088
1376
|
import YAML3, { isSeq, isMap } from "yaml";
|
|
1089
1377
|
function resolveKey(m, raw) {
|
|
1090
1378
|
const rows = listRows(m);
|
|
@@ -1099,7 +1387,7 @@ async function remove(ctx, raw) {
|
|
|
1099
1387
|
const { category, id } = resolveKey(m, raw);
|
|
1100
1388
|
const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
|
|
1101
1389
|
if (users.length) throw new Error(`${category}/${id} \uB294 \uD504\uB85C\uD544 ${users.join(", ")} \uC774 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uD504\uB85C\uD544\uC5D0\uC11C \uBE7C\uC138\uC694.`);
|
|
1102
|
-
const text = await
|
|
1390
|
+
const text = await fs13.readFile(manifestPath(ctx), "utf8");
|
|
1103
1391
|
const doc = YAML3.parseDocument(text);
|
|
1104
1392
|
let deleted;
|
|
1105
1393
|
if (category === PACKAGES) {
|
|
@@ -1121,14 +1409,14 @@ async function remove(ctx, raw) {
|
|
|
1121
1409
|
seq.delete(idx);
|
|
1122
1410
|
if (!seq.items.length) doc.deleteIn(["components", category]);
|
|
1123
1411
|
const src = sourcePath(ctx, category, findComponent(m, category, id));
|
|
1124
|
-
const inside = !
|
|
1412
|
+
const inside = !path16.relative(ctx.shed, src).startsWith("..");
|
|
1125
1413
|
if (inside && await exists(src)) {
|
|
1126
1414
|
await removeTree(src);
|
|
1127
1415
|
deleted = src;
|
|
1128
1416
|
}
|
|
1129
1417
|
ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
|
|
1130
1418
|
}
|
|
1131
|
-
await
|
|
1419
|
+
await fs13.writeFile(manifestPath(ctx), doc.toString());
|
|
1132
1420
|
return { category, id, deleted };
|
|
1133
1421
|
}
|
|
1134
1422
|
async function prune(ctx, opts = {}) {
|
|
@@ -1158,16 +1446,16 @@ var { version } = createRequire(import.meta.url)("../package.json");
|
|
|
1158
1446
|
var program = new Command().name("lshed").description("Keep your coding-agent harness (skills, agents, commands, instructions) in a shed and restore it anywhere by profile.").version(version).option("--shed <dir>", "shed directory (default: $LSHED_HOME, then the shed recorded by the last restore)").option("--root <dir>", "agent config root (default: ~/.claude)");
|
|
1159
1447
|
function adapterFromOpts() {
|
|
1160
1448
|
const { root } = program.opts();
|
|
1161
|
-
return new ClaudeCodeAdapter(root ?
|
|
1449
|
+
return new ClaudeCodeAdapter(root ? path17.resolve(root) : void 0);
|
|
1162
1450
|
}
|
|
1163
1451
|
async function ctxFor(cmd) {
|
|
1164
1452
|
const adapter = adapterFromOpts();
|
|
1165
1453
|
const { shed: flag } = program.opts();
|
|
1166
1454
|
let shed = flag ?? process.env.LSHED_HOME;
|
|
1167
1455
|
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
1168
|
-
if (!shed && cmd === "init") shed =
|
|
1456
|
+
if (!shed && cmd === "init") shed = path17.join(os2.homedir(), "lshed");
|
|
1169
1457
|
if (!shed) throw new Error("\uCC3D\uACE0 \uC704\uCE58\uB97C \uBAA8\uB985\uB2C8\uB2E4. --shed <dir> \uB610\uB294 LSHED_HOME \uC744 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
1170
|
-
return { adapter, shed:
|
|
1458
|
+
return { adapter, shed: path17.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
|
|
1171
1459
|
}
|
|
1172
1460
|
async function run(fn) {
|
|
1173
1461
|
try {
|
|
@@ -1212,7 +1500,7 @@ program.command("status").description("show the applied profile, managed paths a
|
|
|
1212
1500
|
const adapter = adapterFromOpts();
|
|
1213
1501
|
const state = await readState(adapter);
|
|
1214
1502
|
if (!state) {
|
|
1215
|
-
console.log(formatStatus({ state: null, drifted: [], packages: [] }, adapter.root));
|
|
1503
|
+
console.log(formatStatus({ state: null, drifted: [], packages: [], missingEnv: [] }, adapter.root));
|
|
1216
1504
|
return;
|
|
1217
1505
|
}
|
|
1218
1506
|
const ctx = await ctxFor("other");
|
|
@@ -1244,6 +1532,11 @@ program.command("scan").description("(debug) list components found in the agent
|
|
|
1244
1532
|
const adapter = adapterFromOpts();
|
|
1245
1533
|
const found = await adapter.scan();
|
|
1246
1534
|
for (const c of found) console.log(`${c.category}/${c.id} ${c.path}`);
|
|
1247
|
-
|
|
1535
|
+
let n = found.length;
|
|
1536
|
+
for (const e of adapter.entries()) for (const id of Object.keys(await e.read())) {
|
|
1537
|
+
console.log(`${e.name}/${id} (entry)`);
|
|
1538
|
+
n++;
|
|
1539
|
+
}
|
|
1540
|
+
console.error(`${n}\uAC1C \uBC1C\uACAC (root: ${adapter.root})`);
|
|
1248
1541
|
}));
|
|
1249
1542
|
program.parseAsync();
|