lshed 0.2.1 → 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/dist/cli.js CHANGED
@@ -4,12 +4,326 @@
4
4
  import { Command } from "commander";
5
5
  import { createRequire } from "module";
6
6
  import os2 from "os";
7
- import path13 from "path";
7
+ import path17 from "path";
8
8
 
9
9
  // src/adapters/claude-code.ts
10
+ import { promises as fs4 } from "fs";
11
+ import path4 from "path";
12
+ import os from "os";
13
+
14
+ // src/installers/claude-plugin.ts
10
15
  import { promises as fs } from "fs";
11
16
  import path from "path";
12
- import os from "os";
17
+
18
+ // src/source.ts
19
+ var GITHUB_RE = /^([\w.-]+)\/([\w.-]+)(?:@([^#]+))?(?:#(.+))?$/;
20
+ function parseSource(raw) {
21
+ const idx = raw.indexOf(":");
22
+ if (idx <= 0) {
23
+ throw new Error(`source\uC5D0 \uC2A4\uD0B4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}" (\uC608: file:./skills/x, github:user/repo@v1)`);
24
+ }
25
+ const scheme = raw.slice(0, idx);
26
+ const rest2 = raw.slice(idx + 1);
27
+ switch (scheme) {
28
+ case "file":
29
+ if (!rest2) throw new Error(`file: \uB4A4\uC5D0 \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
30
+ return { scheme, path: rest2 };
31
+ case "github": {
32
+ const m = GITHUB_RE.exec(rest2);
33
+ if (!m) throw new Error(`github: \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: "${raw}" (\uC608: github:user/repo@v1.0#sub/path)`);
34
+ const [, owner, repo, ref, subpath] = m;
35
+ return { scheme, owner, repo, ref, subpath };
36
+ }
37
+ case "git": {
38
+ const hash = rest2.lastIndexOf("#");
39
+ const url = hash >= 0 ? rest2.slice(0, hash) : rest2;
40
+ const ref = hash >= 0 ? rest2.slice(hash + 1) : void 0;
41
+ if (!url) throw new Error(`git: \uB4A4\uC5D0 URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
42
+ return { scheme, url, ref: ref || void 0 };
43
+ }
44
+ default:
45
+ if (!/^[a-z][\w-]*$/.test(scheme)) throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
46
+ return { scheme: "other", name: scheme, rest: rest2 };
47
+ }
48
+ }
49
+ function isComponentSource(s) {
50
+ return s.scheme === "file" || s.scheme === "github" || s.scheme === "git";
51
+ }
52
+ function formatSource(s) {
53
+ switch (s.scheme) {
54
+ case "file":
55
+ return `file:${s.path}`;
56
+ case "github":
57
+ return `github:${s.owner}/${s.repo}${s.ref ? `@${s.ref}` : ""}${s.subpath ? `#${s.subpath}` : ""}`;
58
+ case "git":
59
+ return `git:${s.url}${s.ref ? `#${s.ref}` : ""}`;
60
+ case "other":
61
+ return `${s.name}:${s.rest}`;
62
+ }
63
+ }
64
+ function cloneTarget(s) {
65
+ if (s.scheme === "github") return { url: `https://github.com/${s.owner}/${s.repo}.git`, ref: s.ref };
66
+ if (s.scheme === "git") return { url: s.url, ref: s.ref };
67
+ throw new Error(`file: \uCD9C\uCC98\uB294 clone \uB300\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
68
+ }
69
+ function sourceFromRemote(url, ref) {
70
+ const m = /^(?:https?:\/\/github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
71
+ if (m) return formatSource({ scheme: "github", owner: m[1], repo: m[2], ref });
72
+ return formatSource({ scheme: "git", url, ref });
73
+ }
74
+
75
+ // src/installers/claude-plugin.ts
76
+ async function readJson(p, fallback) {
77
+ try {
78
+ return JSON.parse(await fs.readFile(p, "utf8"));
79
+ } catch {
80
+ return fallback;
81
+ }
82
+ }
83
+ var installedPath = (ctx) => path.join(ctx.adapter.root, "plugins", "installed_plugins.json");
84
+ var marketplacesPath = (ctx) => path.join(ctx.adapter.root, "plugins", "known_marketplaces.json");
85
+ function rest(pkg) {
86
+ const s = parseSource(pkg.source);
87
+ if (s.scheme !== "other") throw new Error(`package ${pkg.id}: ${pkg.source} \uB294 \uD50C\uB7EC\uADF8\uC778 \uCD9C\uCC98\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
88
+ return s.rest;
89
+ }
90
+ async function claude(ctx, args) {
91
+ try {
92
+ await ctx.exec("claude", args);
93
+ } catch (e) {
94
+ if (e.code === "ENOENT") throw new Error("claude CLI \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. Claude Code \uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC5B4\uC57C \uD50C\uB7EC\uADF8\uC778\uC744 \uBCF5\uC6D0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
95
+ throw e;
96
+ }
97
+ }
98
+ var marketplaceInstaller = {
99
+ name: "claude-marketplace",
100
+ schemes: ["claude-marketplace"],
101
+ priority: 10,
102
+ async detect(ctx) {
103
+ const m = await readJson(marketplacesPath(ctx), {});
104
+ const out = [];
105
+ for (const [name, v] of Object.entries(m)) {
106
+ const src = v.source;
107
+ if (src.source === "github" && src.repo) out.push({ id: name, source: `claude-marketplace:${src.repo}`, rev: src.repo });
108
+ else ctx.log(` ! marketplace ${name}: ${src.source} \uCD9C\uCC98\uB294 \uC544\uC9C1 \uAE30\uB85D\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4 (\uAC74\uB108\uB700)`);
109
+ }
110
+ return out;
111
+ },
112
+ async status(ctx, pkg) {
113
+ const m = await readJson(marketplacesPath(ctx), {});
114
+ const v = m[pkg.id];
115
+ return { present: !!v, rev: v?.source.repo };
116
+ },
117
+ async install(ctx, pkg, _locked, _opts) {
118
+ const repo = rest(pkg);
119
+ await claude(ctx, ["plugin", "marketplace", "add", repo, "--scope", "user"]);
120
+ return repo;
121
+ },
122
+ async update(ctx, pkg) {
123
+ await claude(ctx, ["plugin", "marketplace", "update", pkg.id]);
124
+ return rest(pkg);
125
+ },
126
+ describe(pkg) {
127
+ return `claude plugin marketplace add ${rest(pkg)}`;
128
+ },
129
+ cwd(ctx) {
130
+ return ctx.adapter.root;
131
+ }
132
+ };
133
+ var pluginInstaller = {
134
+ name: "claude-plugin",
135
+ schemes: ["claude-plugin"],
136
+ priority: 20,
137
+ async detect(ctx) {
138
+ const f = await readJson(installedPath(ctx), { version: 2, plugins: {} });
139
+ const out = [];
140
+ for (const [key, entries] of Object.entries(f.plugins)) {
141
+ const e = entries.find((x2) => x2.scope === "user");
142
+ if (!e) continue;
143
+ const [name] = key.split("@");
144
+ out.push({ id: name, source: `claude-plugin:${key}`, rev: e.version });
145
+ }
146
+ return out;
147
+ },
148
+ async status(ctx, pkg) {
149
+ const f = await readJson(installedPath(ctx), { version: 2, plugins: {} });
150
+ const e = f.plugins[rest(pkg)]?.find((x2) => x2.scope === "user");
151
+ return { present: !!e, rev: e?.version };
152
+ },
153
+ async install(ctx, pkg, _locked, opts) {
154
+ await claude(ctx, ["plugin", "install", rest(pkg), "--scope", "user", ...opts.yes ? ["-y"] : []]);
155
+ return (await this.status(ctx, pkg)).rev ?? "?";
156
+ },
157
+ async update(ctx, pkg) {
158
+ await claude(ctx, ["plugin", "update", rest(pkg)]);
159
+ return (await this.status(ctx, pkg)).rev ?? "?";
160
+ },
161
+ describe(pkg, locked) {
162
+ return `claude plugin install ${rest(pkg)}${locked ? ` (\uC804\uC5D0 ${locked}; \uACE0\uC815\uC740 \uC548 \uB428)` : ""}`;
163
+ },
164
+ cwd(ctx) {
165
+ return ctx.adapter.root;
166
+ }
167
+ };
168
+
169
+ // src/adapters/claude-mcp.ts
170
+ import { promises as fs3 } from "fs";
171
+ import path3 from "path";
172
+
173
+ // src/fsutil.ts
174
+ import { promises as fs2 } from "fs";
175
+ import { createHash } from "crypto";
176
+ import path2 from "path";
177
+
178
+ // src/ignore.ts
179
+ var DEFAULT_IGNORE = [
180
+ "node_modules",
181
+ ".git",
182
+ "__pycache__",
183
+ ".venv",
184
+ ".mypy_cache",
185
+ ".pytest_cache",
186
+ ".DS_Store",
187
+ "*.log"
188
+ ];
189
+ function matches(name, pattern) {
190
+ if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
191
+ return name === pattern;
192
+ }
193
+ function isIgnored(rel, patterns) {
194
+ if (!rel) return false;
195
+ return rel.split("/").some((seg) => patterns.some((p) => matches(seg, p)));
196
+ }
197
+
198
+ // src/fsutil.ts
199
+ async function exists(p) {
200
+ try {
201
+ await fs2.access(p);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+ async function isDir(p) {
208
+ try {
209
+ return (await fs2.stat(p)).isDirectory();
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+ async function listFiles(root, ignore = DEFAULT_IGNORE) {
215
+ if (!await exists(root)) return [];
216
+ if (!await isDir(root)) return [""];
217
+ const out = [];
218
+ async function walk2(dir, rel) {
219
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
220
+ for (const e of entries) {
221
+ const r = rel ? `${rel}/${e.name}` : e.name;
222
+ if (isIgnored(r, ignore)) continue;
223
+ const full = path2.join(dir, e.name);
224
+ let st;
225
+ try {
226
+ st = await fs2.stat(full);
227
+ } catch {
228
+ continue;
229
+ }
230
+ if (st.isDirectory()) await walk2(full, r);
231
+ else if (st.isFile()) out.push(r);
232
+ }
233
+ }
234
+ await walk2(root, "");
235
+ return out.sort();
236
+ }
237
+ async function hashFile(p) {
238
+ return createHash("sha256").update(await fs2.readFile(p)).digest("hex");
239
+ }
240
+ async function hashTree(root, ignore = DEFAULT_IGNORE) {
241
+ if (!await exists(root)) return null;
242
+ const h = createHash("sha256");
243
+ for (const rel of await listFiles(root, ignore)) {
244
+ h.update(rel).update("\0").update(await fs2.readFile(rel ? path2.join(root, rel) : root)).update("\0");
245
+ }
246
+ return h.digest("hex");
247
+ }
248
+ async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
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, {
253
+ recursive: true,
254
+ dereference: true,
255
+ filter: async (from) => {
256
+ const rel = path2.relative(srcRoot, path2.resolve(from)).split(path2.sep).join("/");
257
+ if (isIgnored(rel, ignore)) return false;
258
+ try {
259
+ if ((await fs2.lstat(from)).isSymbolicLink()) await fs2.stat(from);
260
+ } catch {
261
+ return false;
262
+ }
263
+ return true;
264
+ }
265
+ });
266
+ }
267
+ async function removeTree(p) {
268
+ await fs2.rm(p, { recursive: true, force: true });
269
+ }
270
+ async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
271
+ const l = new Set(await listFiles(local, ignore));
272
+ const s = new Set(await listFiles(shed, ignore));
273
+ const out = [];
274
+ for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
275
+ const lp = f ? path2.join(local, f) : local;
276
+ const sp = f ? path2.join(shed, f) : shed;
277
+ if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
278
+ else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
279
+ else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
280
+ }
281
+ return out;
282
+ }
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
13
327
  var CATEGORIES = [
14
328
  { name: "skills", root: "skills", kind: "dir" },
15
329
  { name: "agents", root: "agents", kind: "file" },
@@ -19,8 +333,13 @@ var CATEGORIES = [
19
333
  var ClaudeCodeAdapter = class {
20
334
  name = "claude-code";
21
335
  root;
336
+ mcp;
22
337
  constructor(root) {
23
- this.root = root ?? path.join(os.homedir(), ".claude");
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];
24
343
  }
25
344
  categories() {
26
345
  return CATEGORIES;
@@ -31,22 +350,25 @@ var ClaudeCodeAdapter = class {
31
350
  instructionsFileName() {
32
351
  return "CLAUDE.md";
33
352
  }
353
+ installers() {
354
+ return [marketplaceInstaller, pluginInstaller];
355
+ }
34
356
  async scan() {
35
357
  const out = [];
36
358
  for (const cat of CATEGORIES) {
37
- const dir = path.join(this.root, cat.root);
359
+ const dir = path4.join(this.root, cat.root);
38
360
  let entries;
39
361
  try {
40
- entries = await fs.readdir(dir, { withFileTypes: true });
362
+ entries = await fs4.readdir(dir, { withFileTypes: true });
41
363
  } catch {
42
364
  continue;
43
365
  }
44
366
  for (const e of entries) {
45
367
  if (e.name.startsWith(".")) continue;
46
- const full = path.join(dir, e.name);
368
+ const full = path4.join(dir, e.name);
47
369
  let st;
48
370
  try {
49
- st = await fs.stat(full);
371
+ st = await fs4.stat(full);
50
372
  } catch {
51
373
  continue;
52
374
  }
@@ -61,70 +383,95 @@ var ClaudeCodeAdapter = class {
61
383
  }
62
384
  };
63
385
 
64
- // src/core/init.ts
65
- import { promises as fs7 } from "fs";
66
- import path10 from "path";
67
-
68
386
  // src/core/context.ts
69
- import { promises as fs3 } from "fs";
70
- import path4 from "path";
387
+ import { promises as fs7 } from "fs";
388
+ import path9 from "path";
389
+ import { spawn as spawn2 } from "child_process";
71
390
 
72
- // src/manifest.ts
73
- import { z } from "zod";
74
- import YAML from "yaml";
391
+ // src/installers/git.ts
392
+ import { promises as fs5 } from "fs";
393
+ import path6 from "path";
75
394
 
76
- // src/source.ts
77
- var GITHUB_RE = /^([\w.-]+)\/([\w.-]+)(?:@([^#]+))?(?:#(.+))?$/;
78
- function parseSource(raw) {
79
- const idx = raw.indexOf(":");
80
- if (idx <= 0) {
81
- throw new Error(`source\uC5D0 \uC2A4\uD0B4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}" (\uC608: file:./skills/x, github:user/repo@v1)`);
82
- }
83
- const scheme = raw.slice(0, idx);
84
- const rest = raw.slice(idx + 1);
85
- switch (scheme) {
86
- case "file":
87
- if (!rest) throw new Error(`file: \uB4A4\uC5D0 \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
88
- return { scheme, path: rest };
89
- case "github": {
90
- const m = GITHUB_RE.exec(rest);
91
- if (!m) throw new Error(`github: \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: "${raw}" (\uC608: github:user/repo@v1.0#sub/path)`);
92
- const [, owner, repo, ref, subpath] = m;
93
- return { scheme, owner, repo, ref, subpath };
94
- }
95
- case "git": {
96
- const hash = rest.lastIndexOf("#");
97
- const url = hash >= 0 ? rest.slice(0, hash) : rest;
98
- const ref = hash >= 0 ? rest.slice(hash + 1) : void 0;
99
- if (!url) throw new Error(`git: \uB4A4\uC5D0 URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
100
- return { scheme, url, ref: ref || void 0 };
101
- }
102
- default:
103
- throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
104
- }
105
- }
106
- function formatSource(s) {
107
- switch (s.scheme) {
108
- case "file":
109
- return `file:${s.path}`;
110
- case "github":
111
- return `github:${s.owner}/${s.repo}${s.ref ? `@${s.ref}` : ""}${s.subpath ? `#${s.subpath}` : ""}`;
112
- case "git":
113
- return `git:${s.url}${s.ref ? `#${s.ref}` : ""}`;
114
- }
395
+ // src/git.ts
396
+ import { execFile, spawn } from "child_process";
397
+ import { promisify } from "util";
398
+ import path5 from "path";
399
+ var x = promisify(execFile);
400
+ async function git(args, cwd) {
401
+ const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
402
+ return stdout.trim();
115
403
  }
116
- function cloneTarget(s) {
117
- if (s.scheme === "github") return { url: `https://github.com/${s.owner}/${s.repo}.git`, ref: s.ref };
118
- if (s.scheme === "git") return { url: s.url, ref: s.ref };
119
- throw new Error(`file: \uCD9C\uCC98\uB294 clone \uB300\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
404
+ var isRepo = (dir) => exists(path5.join(dir, ".git"));
405
+ var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
406
+ var head = (dir) => git(["rev-parse", "HEAD"], dir);
407
+ var branch = async (dir) => {
408
+ const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
409
+ return b === "HEAD" ? void 0 : b;
410
+ };
411
+ var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
412
+ var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
413
+ var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
414
+ function runShell(cmd, cwd) {
415
+ return new Promise((resolve, reject) => {
416
+ const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
417
+ p.on("error", reject);
418
+ p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
419
+ });
120
420
  }
121
- function sourceFromRemote(url, ref) {
122
- const m = /^(?:https?:\/\/github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
123
- if (m) return formatSource({ scheme: "github", owner: m[1], repo: m[2], ref });
124
- return formatSource({ scheme: "git", url, ref });
421
+
422
+ // src/installers/git.ts
423
+ function dirOf(ctx, pkg) {
424
+ if (!pkg.into) throw new Error(`package ${pkg.id}: git \uACC4\uC5F4 \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
425
+ return path6.join(ctx.adapter.root, ...pkg.into.split("/"));
125
426
  }
427
+ var gitInstaller = {
428
+ name: "git",
429
+ schemes: ["github", "git"],
430
+ priority: 0,
431
+ async detect(ctx, found) {
432
+ const out = [];
433
+ for (const f of found) {
434
+ if (!await isRepo(f.path)) continue;
435
+ const url = await remoteUrl(f.path);
436
+ if (!url) continue;
437
+ const into = path6.relative(ctx.adapter.root, f.path).split(path6.sep).join("/");
438
+ out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), rev: await head(f.path), path: f.path });
439
+ }
440
+ return out;
441
+ },
442
+ async status(ctx, pkg) {
443
+ const dir = dirOf(ctx, pkg);
444
+ const present = await isRepo(dir);
445
+ return { present, rev: present ? await head(dir) : void 0 };
446
+ },
447
+ async install(ctx, pkg, locked, _opts) {
448
+ const dir = dirOf(ctx, pkg);
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.`);
450
+ const { url, ref } = cloneTarget(parseSource(pkg.source));
451
+ await fs5.mkdir(path6.dirname(dir), { recursive: true });
452
+ await clone(url, dir, ref);
453
+ if (locked) {
454
+ await resetHard(dir, locked).catch(() => {
455
+ throw new Error(`package ${pkg.id}: \uB77D\uC758 \uCEE4\uBC0B ${locked.slice(0, 7)} \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 'lshed update ${pkg.id}' \uB85C \uB77D\uC744 \uAC31\uC2E0\uD558\uC138\uC694.`);
456
+ });
457
+ }
458
+ return head(dir);
459
+ },
460
+ async update(ctx, pkg) {
461
+ const dir = dirOf(ctx, pkg);
462
+ await pullFf(dir);
463
+ return head(dir);
464
+ },
465
+ describe(pkg, locked) {
466
+ const { url, ref } = cloneTarget(parseSource(pkg.source));
467
+ return `clone ${url}${ref ? ` @${ref}` : ""}${locked ? ` \u2192 ${locked.slice(0, 7)}` : ""}`;
468
+ },
469
+ cwd: dirOf
470
+ };
126
471
 
127
472
  // src/manifest.ts
473
+ import { z } from "zod";
474
+ import YAML from "yaml";
128
475
  var ComponentSchema = z.object({
129
476
  id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
130
477
  source: z.string().optional(),
@@ -133,7 +480,9 @@ var ComponentSchema = z.object({
133
480
  var PackageSchema = z.object({
134
481
  id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
135
482
  source: z.string(),
136
- into: z.string().regex(/^[^/\\][^\\]*$/, "into \uB294 \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C (POSIX)"),
483
+ /** git 계열 패키지의 위치 (어댑터 루트 기준). 어댑터 설치기 스킴은 필요 없다 */
484
+ into: z.string().regex(/^[^/\\][^\\]*$/, "into \uB294 \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C (POSIX)").optional(),
485
+ /** 설치 후 실행할 셸 명령. --yes 일 때만 실행 */
137
486
  install: z.string().optional()
138
487
  });
139
488
  var PACKAGES = "packages";
@@ -150,7 +499,7 @@ var ManifestSchema = z.object({
150
499
  });
151
500
  var ManifestError = class extends Error {
152
501
  };
153
- function parseManifest(text, knownCategories2) {
502
+ function parseManifest(text, knownCategories2, knownSchemes) {
154
503
  const raw = YAML.parse(text);
155
504
  const result = ManifestSchema.safeParse(raw);
156
505
  if (!result.success) {
@@ -169,7 +518,7 @@ ${lines.join("\n")}`);
169
518
  if (seen.has(c.id)) problems.push(`${cat}: id "${c.id}" \uC911\uBCF5`);
170
519
  seen.add(c.id);
171
520
  try {
172
- parseSource(effectiveSource(cat, c));
521
+ if (!isComponentSource(parseSource(effectiveSource(cat, c)))) problems.push(`${cat}/${c.id}: \uBD80\uD488 \uCD9C\uCC98\uB294 file:/github:/git: \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
173
522
  } catch (e) {
174
523
  problems.push(`${cat}/${c.id}: ${e.message}`);
175
524
  }
@@ -180,7 +529,11 @@ ${lines.join("\n")}`);
180
529
  if (pkgIds.has(p.id)) problems.push(`packages: id "${p.id}" \uC911\uBCF5`);
181
530
  pkgIds.add(p.id);
182
531
  try {
183
- if (parseSource(p.source).scheme === "file") problems.push(`packages/${p.id}: \uD328\uD0A4\uC9C0 \uCD9C\uCC98\uB294 github: \uB610\uB294 git: \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
532
+ const src = parseSource(p.source);
533
+ const scheme = src.scheme === "other" ? src.name : src.scheme;
534
+ if (scheme === "file") problems.push(`packages/${p.id}: \uD328\uD0A4\uC9C0 \uCD9C\uCC98\uB294 file: \uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
535
+ else if (knownSchemes && !knownSchemes.includes(scheme)) problems.push(`packages/${p.id}: \uC2A4\uD0B4 "${scheme}" \uC744 \uB2E4\uB8F0 \uC124\uCE58\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (${knownSchemes.join(", ")})`);
536
+ if ((scheme === "github" || scheme === "git") && !p.into) problems.push(`packages/${p.id}: ${scheme}: \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
184
537
  } catch (e) {
185
538
  problems.push(`packages/${p.id}: ${e.message}`);
186
539
  }
@@ -202,7 +555,8 @@ ${problems.map((p) => " " + p).join("\n")}`);
202
555
  return m;
203
556
  }
204
557
  function effectiveSource(category, c, kind = "dir") {
205
- return c.source ?? `file:./${category}/${c.id}${kind === "file" ? ".md" : ""}`;
558
+ const ext = kind === "file" ? ".md" : kind === "entry" ? ".json" : "";
559
+ return c.source ?? `file:./${category}/${c.id}${ext}`;
206
560
  }
207
561
  function stringifyManifest(m) {
208
562
  const out = { ...m };
@@ -216,10 +570,10 @@ function packagesOf(m, profile) {
216
570
  }
217
571
 
218
572
  // src/resolvers/file.ts
219
- import path2 from "path";
573
+ import path7 from "path";
220
574
  function resolveSource(shed, raw) {
221
575
  const s = parseSource(raw);
222
- if (s.scheme === "file") return path2.resolve(shed, s.path);
576
+ if (s.scheme === "file") return path7.resolve(shed, s.path);
223
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.`);
224
578
  }
225
579
  function isSaveable(raw) {
@@ -227,8 +581,8 @@ function isSaveable(raw) {
227
581
  }
228
582
 
229
583
  // src/state.ts
230
- import { promises as fs2 } from "fs";
231
- import path3 from "path";
584
+ import { promises as fs6 } from "fs";
585
+ import path8 from "path";
232
586
  import { z as z2 } from "zod";
233
587
  var StateSchema = z2.object({
234
588
  profile: z2.string(),
@@ -239,11 +593,11 @@ var StateSchema = z2.object({
239
593
  });
240
594
  var LSHED_DIR = "lshed";
241
595
  function statePath(adapter) {
242
- return path3.join(adapter.root, LSHED_DIR, "state.json");
596
+ return path8.join(adapter.root, LSHED_DIR, "state.json");
243
597
  }
244
598
  async function readState(adapter) {
245
599
  try {
246
- const raw = JSON.parse(await fs2.readFile(statePath(adapter), "utf8"));
600
+ const raw = JSON.parse(await fs6.readFile(statePath(adapter), "utf8"));
247
601
  return StateSchema.parse(raw);
248
602
  } catch (e) {
249
603
  if (e.code === "ENOENT") return null;
@@ -252,49 +606,46 @@ async function readState(adapter) {
252
606
  }
253
607
  async function writeState(adapter, state) {
254
608
  const p = statePath(adapter);
255
- await fs2.mkdir(path3.dirname(p), { recursive: true });
256
- await fs2.writeFile(p, JSON.stringify(state, null, 2) + "\n");
609
+ await fs6.mkdir(path8.dirname(p), { recursive: true });
610
+ await fs6.writeFile(p, JSON.stringify(state, null, 2) + "\n");
257
611
  }
258
612
 
259
- // src/ignore.ts
260
- var DEFAULT_IGNORE = [
261
- "node_modules",
262
- ".git",
263
- "__pycache__",
264
- ".venv",
265
- ".mypy_cache",
266
- ".pytest_cache",
267
- ".DS_Store",
268
- "*.log"
269
- ];
270
- function matches(name, pattern) {
271
- if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
272
- return name === pattern;
613
+ // src/core/context.ts
614
+ function spawnExec(cmd, args, cwd) {
615
+ return new Promise((resolve, reject) => {
616
+ const p = spawn2(cmd, args, { cwd, stdio: "inherit" });
617
+ p.on("error", reject);
618
+ p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} \uAC00 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4`)));
619
+ });
273
620
  }
274
- function isIgnored(rel, patterns) {
275
- if (!rel) return false;
276
- return rel.split("/").some((seg) => patterns.some((p) => matches(seg, p)));
621
+ function installersFor(ctx) {
622
+ return [gitInstaller, ...ctx.adapter.installers()].sort((a, b) => a.priority - b.priority);
623
+ }
624
+ function installerFor(ctx, source) {
625
+ const s = parseSource(source);
626
+ const scheme = s.scheme === "other" ? s.name : s.scheme;
627
+ const inst = installersFor(ctx).find((i) => i.schemes.includes(scheme));
628
+ if (!inst) throw new Error(`"${source}": \uC2A4\uD0B4 ${scheme} \uC744 \uB2E4\uB8F0 \uC124\uCE58\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`);
629
+ return inst;
277
630
  }
278
-
279
- // src/core/context.ts
280
631
  var MANIFEST_FILE = "lshed.yaml";
281
632
  var INSTRUCTIONS = "instructions";
282
633
  var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
283
634
  function manifestPath(ctx) {
284
- return path4.join(ctx.shed, MANIFEST_FILE);
635
+ return path9.join(ctx.shed, MANIFEST_FILE);
285
636
  }
286
637
  function knownCategories(adapter) {
287
- return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
638
+ return [...adapter.categories().map((c) => c.name), ...adapter.entries().map((e) => e.name), INSTRUCTIONS];
288
639
  }
289
640
  async function loadManifest(ctx) {
290
641
  let text;
291
642
  try {
292
- text = await fs3.readFile(manifestPath(ctx), "utf8");
643
+ text = await fs7.readFile(manifestPath(ctx), "utf8");
293
644
  } catch {
294
645
  throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
295
646
  \uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
296
647
  }
297
- const m = parseManifest(text, knownCategories(ctx.adapter));
648
+ const m = parseManifest(text, knownCategories(ctx.adapter), installersFor(ctx).flatMap((i) => [...i.schemes]));
298
649
  ctx.ignore = [...DEFAULT_IGNORE, ...m.ignore ?? []];
299
650
  return m;
300
651
  }
@@ -303,12 +654,22 @@ function ignoreOf(ctx) {
303
654
  }
304
655
  function targetRel(cat, id) {
305
656
  if (cat === INSTRUCTIONS) return `${FRAGMENTS_DIR}/${id}.md`;
657
+ if (cat.kind === "entry") return `${cat.name}:${id}`;
306
658
  return cat.kind === "dir" ? `${cat.root}/${id}` : `${cat.root}/${id}.md`;
307
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
+ }
308
671
  function sourcePath(ctx, category, c) {
309
- const cat = ctx.adapter.categories().find((k) => k.name === category);
310
- const kind = category === INSTRUCTIONS ? "file" : cat?.kind ?? "dir";
311
- return resolveSource(ctx.shed, effectiveSource(category, c, kind));
672
+ return resolveSource(ctx.shed, effectiveSource(category, c, kindOf(ctx, category)));
312
673
  }
313
674
  function findComponent(m, category, id) {
314
675
  const c = (m.components[category] ?? []).find((x2) => x2.id === id);
@@ -324,113 +685,29 @@ function planProfile(ctx, m, profile) {
324
685
  const items = [];
325
686
  for (const [category, ids] of Object.entries(p)) {
326
687
  if (category === PACKAGES) continue;
327
- 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);
328
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;
329
691
  for (const id of ids) {
330
692
  const component = findComponent(m, category, id);
331
- 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 });
332
694
  }
333
695
  }
334
696
  return items;
335
697
  }
336
698
  function abs(ctx, rel) {
337
- return path4.join(ctx.adapter.root, ...rel.split("/"));
699
+ return path9.join(ctx.adapter.root, ...rel.split("/"));
338
700
  }
339
701
 
340
- // src/fsutil.ts
341
- import { promises as fs4 } from "fs";
342
- import { createHash } from "crypto";
343
- import path5 from "path";
344
- async function exists(p) {
345
- try {
346
- await fs4.access(p);
347
- return true;
348
- } catch {
349
- return false;
350
- }
351
- }
352
- async function isDir(p) {
353
- try {
354
- return (await fs4.stat(p)).isDirectory();
355
- } catch {
356
- return false;
357
- }
358
- }
359
- async function listFiles(root, ignore = DEFAULT_IGNORE) {
360
- if (!await exists(root)) return [];
361
- if (!await isDir(root)) return [""];
362
- const out = [];
363
- async function walk(dir, rel) {
364
- const entries = await fs4.readdir(dir, { withFileTypes: true });
365
- for (const e of entries) {
366
- const r = rel ? `${rel}/${e.name}` : e.name;
367
- if (isIgnored(r, ignore)) continue;
368
- const full = path5.join(dir, e.name);
369
- let st;
370
- try {
371
- st = await fs4.stat(full);
372
- } catch {
373
- continue;
374
- }
375
- if (st.isDirectory()) await walk(full, r);
376
- else if (st.isFile()) out.push(r);
377
- }
378
- }
379
- await walk(root, "");
380
- return out.sort();
381
- }
382
- async function hashFile(p) {
383
- return createHash("sha256").update(await fs4.readFile(p)).digest("hex");
384
- }
385
- async function hashTree(root, ignore = DEFAULT_IGNORE) {
386
- if (!await exists(root)) return null;
387
- const h = createHash("sha256");
388
- for (const rel of await listFiles(root, ignore)) {
389
- h.update(rel).update("\0").update(await fs4.readFile(rel ? path5.join(root, rel) : root)).update("\0");
390
- }
391
- return h.digest("hex");
392
- }
393
- async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
394
- await fs4.rm(dst, { recursive: true, force: true });
395
- await fs4.mkdir(path5.dirname(dst), { recursive: true });
396
- const srcRoot = path5.resolve(src);
397
- await fs4.cp(src, dst, {
398
- recursive: true,
399
- dereference: true,
400
- filter: async (from) => {
401
- const rel = path5.relative(srcRoot, path5.resolve(from)).split(path5.sep).join("/");
402
- if (isIgnored(rel, ignore)) return false;
403
- try {
404
- if ((await fs4.lstat(from)).isSymbolicLink()) await fs4.stat(from);
405
- } catch {
406
- return false;
407
- }
408
- return true;
409
- }
410
- });
411
- }
412
- async function removeTree(p) {
413
- await fs4.rm(p, { recursive: true, force: true });
414
- }
415
- async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
416
- const l = new Set(await listFiles(local, ignore));
417
- const s = new Set(await listFiles(shed, ignore));
418
- const out = [];
419
- for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
420
- const lp = f ? path5.join(local, f) : local;
421
- const sp = f ? path5.join(shed, f) : shed;
422
- if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
423
- else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
424
- else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
425
- }
426
- return out;
427
- }
702
+ // src/core/init.ts
703
+ import { promises as fs11 } from "fs";
704
+ import path14 from "path";
428
705
 
429
706
  // src/core/instructions.ts
430
- import path6 from "path";
707
+ import path10 from "path";
431
708
  var MARKER = "<!-- generated by lshed";
432
709
  function instructionsFile(ctx) {
433
- return path6.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
710
+ return path10.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
434
711
  }
435
712
  function isGenerated(text) {
436
713
  return text.trimStart().startsWith(MARKER);
@@ -449,49 +726,23 @@ ${f.content.trimEnd()}
449
726
  }
450
727
 
451
728
  // src/core/packages.ts
452
- import { promises as fs6 } from "fs";
453
- import path9 from "path";
454
-
455
- // src/git.ts
456
- import { execFile, spawn } from "child_process";
457
- import { promisify } from "util";
458
- import path7 from "path";
459
- var x = promisify(execFile);
460
- async function git(args, cwd) {
461
- const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
462
- return stdout.trim();
463
- }
464
- var isRepo = (dir) => exists(path7.join(dir, ".git"));
465
- var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
466
- var head = (dir) => git(["rev-parse", "HEAD"], dir);
467
- var branch = async (dir) => {
468
- const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
469
- return b === "HEAD" ? void 0 : b;
470
- };
471
- var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
472
- var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
473
- var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
474
- function runShell(cmd, cwd) {
475
- return new Promise((resolve, reject) => {
476
- const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
477
- p.on("error", reject);
478
- p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
479
- });
480
- }
729
+ import { promises as fs9 } from "fs";
730
+ import path12 from "path";
481
731
 
482
732
  // src/lock.ts
483
- import { promises as fs5 } from "fs";
484
- import path8 from "path";
733
+ import { promises as fs8 } from "fs";
734
+ import path11 from "path";
485
735
  import YAML2 from "yaml";
486
736
  import { z as z3 } from "zod";
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 ?? "" }));
487
738
  var LockSchema = z3.object({
488
739
  version: z3.literal(1),
489
- packages: z3.record(z3.string(), z3.object({ source: z3.string(), commit: z3.string() })).default({})
740
+ packages: z3.record(z3.string(), EntrySchema).default({})
490
741
  });
491
742
  var LOCK_FILE = "lshed.lock";
492
743
  async function readLock(shed) {
493
744
  try {
494
- return LockSchema.parse(YAML2.parse(await fs5.readFile(path8.join(shed, LOCK_FILE), "utf8")));
745
+ return LockSchema.parse(YAML2.parse(await fs8.readFile(path11.join(shed, LOCK_FILE), "utf8")));
495
746
  } catch (e) {
496
747
  if (e.code === "ENOENT") return { version: 1, packages: {} };
497
748
  throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
@@ -499,44 +750,39 @@ async function readLock(shed) {
499
750
  }
500
751
  async function writeLock(shed, lock) {
501
752
  const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
502
- await fs5.writeFile(path8.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
753
+ await fs8.writeFile(path11.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
503
754
  }
504
755
 
505
756
  // src/core/packages.ts
506
757
  async function detectPackages(ctx, found) {
507
758
  const out = [];
508
- for (const f of found) {
509
- if (!await isRepo(f.path)) continue;
510
- const url = await remoteUrl(f.path);
511
- if (!url) continue;
512
- const into = path9.relative(ctx.adapter.root, f.path).split(path9.sep).join("/");
513
- out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), commit: await head(f.path), path: f.path });
514
- }
759
+ for (const inst of installersFor(ctx)) out.push(...await inst.detect(ctx, found));
515
760
  return out;
516
761
  }
517
762
  async function detectGenerated(found, pkgs) {
518
763
  const out = /* @__PURE__ */ new Map();
519
- if (!pkgs.length) return out;
520
- const roots = await Promise.all(pkgs.map(async (p) => ({ id: p.id, real: await fs6.realpath(p.path) })));
764
+ const located = pkgs.filter((p) => p.path);
765
+ if (!located.length) return out;
766
+ const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await fs9.realpath(p.path) })));
521
767
  for (const f of found) {
522
- if (pkgs.some((p) => p.path === f.path)) continue;
768
+ if (located.some((p) => p.path === f.path)) continue;
523
769
  let entries;
524
770
  try {
525
- if (!(await fs6.stat(f.path)).isDirectory()) continue;
526
- entries = await fs6.readdir(f.path);
771
+ if (!(await fs9.stat(f.path)).isDirectory()) continue;
772
+ entries = await fs9.readdir(f.path);
527
773
  } catch {
528
774
  continue;
529
775
  }
530
776
  for (const name of entries) {
531
- const p = path9.join(f.path, name);
777
+ const p = path12.join(f.path, name);
532
778
  let target;
533
779
  try {
534
- if (!(await fs6.lstat(p)).isSymbolicLink()) continue;
535
- target = await fs6.realpath(p).catch(async () => path9.resolve(f.path, await fs6.readlink(p)));
780
+ if (!(await fs9.lstat(p)).isSymbolicLink()) continue;
781
+ target = await fs9.realpath(p).catch(async () => path12.resolve(f.path, await fs9.readlink(p)));
536
782
  } catch {
537
783
  continue;
538
784
  }
539
- const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path9.sep));
785
+ const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path12.sep));
540
786
  if (owner) {
541
787
  out.set(`${f.category}/${f.id}`, owner.id);
542
788
  break;
@@ -546,36 +792,34 @@ async function detectGenerated(found, pkgs) {
546
792
  return out;
547
793
  }
548
794
  async function packageStatus(ctx, pkg, lock) {
549
- const dir = abs(ctx, pkg.into);
550
- const present = await isRepo(dir);
551
- return { pkg, dir, present, head: present ? await head(dir) : void 0, locked: lock.packages[pkg.id]?.commit };
795
+ const st = await installerFor(ctx, pkg.source).status(ctx, pkg);
796
+ return { pkg, ...st, locked: lock.packages[pkg.id]?.rev || void 0 };
797
+ }
798
+ var short = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
799
+ function ordered(ctx, pkgs) {
800
+ return pkgs.map((p, i) => ({ p, i, pr: installerFor(ctx, p.source).priority })).sort((a, b) => a.pr - b.pr || a.i - b.i).map((x2) => x2.p);
552
801
  }
553
802
  async function ensurePackages(ctx, pkgs, opts = {}) {
554
803
  const lock = await readLock(ctx.shed);
555
- const res = { cloned: [], pendingInstalls: [], lockChanged: false };
556
- for (const pkg of pkgs) {
804
+ const res = { installed: [], pendingInstalls: [], lockChanged: false };
805
+ for (const pkg of ordered(ctx, pkgs)) {
806
+ const inst = installerFor(ctx, pkg.source);
557
807
  const st = await packageStatus(ctx, pkg, lock);
558
808
  if (st.present) {
559
- const note = st.locked && st.head !== st.locked ? ` (HEAD ${st.head.slice(0, 7)} \u2260 lock ${st.locked.slice(0, 7)})` : "";
809
+ const note = st.locked && st.rev !== st.locked ? ` (${short(st.rev)} \u2260 lock ${short(st.locked)})` : "";
560
810
  ctx.log(` = package ${pkg.id}${note}`);
561
811
  continue;
562
812
  }
563
- if (await exists(st.dir)) throw new Error(`package ${pkg.id}: ${st.dir} \uAC00 \uC788\uC9C0\uB9CC git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uCE58\uC6B0\uAC70\uB098 into \uB97C \uBC14\uAFB8\uC138\uC694.`);
564
- const { url, ref } = cloneTarget(parseSource(pkg.source));
565
- ctx.log(` + package ${pkg.id} (clone ${url}${ref ? ` @${ref}` : ""}${st.locked ? ` \u2192 ${st.locked.slice(0, 7)}` : ""})`);
813
+ ctx.log(` + package ${pkg.id} (${inst.describe(pkg, st.locked)})`);
566
814
  if (opts.dryRun) continue;
567
- await fs6.mkdir(path9.dirname(st.dir), { recursive: true });
568
- await clone(url, st.dir, ref);
569
- if (st.locked) {
570
- await resetHard(st.dir, st.locked).catch(() => {
571
- throw new Error(`package ${pkg.id}: \uB77D\uC758 \uCEE4\uBC0B ${st.locked.slice(0, 7)} \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 'lshed update ${pkg.id}' \uB85C \uB77D\uC744 \uAC31\uC2E0\uD558\uC138\uC694.`);
572
- });
573
- } else {
574
- lock.packages[pkg.id] = { source: pkg.source, commit: await head(st.dir) };
815
+ const rev = await inst.install(ctx, pkg, st.locked, opts);
816
+ if (rev !== st.locked) {
817
+ if (st.locked) ctx.log(` \uB77D\uC740 ${short(st.locked)} \uC774\uC9C0\uB9CC ${short(rev)} \uC774 \uC124\uCE58\uB428. \uB77D\uC744 \uB530\uB77C\uAC00\uAC8C \uAC31\uC2E0`);
818
+ lock.packages[pkg.id] = { source: pkg.source, rev };
575
819
  res.lockChanged = true;
576
820
  }
577
- res.cloned.push(pkg.id);
578
- if (pkg.install) await maybeInstall(ctx, pkg, st.dir, opts, res);
821
+ res.installed.push(pkg.id);
822
+ await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
579
823
  }
580
824
  if (res.lockChanged) await writeLock(ctx.shed, lock);
581
825
  return res;
@@ -583,7 +827,7 @@ async function ensurePackages(ctx, pkgs, opts = {}) {
583
827
  async function maybeInstall(ctx, pkg, dir, opts, res) {
584
828
  if (!pkg.install) return;
585
829
  if (opts.yes) {
586
- ctx.log(` $ (${pkg.into}) ${pkg.install}`);
830
+ ctx.log(` $ (${path12.relative(ctx.adapter.root, dir) || "."}) ${pkg.install}`);
587
831
  await runShell(pkg.install, dir);
588
832
  } else {
589
833
  res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
@@ -597,33 +841,159 @@ function reportPending(ctx, res) {
597
841
  }
598
842
  async function updatePackages(ctx, pkgs, opts = {}) {
599
843
  const lock = await readLock(ctx.shed);
600
- const res = { cloned: [], pendingInstalls: [], lockChanged: false };
601
- for (const pkg of pkgs) {
844
+ const res = { installed: [], pendingInstalls: [], lockChanged: false };
845
+ for (const pkg of ordered(ctx, pkgs)) {
846
+ const inst = installerFor(ctx, pkg.source);
602
847
  const st = await packageStatus(ctx, pkg, lock);
603
848
  if (!st.present) {
604
849
  ctx.log(` ! package ${pkg.id}: \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC74C. \uBA3C\uC800 restore \uD558\uC138\uC694`);
605
850
  continue;
606
851
  }
607
852
  if (opts.dryRun) {
608
- ctx.log(` ~ package ${pkg.id} (git pull --ff-only)`);
853
+ ctx.log(` ~ package ${pkg.id} (${inst.name} update)`);
609
854
  continue;
610
855
  }
611
- await pullFf(st.dir);
612
- const now = await head(st.dir);
613
- const before = lock.packages[pkg.id]?.commit;
856
+ const now = await inst.update(ctx, pkg, opts);
857
+ const before = lock.packages[pkg.id]?.rev;
614
858
  if (now !== before) {
615
- lock.packages[pkg.id] = { source: pkg.source, commit: now };
859
+ lock.packages[pkg.id] = { source: pkg.source, rev: now };
616
860
  res.lockChanged = true;
617
- ctx.log(` \u2191 package ${pkg.id} ${before ? before.slice(0, 7) : "(\uC5C6\uC74C)"} \u2192 ${now.slice(0, 7)}`);
618
- await maybeInstall(ctx, pkg, st.dir, opts, res);
861
+ ctx.log(` \u2191 package ${pkg.id} ${before ? short(before) : "(\uC5C6\uC74C)"} \u2192 ${short(now)}`);
862
+ await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
619
863
  } else {
620
- ctx.log(` = package ${pkg.id} ${now.slice(0, 7)} (\uCD5C\uC2E0)`);
864
+ ctx.log(` = package ${pkg.id} ${short(now)} (\uCD5C\uC2E0)`);
621
865
  }
622
866
  }
623
867
  if (res.lockChanged) await writeLock(ctx.shed, lock);
624
868
  return res;
625
869
  }
626
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
+
627
997
  // src/core/init.ts
628
998
  async function init(ctx, opts = {}) {
629
999
  const profileName = opts.profile ?? "default";
@@ -640,8 +1010,9 @@ async function init(ctx, opts = {}) {
640
1010
  const generated = await detectGenerated(all, pkgs);
641
1011
  const found = all.filter((f) => !pkgs.some((p) => p.path === f.path) && !generated.has(`${f.category}/${f.id}`));
642
1012
  for (const p of pkgs) {
643
- m.packages.push({ id: p.id, source: p.source, into: p.into });
644
- ctx.log(` \u2261 package ${p.id} ${p.source} @${p.commit.slice(0, 7)} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
1013
+ m.packages.push(p.into ? { id: p.id, source: p.source, into: p.into } : { id: p.id, source: p.source });
1014
+ const rev = /^[0-9a-f]{40}$/.test(p.rev) ? p.rev.slice(0, 7) : p.rev;
1015
+ ctx.log(` \u2261 package ${p.id} ${p.source} @${rev} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
645
1016
  }
646
1017
  if (pkgs.length) m.profiles[profileName][PACKAGES] = pkgs.map((p) => p.id);
647
1018
  for (const [key, by] of generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
@@ -657,7 +1028,7 @@ async function init(ctx, opts = {}) {
657
1028
  m.components[cat.name] = [];
658
1029
  m.profiles[profileName][cat.name] = [];
659
1030
  for (const f of mine) {
660
- const dst = path10.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
1031
+ const dst = path14.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
661
1032
  await copyTree(f.path, dst, ignoreOf(ctx));
662
1033
  m.components[cat.name].push({ id: f.id });
663
1034
  m.profiles[profileName][cat.name].push(f.id);
@@ -666,34 +1037,62 @@ async function init(ctx, opts = {}) {
666
1037
  ctx.log(` + ${cat.name}/${f.id}`);
667
1038
  }
668
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
+ }
669
1067
  const instr = instructionsFile(ctx);
670
1068
  if (await exists(instr)) {
671
- const text = await fs7.readFile(instr, "utf8");
1069
+ const text = await fs11.readFile(instr, "utf8");
672
1070
  if (!isGenerated(text)) {
673
- const dst = path10.join(ctx.shed, INSTRUCTIONS, "main.md");
674
- await fs7.mkdir(path10.dirname(dst), { recursive: true });
675
- await fs7.writeFile(dst, text);
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);
676
1074
  const fragRel = targetRel(INSTRUCTIONS, "main");
677
1075
  await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
678
1076
  managed.push(fragRel);
679
1077
  m.components[INSTRUCTIONS] = [{ id: "main" }];
680
1078
  m.profiles[profileName][INSTRUCTIONS] = ["main"];
681
1079
  copied++;
682
- ctx.log(` + ${INSTRUCTIONS}/main (${path10.basename(instr)})`);
1080
+ ctx.log(` + ${INSTRUCTIONS}/main (${path14.basename(instr)})`);
683
1081
  }
684
1082
  }
685
- await fs7.mkdir(ctx.shed, { recursive: true });
1083
+ await fs11.mkdir(ctx.shed, { recursive: true });
686
1084
  let yamlText = stringifyManifest(m);
687
1085
  for (const p of pkgs) {
1086
+ if (!p.into) continue;
688
1087
  yamlText = yamlText.replace(` into: ${p.into}
689
1088
  `, ` into: ${p.into}
690
1089
  # install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
691
1090
  `);
692
1091
  }
693
- await fs7.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
1092
+ await fs11.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
694
1093
  ` + yamlText);
695
1094
  if (pkgs.length) {
696
- await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, commit: p.commit }])) });
1095
+ await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
697
1096
  }
698
1097
  await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
699
1098
  const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
@@ -702,13 +1101,13 @@ async function init(ctx, opts = {}) {
702
1101
  if (skipped.length) parts.push(`\uC81C\uC678 ${skipped.length}\uAC1C`);
703
1102
  ctx.log(`
704
1103
  ${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
705
- if (pkgs.length) ctx.log(`\uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
1104
+ if (pkgs.some((p) => p.into)) ctx.log(`git \uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
706
1105
  return { manifest: m, copied, skipped, packages: pkgs.map((p) => p.id), generated: [...generated.keys()] };
707
1106
  }
708
1107
 
709
1108
  // src/core/restore.ts
710
- import { promises as fs8 } from "fs";
711
- import path11 from "path";
1109
+ import { promises as fs12 } from "fs";
1110
+ import path15 from "path";
712
1111
  async function restore(ctx, profileArg, opts = {}) {
713
1112
  const backup = opts.backup ?? true;
714
1113
  const state = await readState(ctx.adapter);
@@ -727,22 +1126,59 @@ async function restore(ctx, profileArg, opts = {}) {
727
1126
  const oldManaged = new Set(state?.managed ?? []);
728
1127
  const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
729
1128
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
730
- const backupDir = path11.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
1129
+ const backupDir = path15.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
731
1130
  const backedUp = [];
732
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
+ };
733
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
+ }
734
1148
  const from = abs(ctx, rel);
735
1149
  if (!await exists(from)) return;
736
1150
  backedUp.push(rel);
737
1151
  if (opts.dryRun || !backup) return;
738
- await copyTree(from, path11.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
1152
+ await copyTree(from, path15.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
739
1153
  }
740
1154
  for (const rel of toRemove) {
741
1155
  ctx.log(` - ${rel}`);
742
1156
  await backUp(rel);
743
- if (!opts.dryRun) await removeTree(abs(ctx, rel));
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));
744
1161
  }
745
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
+ }
746
1182
  const target = abs(ctx, it.rel);
747
1183
  const same = await hashTree(target, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx));
748
1184
  const mark = same ? "=" : await exists(target) ? "~" : "+";
@@ -758,14 +1194,14 @@ async function restore(ctx, profileArg, opts = {}) {
758
1194
  const instrPath = instructionsFile(ctx);
759
1195
  if (fragments.length) {
760
1196
  const contents = [];
761
- for (const f of fragments) contents.push({ id: f.id, content: await fs8.readFile(f.src, "utf8") });
1197
+ for (const f of fragments) contents.push({ id: f.id, content: await fs12.readFile(f.src, "utf8") });
762
1198
  const rendered = renderInstructions(ctx, profile, contents);
763
- const existing = await exists(instrPath) ? await fs8.readFile(instrPath, "utf8") : null;
1199
+ const existing = await exists(instrPath) ? await fs12.readFile(instrPath, "utf8") : null;
764
1200
  if (existing !== rendered) {
765
1201
  const mark = existing === null ? "+" : "~";
766
1202
  ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
767
1203
  if (existing !== null) await backUp(instrRel);
768
- if (!opts.dryRun) await fs8.writeFile(instrPath, rendered);
1204
+ if (!opts.dryRun) await fs12.writeFile(instrPath, rendered);
769
1205
  } else {
770
1206
  ctx.log(` = ${instrRel}`);
771
1207
  }
@@ -775,14 +1211,22 @@ async function restore(ctx, profileArg, opts = {}) {
775
1211
  ctx.log(`
776
1212
  (dry-run) \uBCC0\uACBD \uC5C6\uC74C. \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}, \uBC31\uC5C5 \uC608\uC815 ${backedUp.length}`);
777
1213
  reportPending(ctx, pkgRes);
778
- return { profile, placed, removed: toRemove, backedUp, backupDir: null };
1214
+ reportMissingEnv(ctx, missingEnv);
1215
+ return { profile, placed, removed: toRemove, backedUp, backupDir: null, missingEnv };
779
1216
  }
780
1217
  await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
781
1218
  const bdir = backup && backedUp.length ? backupDir : null;
782
1219
  ctx.log(`
783
- \uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.cloned.length ? `, \uD328\uD0A4\uC9C0 clone ${pkgRes.cloned.length}` : ""}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
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}` : ""}`);
784
1221
  reportPending(ctx, pkgRes);
785
- return { profile, placed, removed: toRemove, backedUp, backupDir: bdir };
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(", ")}`);
786
1230
  }
787
1231
 
788
1232
  // src/core/diff.ts
@@ -791,8 +1235,16 @@ async function diff(ctx) {
791
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.");
792
1236
  const m = await loadManifest(ctx);
793
1237
  const out = [];
1238
+ const localEntries = /* @__PURE__ */ new Map();
794
1239
  for (const item of planProfile(ctx, m, state.profile)) {
795
- const changes = await diffTrees(abs(ctx, item.rel), item.src, ignoreOf(ctx));
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
+ }
796
1248
  if (changes.length) out.push({ item, changes });
797
1249
  }
798
1250
  return out;
@@ -811,12 +1263,18 @@ function formatDiff(diffs) {
811
1263
  // src/core/status.ts
812
1264
  async function status(ctx) {
813
1265
  const state = await readState(ctx.adapter);
814
- if (!state) return { state: null, drifted: [], packages: [] };
1266
+ if (!state) return { state: null, drifted: [], packages: [], missingEnv: [] };
815
1267
  const d = await diff(ctx);
816
1268
  const m = await loadManifest(ctx);
817
1269
  const lock = await readLock(ctx.shed);
818
1270
  const packages = await Promise.all(packagesOf(m, state.profile).map((p) => packageStatus(ctx, p, lock)));
819
- return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages };
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 };
820
1278
  }
821
1279
  function formatStatus(s, adapterRoot) {
822
1280
  if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
@@ -828,10 +1286,12 @@ function formatStatus(s, adapterRoot) {
828
1286
  `\uAD00\uB9AC \uC911 ${s.state.managed.length}\uAC1C \uACBD\uB85C (${adapterRoot})`
829
1287
  ];
830
1288
  lines.push(s.drifted.length ? `\uB4DC\uB9AC\uD504\uD2B8 ${s.drifted.length}\uAC1C: ${s.drifted.join(", ")} \u2192 lshed diff` : "\uB4DC\uB9AC\uD504\uD2B8 \uC5C6\uC74C");
1289
+ const short2 = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
831
1290
  for (const p of s.packages) {
832
- const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${p.head.slice(0, 7)} (\uB77D \uC5C6\uC74C)` : p.head === p.locked ? `${p.head.slice(0, 7)} = lock` : `${p.head.slice(0, 7)} \u2260 lock ${p.locked.slice(0, 7)} \u2192 lshed update`;
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`;
833
1292
  lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
834
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`);
835
1295
  return lines.join("\n");
836
1296
  }
837
1297
 
@@ -851,13 +1311,27 @@ async function save(ctx, ids = []) {
851
1311
  });
852
1312
  }
853
1313
  const saved = [];
1314
+ const localEntries = /* @__PURE__ */ new Map();
854
1315
  for (const it of plan) {
855
- const kind = it.category === INSTRUCTIONS ? "file" : ctx.adapter.categories().find((c) => c.name === it.category).kind;
856
- const src = effectiveSource(it.category, it.component, kind);
1316
+ const src = effectiveSource(it.category, it.component, kindOf(ctx, it.category));
857
1317
  if (!isSaveable(src)) {
858
1318
  ctx.log(` ! ${it.category}/${it.id}: \uC6D0\uACA9 \uCD9C\uCC98(${src})\uB294 save \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
859
1319
  continue;
860
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
+ }
861
1335
  const local = abs(ctx, it.rel);
862
1336
  if (!await exists(local)) {
863
1337
  ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
@@ -897,8 +1371,8 @@ function formatRows(rows, m) {
897
1371
  }
898
1372
 
899
1373
  // src/core/remove.ts
900
- import { promises as fs9 } from "fs";
901
- import path12 from "path";
1374
+ import { promises as fs13 } from "fs";
1375
+ import path16 from "path";
902
1376
  import YAML3, { isSeq, isMap } from "yaml";
903
1377
  function resolveKey(m, raw) {
904
1378
  const rows = listRows(m);
@@ -913,7 +1387,7 @@ async function remove(ctx, raw) {
913
1387
  const { category, id } = resolveKey(m, raw);
914
1388
  const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
915
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.`);
916
- const text = await fs9.readFile(manifestPath(ctx), "utf8");
1390
+ const text = await fs13.readFile(manifestPath(ctx), "utf8");
917
1391
  const doc = YAML3.parseDocument(text);
918
1392
  let deleted;
919
1393
  if (category === PACKAGES) {
@@ -935,14 +1409,14 @@ async function remove(ctx, raw) {
935
1409
  seq.delete(idx);
936
1410
  if (!seq.items.length) doc.deleteIn(["components", category]);
937
1411
  const src = sourcePath(ctx, category, findComponent(m, category, id));
938
- const inside = !path12.relative(ctx.shed, src).startsWith("..");
1412
+ const inside = !path16.relative(ctx.shed, src).startsWith("..");
939
1413
  if (inside && await exists(src)) {
940
1414
  await removeTree(src);
941
1415
  deleted = src;
942
1416
  }
943
1417
  ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
944
1418
  }
945
- await fs9.writeFile(manifestPath(ctx), doc.toString());
1419
+ await fs13.writeFile(manifestPath(ctx), doc.toString());
946
1420
  return { category, id, deleted };
947
1421
  }
948
1422
  async function prune(ctx, opts = {}) {
@@ -972,16 +1446,16 @@ var { version } = createRequire(import.meta.url)("../package.json");
972
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)");
973
1447
  function adapterFromOpts() {
974
1448
  const { root } = program.opts();
975
- return new ClaudeCodeAdapter(root ? path13.resolve(root) : void 0);
1449
+ return new ClaudeCodeAdapter(root ? path17.resolve(root) : void 0);
976
1450
  }
977
1451
  async function ctxFor(cmd) {
978
1452
  const adapter = adapterFromOpts();
979
1453
  const { shed: flag } = program.opts();
980
1454
  let shed = flag ?? process.env.LSHED_HOME;
981
1455
  if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
982
- if (!shed && cmd === "init") shed = path13.join(os2.homedir(), "lshed");
1456
+ if (!shed && cmd === "init") shed = path17.join(os2.homedir(), "lshed");
983
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.");
984
- return { adapter, shed: path13.resolve(shed), log: (l) => console.log(l) };
1458
+ return { adapter, shed: path17.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
985
1459
  }
986
1460
  async function run(fn) {
987
1461
  try {
@@ -1026,7 +1500,7 @@ program.command("status").description("show the applied profile, managed paths a
1026
1500
  const adapter = adapterFromOpts();
1027
1501
  const state = await readState(adapter);
1028
1502
  if (!state) {
1029
- console.log(formatStatus({ state: null, drifted: [], packages: [] }, adapter.root));
1503
+ console.log(formatStatus({ state: null, drifted: [], packages: [], missingEnv: [] }, adapter.root));
1030
1504
  return;
1031
1505
  }
1032
1506
  const ctx = await ctxFor("other");
@@ -1058,6 +1532,11 @@ program.command("scan").description("(debug) list components found in the agent
1058
1532
  const adapter = adapterFromOpts();
1059
1533
  const found = await adapter.scan();
1060
1534
  for (const c of found) console.log(`${c.category}/${c.id} ${c.path}`);
1061
- console.error(`${found.length}\uAC1C \uBC1C\uACAC (root: ${adapter.root})`);
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})`);
1062
1541
  }));
1063
1542
  program.parseAsync();