lshed 0.1.0 → 0.3.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,169 @@
4
4
  import { Command } from "commander";
5
5
  import { createRequire } from "module";
6
6
  import os2 from "os";
7
- import path9 from "path";
7
+ import path15 from "path";
8
8
 
9
9
  // src/adapters/claude-code.ts
10
+ import { promises as fs2 } from "fs";
11
+ import path2 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-code.ts
13
170
  var CATEGORIES = [
14
171
  { name: "skills", root: "skills", kind: "dir" },
15
172
  { name: "agents", root: "agents", kind: "file" },
@@ -20,7 +177,7 @@ var ClaudeCodeAdapter = class {
20
177
  name = "claude-code";
21
178
  root;
22
179
  constructor(root) {
23
- this.root = root ?? path.join(os.homedir(), ".claude");
180
+ this.root = root ?? path2.join(os.homedir(), ".claude");
24
181
  }
25
182
  categories() {
26
183
  return CATEGORIES;
@@ -31,22 +188,32 @@ var ClaudeCodeAdapter = class {
31
188
  instructionsFileName() {
32
189
  return "CLAUDE.md";
33
190
  }
191
+ installers() {
192
+ return [marketplaceInstaller, pluginInstaller];
193
+ }
34
194
  async scan() {
35
195
  const out = [];
36
196
  for (const cat of CATEGORIES) {
37
- const dir = path.join(this.root, cat.root);
197
+ const dir = path2.join(this.root, cat.root);
38
198
  let entries;
39
199
  try {
40
- entries = await fs.readdir(dir, { withFileTypes: true });
200
+ entries = await fs2.readdir(dir, { withFileTypes: true });
41
201
  } catch {
42
202
  continue;
43
203
  }
44
204
  for (const e of entries) {
45
205
  if (e.name.startsWith(".")) continue;
46
- if (cat.kind === "dir" && e.isDirectory()) {
47
- out.push({ category: cat.name, id: e.name, path: path.join(dir, e.name) });
48
- } else if (cat.kind === "file" && e.isFile() && e.name.endsWith(".md")) {
49
- out.push({ category: cat.name, id: e.name.slice(0, -3), path: path.join(dir, e.name) });
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 });
50
217
  }
51
218
  }
52
219
  }
@@ -54,59 +221,236 @@ var ClaudeCodeAdapter = class {
54
221
  }
55
222
  };
56
223
 
57
- // src/core/init.ts
58
- import { promises as fs5 } from "fs";
59
- import path7 from "path";
60
-
61
224
  // src/core/context.ts
62
- import { promises as fs3 } from "fs";
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";
63
236
  import path4 from "path";
64
237
 
65
- // src/manifest.ts
66
- import { z } from "zod";
67
- import YAML from "yaml";
238
+ // src/fsutil.ts
239
+ import { promises as fs3 } from "fs";
240
+ import { createHash } from "crypto";
241
+ import path3 from "path";
68
242
 
69
- // src/source.ts
70
- var GITHUB_RE = /^([\w.-]+)\/([\w.-]+)(?:@([^#]+))?(?:#(.+))?$/;
71
- function parseSource(raw) {
72
- const idx = raw.indexOf(":");
73
- if (idx <= 0) {
74
- throw new Error(`source\uC5D0 \uC2A4\uD0B4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}" (\uC608: file:./skills/x, github:user/repo@v1)`);
243
+ // src/ignore.ts
244
+ var DEFAULT_IGNORE = [
245
+ "node_modules",
246
+ ".git",
247
+ "__pycache__",
248
+ ".venv",
249
+ ".mypy_cache",
250
+ ".pytest_cache",
251
+ ".DS_Store",
252
+ "*.log"
253
+ ];
254
+ function matches(name, pattern) {
255
+ if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
256
+ return name === pattern;
257
+ }
258
+ function isIgnored(rel, patterns) {
259
+ if (!rel) return false;
260
+ return rel.split("/").some((seg) => patterns.some((p) => matches(seg, p)));
261
+ }
262
+
263
+ // src/fsutil.ts
264
+ async function exists(p) {
265
+ try {
266
+ await fs3.access(p);
267
+ return true;
268
+ } catch {
269
+ return false;
75
270
  }
76
- const scheme = raw.slice(0, idx);
77
- const rest = raw.slice(idx + 1);
78
- switch (scheme) {
79
- case "file":
80
- if (!rest) throw new Error(`file: \uB4A4\uC5D0 \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
81
- return { scheme, path: rest };
82
- case "github": {
83
- const m = GITHUB_RE.exec(rest);
84
- if (!m) throw new Error(`github: \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: "${raw}" (\uC608: github:user/repo@v1.0#sub/path)`);
85
- const [, owner, repo, ref, subpath] = m;
86
- return { scheme, owner, repo, ref, subpath };
271
+ }
272
+ async function isDir(p) {
273
+ try {
274
+ return (await fs3.stat(p)).isDirectory();
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+ async function listFiles(root, ignore = DEFAULT_IGNORE) {
280
+ if (!await exists(root)) return [];
281
+ if (!await isDir(root)) return [""];
282
+ const out = [];
283
+ async function walk(dir, rel) {
284
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
285
+ for (const e of entries) {
286
+ const r = rel ? `${rel}/${e.name}` : e.name;
287
+ if (isIgnored(r, ignore)) continue;
288
+ const full = path3.join(dir, e.name);
289
+ let st;
290
+ try {
291
+ st = await fs3.stat(full);
292
+ } catch {
293
+ continue;
294
+ }
295
+ if (st.isDirectory()) await walk(full, r);
296
+ else if (st.isFile()) out.push(r);
87
297
  }
88
- default:
89
- throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
90
298
  }
299
+ await walk(root, "");
300
+ return out.sort();
301
+ }
302
+ async function hashFile(p) {
303
+ return createHash("sha256").update(await fs3.readFile(p)).digest("hex");
304
+ }
305
+ async function hashTree(root, ignore = DEFAULT_IGNORE) {
306
+ if (!await exists(root)) return null;
307
+ const h = createHash("sha256");
308
+ for (const rel of await listFiles(root, ignore)) {
309
+ h.update(rel).update("\0").update(await fs3.readFile(rel ? path3.join(root, rel) : root)).update("\0");
310
+ }
311
+ return h.digest("hex");
312
+ }
313
+ async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
314
+ await fs3.rm(dst, { recursive: true, force: true });
315
+ await fs3.mkdir(path3.dirname(dst), { recursive: true });
316
+ const srcRoot = path3.resolve(src);
317
+ await fs3.cp(src, dst, {
318
+ recursive: true,
319
+ dereference: true,
320
+ filter: async (from) => {
321
+ const rel = path3.relative(srcRoot, path3.resolve(from)).split(path3.sep).join("/");
322
+ if (isIgnored(rel, ignore)) return false;
323
+ try {
324
+ if ((await fs3.lstat(from)).isSymbolicLink()) await fs3.stat(from);
325
+ } catch {
326
+ return false;
327
+ }
328
+ return true;
329
+ }
330
+ });
331
+ }
332
+ async function removeTree(p) {
333
+ await fs3.rm(p, { recursive: true, force: true });
334
+ }
335
+ async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
336
+ const l = new Set(await listFiles(local, ignore));
337
+ const s = new Set(await listFiles(shed, ignore));
338
+ const out = [];
339
+ for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
340
+ const lp = f ? path3.join(local, f) : local;
341
+ const sp = f ? path3.join(shed, f) : shed;
342
+ if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
343
+ else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
344
+ else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
345
+ }
346
+ return out;
91
347
  }
92
348
 
349
+ // src/git.ts
350
+ var x = promisify(execFile);
351
+ async function git(args, cwd) {
352
+ const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
353
+ return stdout.trim();
354
+ }
355
+ var isRepo = (dir) => exists(path4.join(dir, ".git"));
356
+ var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
357
+ var head = (dir) => git(["rev-parse", "HEAD"], dir);
358
+ var branch = async (dir) => {
359
+ const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
360
+ return b === "HEAD" ? void 0 : b;
361
+ };
362
+ var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
363
+ var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
364
+ var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
365
+ function runShell(cmd, cwd) {
366
+ return new Promise((resolve, reject) => {
367
+ const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
368
+ p.on("error", reject);
369
+ p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
370
+ });
371
+ }
372
+
373
+ // src/installers/git.ts
374
+ function dirOf(ctx, pkg) {
375
+ 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 path5.join(ctx.adapter.root, ...pkg.into.split("/"));
377
+ }
378
+ var gitInstaller = {
379
+ name: "git",
380
+ schemes: ["github", "git"],
381
+ priority: 0,
382
+ async detect(ctx, found) {
383
+ const out = [];
384
+ for (const f of found) {
385
+ if (!await isRepo(f.path)) continue;
386
+ const url = await remoteUrl(f.path);
387
+ if (!url) continue;
388
+ const into = path5.relative(ctx.adapter.root, f.path).split(path5.sep).join("/");
389
+ out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), rev: await head(f.path), path: f.path });
390
+ }
391
+ return out;
392
+ },
393
+ async status(ctx, pkg) {
394
+ const dir = dirOf(ctx, pkg);
395
+ const present = await isRepo(dir);
396
+ return { present, rev: present ? await head(dir) : void 0 };
397
+ },
398
+ async install(ctx, pkg, locked, _opts) {
399
+ const dir = dirOf(ctx, pkg);
400
+ 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
+ const { url, ref } = cloneTarget(parseSource(pkg.source));
402
+ await fs4.mkdir(path5.dirname(dir), { recursive: true });
403
+ await clone(url, dir, ref);
404
+ if (locked) {
405
+ await resetHard(dir, locked).catch(() => {
406
+ 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.`);
407
+ });
408
+ }
409
+ return head(dir);
410
+ },
411
+ async update(ctx, pkg) {
412
+ const dir = dirOf(ctx, pkg);
413
+ await pullFf(dir);
414
+ return head(dir);
415
+ },
416
+ describe(pkg, locked) {
417
+ const { url, ref } = cloneTarget(parseSource(pkg.source));
418
+ return `clone ${url}${ref ? ` @${ref}` : ""}${locked ? ` \u2192 ${locked.slice(0, 7)}` : ""}`;
419
+ },
420
+ cwd: dirOf
421
+ };
422
+
93
423
  // src/manifest.ts
424
+ import { z } from "zod";
425
+ import YAML from "yaml";
94
426
  var ComponentSchema = z.object({
95
427
  id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
96
428
  source: z.string().optional(),
97
429
  tags: z.array(z.string()).optional()
98
430
  });
431
+ var PackageSchema = z.object({
432
+ id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
433
+ source: z.string(),
434
+ /** git 계열 패키지의 위치 (어댑터 루트 기준). 어댑터 설치기 스킴은 필요 없다 */
435
+ into: z.string().regex(/^[^/\\][^\\]*$/, "into \uB294 \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C (POSIX)").optional(),
436
+ /** 설치 후 실행할 셸 명령. --yes 일 때만 실행 */
437
+ install: z.string().optional()
438
+ });
439
+ var PACKAGES = "packages";
99
440
  var ComponentsSchema = z.record(z.string(), z.array(ComponentSchema));
100
441
  var ProfileSchema = z.record(z.string(), z.array(z.string()));
101
442
  var ManifestSchema = z.object({
102
443
  version: z.literal(1),
103
444
  agent: z.string().default("claude-code"),
445
+ /** 창고에 담지 않을 이름들. 기본값(DEFAULT_IGNORE)에 더해진다. */
446
+ ignore: z.array(z.string()).optional(),
104
447
  components: ComponentsSchema.default({}),
448
+ packages: z.array(PackageSchema).default([]),
105
449
  profiles: z.record(z.string(), ProfileSchema).default({})
106
450
  });
107
451
  var ManifestError = class extends Error {
108
452
  };
109
- function parseManifest(text, knownCategories2) {
453
+ function parseManifest(text, knownCategories2, knownSchemes) {
110
454
  const raw = YAML.parse(text);
111
455
  const result = ManifestSchema.safeParse(raw);
112
456
  if (!result.success) {
@@ -125,14 +469,32 @@ ${lines.join("\n")}`);
125
469
  if (seen.has(c.id)) problems.push(`${cat}: id "${c.id}" \uC911\uBCF5`);
126
470
  seen.add(c.id);
127
471
  try {
128
- parseSource(effectiveSource(cat, c));
472
+ 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`);
129
473
  } catch (e) {
130
474
  problems.push(`${cat}/${c.id}: ${e.message}`);
131
475
  }
132
476
  }
133
477
  }
478
+ const pkgIds = /* @__PURE__ */ new Set();
479
+ for (const p of m.packages) {
480
+ if (pkgIds.has(p.id)) problems.push(`packages: id "${p.id}" \uC911\uBCF5`);
481
+ pkgIds.add(p.id);
482
+ try {
483
+ const src = parseSource(p.source);
484
+ const scheme = src.scheme === "other" ? src.name : src.scheme;
485
+ if (scheme === "file") problems.push(`packages/${p.id}: \uD328\uD0A4\uC9C0 \uCD9C\uCC98\uB294 file: \uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
486
+ 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(", ")})`);
487
+ if ((scheme === "github" || scheme === "git") && !p.into) problems.push(`packages/${p.id}: ${scheme}: \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
488
+ } catch (e) {
489
+ problems.push(`packages/${p.id}: ${e.message}`);
490
+ }
491
+ }
134
492
  for (const [profile, cats] of Object.entries(m.profiles)) {
135
493
  for (const [cat, ids] of Object.entries(cats)) {
494
+ if (cat === PACKAGES) {
495
+ for (const id of ids) if (!pkgIds.has(id)) problems.push(`profiles.${profile}.packages: "${id}" \uB294 packages \uC5D0 \uC5C6\uC74C`);
496
+ continue;
497
+ }
136
498
  const available = new Set((m.components[cat] ?? []).map((c) => c.id));
137
499
  for (const id of ids) {
138
500
  if (!available.has(id)) problems.push(`profiles.${profile}.${cat}: "${id}" \uB294 components\uC5D0 \uC5C6\uC74C`);
@@ -147,14 +509,21 @@ function effectiveSource(category, c, kind = "dir") {
147
509
  return c.source ?? `file:./${category}/${c.id}${kind === "file" ? ".md" : ""}`;
148
510
  }
149
511
  function stringifyManifest(m) {
150
- return YAML.stringify(m, { lineWidth: 0 });
512
+ const out = { ...m };
513
+ if (!m.packages.length) delete out.packages;
514
+ if (!m.ignore?.length) delete out.ignore;
515
+ return YAML.stringify(out, { lineWidth: 0 });
516
+ }
517
+ function packagesOf(m, profile) {
518
+ const ids = m.profiles[profile]?.[PACKAGES] ?? [];
519
+ return ids.map((id) => m.packages.find((p) => p.id === id));
151
520
  }
152
521
 
153
522
  // src/resolvers/file.ts
154
- import path2 from "path";
523
+ import path6 from "path";
155
524
  function resolveSource(shed, raw) {
156
525
  const s = parseSource(raw);
157
- if (s.scheme === "file") return path2.resolve(shed, s.path);
526
+ if (s.scheme === "file") return path6.resolve(shed, s.path);
158
527
  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.`);
159
528
  }
160
529
  function isSaveable(raw) {
@@ -162,8 +531,8 @@ function isSaveable(raw) {
162
531
  }
163
532
 
164
533
  // src/state.ts
165
- import { promises as fs2 } from "fs";
166
- import path3 from "path";
534
+ import { promises as fs5 } from "fs";
535
+ import path7 from "path";
167
536
  import { z as z2 } from "zod";
168
537
  var StateSchema = z2.object({
169
538
  profile: z2.string(),
@@ -174,11 +543,11 @@ var StateSchema = z2.object({
174
543
  });
175
544
  var LSHED_DIR = "lshed";
176
545
  function statePath(adapter) {
177
- return path3.join(adapter.root, LSHED_DIR, "state.json");
546
+ return path7.join(adapter.root, LSHED_DIR, "state.json");
178
547
  }
179
548
  async function readState(adapter) {
180
549
  try {
181
- const raw = JSON.parse(await fs2.readFile(statePath(adapter), "utf8"));
550
+ const raw = JSON.parse(await fs5.readFile(statePath(adapter), "utf8"));
182
551
  return StateSchema.parse(raw);
183
552
  } catch (e) {
184
553
  if (e.code === "ENOENT") return null;
@@ -187,16 +556,33 @@ async function readState(adapter) {
187
556
  }
188
557
  async function writeState(adapter, state) {
189
558
  const p = statePath(adapter);
190
- await fs2.mkdir(path3.dirname(p), { recursive: true });
191
- await fs2.writeFile(p, JSON.stringify(state, null, 2) + "\n");
559
+ await fs5.mkdir(path7.dirname(p), { recursive: true });
560
+ await fs5.writeFile(p, JSON.stringify(state, null, 2) + "\n");
192
561
  }
193
562
 
194
563
  // src/core/context.ts
564
+ function spawnExec(cmd, args, cwd) {
565
+ return new Promise((resolve, reject) => {
566
+ const p = spawn2(cmd, args, { cwd, stdio: "inherit" });
567
+ p.on("error", reject);
568
+ p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} \uAC00 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4`)));
569
+ });
570
+ }
571
+ function installersFor(ctx) {
572
+ return [gitInstaller, ...ctx.adapter.installers()].sort((a, b) => a.priority - b.priority);
573
+ }
574
+ function installerFor(ctx, source) {
575
+ const s = parseSource(source);
576
+ const scheme = s.scheme === "other" ? s.name : s.scheme;
577
+ const inst = installersFor(ctx).find((i) => i.schemes.includes(scheme));
578
+ if (!inst) throw new Error(`"${source}": \uC2A4\uD0B4 ${scheme} \uC744 \uB2E4\uB8F0 \uC124\uCE58\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`);
579
+ return inst;
580
+ }
195
581
  var MANIFEST_FILE = "lshed.yaml";
196
582
  var INSTRUCTIONS = "instructions";
197
583
  var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
198
584
  function manifestPath(ctx) {
199
- return path4.join(ctx.shed, MANIFEST_FILE);
585
+ return path8.join(ctx.shed, MANIFEST_FILE);
200
586
  }
201
587
  function knownCategories(adapter) {
202
588
  return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
@@ -204,12 +590,17 @@ function knownCategories(adapter) {
204
590
  async function loadManifest(ctx) {
205
591
  let text;
206
592
  try {
207
- text = await fs3.readFile(manifestPath(ctx), "utf8");
593
+ text = await fs6.readFile(manifestPath(ctx), "utf8");
208
594
  } catch {
209
595
  throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
210
596
  \uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
211
597
  }
212
- return parseManifest(text, knownCategories(ctx.adapter));
598
+ const m = parseManifest(text, knownCategories(ctx.adapter), installersFor(ctx).flatMap((i) => [...i.schemes]));
599
+ ctx.ignore = [...DEFAULT_IGNORE, ...m.ignore ?? []];
600
+ return m;
601
+ }
602
+ function ignoreOf(ctx) {
603
+ return ctx.ignore ?? DEFAULT_IGNORE;
213
604
  }
214
605
  function targetRel(cat, id) {
215
606
  if (cat === INSTRUCTIONS) return `${FRAGMENTS_DIR}/${id}.md`;
@@ -221,7 +612,7 @@ function sourcePath(ctx, category, c) {
221
612
  return resolveSource(ctx.shed, effectiveSource(category, c, kind));
222
613
  }
223
614
  function findComponent(m, category, id) {
224
- const c = (m.components[category] ?? []).find((x) => x.id === id);
615
+ const c = (m.components[category] ?? []).find((x2) => x2.id === id);
225
616
  if (!c) throw new Error(`${category}/${id} \uB294 components \uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4`);
226
617
  return c;
227
618
  }
@@ -233,6 +624,7 @@ function planProfile(ctx, m, profile) {
233
624
  }
234
625
  const items = [];
235
626
  for (const [category, ids] of Object.entries(p)) {
627
+ if (category === PACKAGES) continue;
236
628
  const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category);
237
629
  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`);
238
630
  for (const id of ids) {
@@ -243,117 +635,214 @@ function planProfile(ctx, m, profile) {
243
635
  return items;
244
636
  }
245
637
  function abs(ctx, rel) {
246
- return path4.join(ctx.adapter.root, ...rel.split("/"));
638
+ return path8.join(ctx.adapter.root, ...rel.split("/"));
247
639
  }
248
640
 
249
- // src/fsutil.ts
250
- import { promises as fs4 } from "fs";
251
- import { createHash } from "crypto";
252
- import path5 from "path";
253
- async function exists(p) {
254
- try {
255
- await fs4.access(p);
256
- return true;
257
- } catch {
258
- return false;
259
- }
260
- }
261
- async function isDir(p) {
262
- try {
263
- return (await fs4.stat(p)).isDirectory();
264
- } catch {
265
- return false;
266
- }
267
- }
268
- async function listFiles(root) {
269
- if (!await exists(root)) return [];
270
- if (!await isDir(root)) return [""];
271
- const out = [];
272
- async function walk(dir, rel) {
273
- const entries = await fs4.readdir(dir, { withFileTypes: true });
274
- for (const e of entries) {
275
- const r = rel ? `${rel}/${e.name}` : e.name;
276
- if (e.isDirectory()) await walk(path5.join(dir, e.name), r);
277
- else if (e.isFile()) out.push(r);
278
- }
279
- }
280
- await walk(root, "");
281
- return out.sort();
282
- }
283
- async function hashFile(p) {
284
- return createHash("sha256").update(await fs4.readFile(p)).digest("hex");
285
- }
286
- async function hashTree(root) {
287
- if (!await exists(root)) return null;
288
- const h = createHash("sha256");
289
- for (const rel of await listFiles(root)) {
290
- h.update(rel).update("\0").update(await fs4.readFile(rel ? path5.join(root, rel) : root)).update("\0");
291
- }
292
- return h.digest("hex");
293
- }
294
- async function copyTree(src, dst) {
295
- await fs4.rm(dst, { recursive: true, force: true });
296
- await fs4.mkdir(path5.dirname(dst), { recursive: true });
297
- await fs4.cp(src, dst, { recursive: true });
298
- }
299
- async function removeTree(p) {
300
- await fs4.rm(p, { recursive: true, force: true });
301
- }
302
- async function diffTrees(local, shed) {
303
- const l = new Set(await listFiles(local));
304
- const s = new Set(await listFiles(shed));
305
- const out = [];
306
- for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
307
- const lp = f ? path5.join(local, f) : local;
308
- const sp = f ? path5.join(shed, f) : shed;
309
- if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
310
- else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
311
- else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
312
- }
313
- return out;
314
- }
641
+ // src/core/init.ts
642
+ import { promises as fs9 } from "fs";
643
+ import path12 from "path";
315
644
 
316
645
  // src/core/instructions.ts
317
- import path6 from "path";
646
+ import path9 from "path";
318
647
  var MARKER = "<!-- generated by lshed";
319
648
  function instructionsFile(ctx) {
320
- return path6.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
649
+ return path9.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
321
650
  }
322
651
  function isGenerated(text) {
323
652
  return text.trimStart().startsWith(MARKER);
324
653
  }
325
654
  function renderInstructions(ctx, profile, fragments) {
326
- const head = `${MARKER}; profile: ${profile} -->
655
+ const head2 = `${MARKER}; profile: ${profile} -->
327
656
  <!-- Do not edit this file. Edit the fragments in your shed and run 'lshed restore'. -->
328
657
 
329
658
  `;
330
659
  if (ctx.adapter.instructionsStrategy() === "import") {
331
- return head + fragments.map((f) => `@${FRAGMENTS_DIR}/${f.id}.md`).join("\n") + "\n";
660
+ return head2 + fragments.map((f) => `@${FRAGMENTS_DIR}/${f.id}.md`).join("\n") + "\n";
332
661
  }
333
- return head + fragments.map((f) => `<!-- ${f.id} -->
662
+ return head2 + fragments.map((f) => `<!-- ${f.id} -->
334
663
  ${f.content.trimEnd()}
335
664
  `).join("\n");
336
665
  }
337
666
 
667
+ // src/core/packages.ts
668
+ import { promises as fs8 } from "fs";
669
+ import path11 from "path";
670
+
671
+ // src/lock.ts
672
+ import { promises as fs7 } from "fs";
673
+ import path10 from "path";
674
+ import YAML2 from "yaml";
675
+ import { z as z3 } from "zod";
676
+ 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 ?? "" }));
677
+ var LockSchema = z3.object({
678
+ version: z3.literal(1),
679
+ packages: z3.record(z3.string(), EntrySchema).default({})
680
+ });
681
+ var LOCK_FILE = "lshed.lock";
682
+ async function readLock(shed) {
683
+ try {
684
+ return LockSchema.parse(YAML2.parse(await fs7.readFile(path10.join(shed, LOCK_FILE), "utf8")));
685
+ } catch (e) {
686
+ if (e.code === "ENOENT") return { version: 1, packages: {} };
687
+ throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
688
+ }
689
+ }
690
+ async function writeLock(shed, lock) {
691
+ const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
692
+ await fs7.writeFile(path10.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
693
+ }
694
+
695
+ // src/core/packages.ts
696
+ async function detectPackages(ctx, found) {
697
+ const out = [];
698
+ for (const inst of installersFor(ctx)) out.push(...await inst.detect(ctx, found));
699
+ return out;
700
+ }
701
+ async function detectGenerated(found, pkgs) {
702
+ const out = /* @__PURE__ */ new Map();
703
+ const located = pkgs.filter((p) => p.path);
704
+ if (!located.length) return out;
705
+ const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await fs8.realpath(p.path) })));
706
+ for (const f of found) {
707
+ if (located.some((p) => p.path === f.path)) continue;
708
+ let entries;
709
+ try {
710
+ if (!(await fs8.stat(f.path)).isDirectory()) continue;
711
+ entries = await fs8.readdir(f.path);
712
+ } catch {
713
+ continue;
714
+ }
715
+ for (const name of entries) {
716
+ const p = path11.join(f.path, name);
717
+ let target;
718
+ try {
719
+ if (!(await fs8.lstat(p)).isSymbolicLink()) continue;
720
+ target = await fs8.realpath(p).catch(async () => path11.resolve(f.path, await fs8.readlink(p)));
721
+ } catch {
722
+ continue;
723
+ }
724
+ const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path11.sep));
725
+ if (owner) {
726
+ out.set(`${f.category}/${f.id}`, owner.id);
727
+ break;
728
+ }
729
+ }
730
+ }
731
+ return out;
732
+ }
733
+ async function packageStatus(ctx, pkg, lock) {
734
+ const st = await installerFor(ctx, pkg.source).status(ctx, pkg);
735
+ return { pkg, ...st, locked: lock.packages[pkg.id]?.rev || void 0 };
736
+ }
737
+ var short = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
738
+ function ordered(ctx, pkgs) {
739
+ 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);
740
+ }
741
+ async function ensurePackages(ctx, pkgs, opts = {}) {
742
+ const lock = await readLock(ctx.shed);
743
+ const res = { installed: [], pendingInstalls: [], lockChanged: false };
744
+ for (const pkg of ordered(ctx, pkgs)) {
745
+ const inst = installerFor(ctx, pkg.source);
746
+ const st = await packageStatus(ctx, pkg, lock);
747
+ if (st.present) {
748
+ const note = st.locked && st.rev !== st.locked ? ` (${short(st.rev)} \u2260 lock ${short(st.locked)})` : "";
749
+ ctx.log(` = package ${pkg.id}${note}`);
750
+ continue;
751
+ }
752
+ ctx.log(` + package ${pkg.id} (${inst.describe(pkg, st.locked)})`);
753
+ if (opts.dryRun) continue;
754
+ const rev = await inst.install(ctx, pkg, st.locked, opts);
755
+ if (rev !== st.locked) {
756
+ 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`);
757
+ lock.packages[pkg.id] = { source: pkg.source, rev };
758
+ res.lockChanged = true;
759
+ }
760
+ res.installed.push(pkg.id);
761
+ await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
762
+ }
763
+ if (res.lockChanged) await writeLock(ctx.shed, lock);
764
+ return res;
765
+ }
766
+ async function maybeInstall(ctx, pkg, dir, opts, res) {
767
+ if (!pkg.install) return;
768
+ if (opts.yes) {
769
+ ctx.log(` $ (${path11.relative(ctx.adapter.root, dir) || "."}) ${pkg.install}`);
770
+ await runShell(pkg.install, dir);
771
+ } else {
772
+ res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
773
+ }
774
+ }
775
+ function reportPending(ctx, res) {
776
+ if (!res.pendingInstalls.length) return;
777
+ ctx.log(`
778
+ \uC124\uCE58 \uBA85\uB839 ${res.pendingInstalls.length}\uAC1C\uB97C \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD655\uC778 \uD6C4 '--yes' \uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uAC70\uB098 \uC9C1\uC811 \uB3CC\uB9AC\uC138\uC694:`);
779
+ for (const p of res.pendingInstalls) ctx.log(` cd ${p.dir} && ${p.cmd}`);
780
+ }
781
+ async function updatePackages(ctx, pkgs, opts = {}) {
782
+ const lock = await readLock(ctx.shed);
783
+ const res = { installed: [], pendingInstalls: [], lockChanged: false };
784
+ for (const pkg of ordered(ctx, pkgs)) {
785
+ const inst = installerFor(ctx, pkg.source);
786
+ const st = await packageStatus(ctx, pkg, lock);
787
+ if (!st.present) {
788
+ ctx.log(` ! package ${pkg.id}: \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC74C. \uBA3C\uC800 restore \uD558\uC138\uC694`);
789
+ continue;
790
+ }
791
+ if (opts.dryRun) {
792
+ ctx.log(` ~ package ${pkg.id} (${inst.name} update)`);
793
+ continue;
794
+ }
795
+ const now = await inst.update(ctx, pkg, opts);
796
+ const before = lock.packages[pkg.id]?.rev;
797
+ if (now !== before) {
798
+ lock.packages[pkg.id] = { source: pkg.source, rev: now };
799
+ res.lockChanged = true;
800
+ ctx.log(` \u2191 package ${pkg.id} ${before ? short(before) : "(\uC5C6\uC74C)"} \u2192 ${short(now)}`);
801
+ await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
802
+ } else {
803
+ ctx.log(` = package ${pkg.id} ${short(now)} (\uCD5C\uC2E0)`);
804
+ }
805
+ }
806
+ if (res.lockChanged) await writeLock(ctx.shed, lock);
807
+ return res;
808
+ }
809
+
338
810
  // src/core/init.ts
339
811
  async function init(ctx, opts = {}) {
340
812
  const profileName = opts.profile ?? "default";
813
+ const exclude = opts.exclude ?? [];
814
+ const skipped = [];
341
815
  if (await exists(manifestPath(ctx))) {
342
816
  throw new Error(`\uC774\uBBF8 \uCD08\uAE30\uD654\uB41C \uCC3D\uACE0\uC785\uB2C8\uB2E4: ${manifestPath(ctx)}
343
817
  \uB2E4\uB978 \uD658\uACBD\uC758 \uC124\uC815\uC744 \uC774 \uCC3D\uACE0\uB85C \uAC00\uC838\uC624\uB824\uBA74 'lshed restore' \uD6C4 'lshed save' \uB97C \uC4F0\uC138\uC694.`);
344
818
  }
345
- const found = await ctx.adapter.scan();
346
- const m = { version: 1, agent: ctx.adapter.name, components: {}, profiles: { [profileName]: {} } };
819
+ const all = await ctx.adapter.scan();
820
+ const m = { version: 1, agent: ctx.adapter.name, components: {}, packages: [], profiles: { [profileName]: {} } };
821
+ const isExcluded = (cat, id) => exclude.some((e) => e === id || e === `${cat}/${id}`);
822
+ const pkgs = (await detectPackages(ctx, all)).filter((p) => !isExcluded("", p.id));
823
+ const generated = await detectGenerated(all, pkgs);
824
+ const found = all.filter((f) => !pkgs.some((p) => p.path === f.path) && !generated.has(`${f.category}/${f.id}`));
825
+ for (const p of pkgs) {
826
+ m.packages.push(p.into ? { id: p.id, source: p.source, into: p.into } : { id: p.id, source: p.source });
827
+ const rev = /^[0-9a-f]{40}$/.test(p.rev) ? p.rev.slice(0, 7) : p.rev;
828
+ ctx.log(` \u2261 package ${p.id} ${p.source} @${rev} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
829
+ }
830
+ if (pkgs.length) m.profiles[profileName][PACKAGES] = pkgs.map((p) => p.id);
831
+ for (const [key, by] of generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
347
832
  const managed = [];
348
833
  let copied = 0;
349
834
  for (const cat of ctx.adapter.categories()) {
350
- const mine = found.filter((f) => f.category === cat.name);
835
+ const mine = found.filter((f) => f.category === cat.name && !isExcluded(f.category, f.id));
836
+ for (const f of found.filter((f2) => f2.category === cat.name && isExcluded(f2.category, f2.id))) {
837
+ skipped.push(`${f.category}/${f.id}`);
838
+ ctx.log(` - ${f.category}/${f.id} (--exclude)`);
839
+ }
351
840
  if (!mine.length) continue;
352
841
  m.components[cat.name] = [];
353
842
  m.profiles[profileName][cat.name] = [];
354
843
  for (const f of mine) {
355
- const dst = path7.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
356
- await copyTree(f.path, dst);
844
+ const dst = path12.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
845
+ await copyTree(f.path, dst, ignoreOf(ctx));
357
846
  m.components[cat.name].push({ id: f.id });
358
847
  m.profiles[profileName][cat.name].push(f.id);
359
848
  managed.push(targetRel(cat, f.id));
@@ -363,32 +852,48 @@ async function init(ctx, opts = {}) {
363
852
  }
364
853
  const instr = instructionsFile(ctx);
365
854
  if (await exists(instr)) {
366
- const text = await fs5.readFile(instr, "utf8");
855
+ const text = await fs9.readFile(instr, "utf8");
367
856
  if (!isGenerated(text)) {
368
- const dst = path7.join(ctx.shed, INSTRUCTIONS, "main.md");
369
- await fs5.mkdir(path7.dirname(dst), { recursive: true });
370
- await fs5.writeFile(dst, text);
857
+ const dst = path12.join(ctx.shed, INSTRUCTIONS, "main.md");
858
+ await fs9.mkdir(path12.dirname(dst), { recursive: true });
859
+ await fs9.writeFile(dst, text);
371
860
  const fragRel = targetRel(INSTRUCTIONS, "main");
372
- await copyTree(dst, abs(ctx, fragRel));
861
+ await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
373
862
  managed.push(fragRel);
374
863
  m.components[INSTRUCTIONS] = [{ id: "main" }];
375
864
  m.profiles[profileName][INSTRUCTIONS] = ["main"];
376
865
  copied++;
377
- ctx.log(` + ${INSTRUCTIONS}/main (${path7.basename(instr)})`);
866
+ ctx.log(` + ${INSTRUCTIONS}/main (${path12.basename(instr)})`);
378
867
  }
379
868
  }
380
- await fs5.mkdir(ctx.shed, { recursive: true });
381
- await fs5.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
382
- ` + stringifyManifest(m));
869
+ await fs9.mkdir(ctx.shed, { recursive: true });
870
+ let yamlText = stringifyManifest(m);
871
+ for (const p of pkgs) {
872
+ if (!p.into) continue;
873
+ yamlText = yamlText.replace(` into: ${p.into}
874
+ `, ` into: ${p.into}
875
+ # install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
876
+ `);
877
+ }
878
+ await fs9.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
879
+ ` + yamlText);
880
+ if (pkgs.length) {
881
+ await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
882
+ }
383
883
  await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
884
+ const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
885
+ if (pkgs.length) parts.push(`\uD328\uD0A4\uC9C0 ${pkgs.length}\uAC1C`);
886
+ if (generated.size) parts.push(`\uC0DD\uC131\uBB3C ${generated.size}\uAC1C \uAC74\uB108\uB700`);
887
+ if (skipped.length) parts.push(`\uC81C\uC678 ${skipped.length}\uAC1C`);
384
888
  ctx.log(`
385
- ${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (\uBD80\uD488 ${copied}\uAC1C, \uD504\uB85C\uD544 "${profileName}")`);
386
- return { manifest: m, copied };
889
+ ${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
890
+ 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.`);
891
+ return { manifest: m, copied, skipped, packages: pkgs.map((p) => p.id), generated: [...generated.keys()] };
387
892
  }
388
893
 
389
894
  // src/core/restore.ts
390
- import { promises as fs6 } from "fs";
391
- import path8 from "path";
895
+ import { promises as fs10 } from "fs";
896
+ import path13 from "path";
392
897
  async function restore(ctx, profileArg, opts = {}) {
393
898
  const backup = opts.backup ?? true;
394
899
  const state = await readState(ctx.adapter);
@@ -399,6 +904,7 @@ async function restore(ctx, profileArg, opts = {}) {
399
904
  for (const it of plan) {
400
905
  if (!await exists(it.src)) throw new Error(`${it.category}/${it.id}: \uCC3D\uACE0\uC5D0 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${it.src}`);
401
906
  }
907
+ const pkgRes = await ensurePackages(ctx, packagesOf(m, profile), { dryRun: opts.dryRun, yes: opts.yes });
402
908
  const instrRel = ctx.adapter.instructionsFileName();
403
909
  const fragments = plan.filter((p) => p.category === INSTRUCTIONS);
404
910
  const newManaged = new Set(plan.map((p) => p.rel));
@@ -406,7 +912,7 @@ async function restore(ctx, profileArg, opts = {}) {
406
912
  const oldManaged = new Set(state?.managed ?? []);
407
913
  const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
408
914
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
409
- const backupDir = path8.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
915
+ const backupDir = path13.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
410
916
  const backedUp = [];
411
917
  const placed = [];
412
918
  async function backUp(rel) {
@@ -414,7 +920,7 @@ async function restore(ctx, profileArg, opts = {}) {
414
920
  if (!await exists(from)) return;
415
921
  backedUp.push(rel);
416
922
  if (opts.dryRun || !backup) return;
417
- await copyTree(from, path8.join(backupDir, ...rel.split("/")));
923
+ await copyTree(from, path13.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
418
924
  }
419
925
  for (const rel of toRemove) {
420
926
  ctx.log(` - ${rel}`);
@@ -423,7 +929,7 @@ async function restore(ctx, profileArg, opts = {}) {
423
929
  }
424
930
  for (const it of plan) {
425
931
  const target = abs(ctx, it.rel);
426
- const same = await hashTree(target) === await hashTree(it.src);
932
+ const same = await hashTree(target, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx));
427
933
  const mark = same ? "=" : await exists(target) ? "~" : "+";
428
934
  ctx.log(` ${mark} ${it.rel}`);
429
935
  if (same) {
@@ -431,20 +937,20 @@ async function restore(ctx, profileArg, opts = {}) {
431
937
  continue;
432
938
  }
433
939
  if (await exists(target)) await backUp(it.rel);
434
- if (!opts.dryRun) await copyTree(it.src, target);
940
+ if (!opts.dryRun) await copyTree(it.src, target, ignoreOf(ctx));
435
941
  placed.push(it.rel);
436
942
  }
437
943
  const instrPath = instructionsFile(ctx);
438
944
  if (fragments.length) {
439
945
  const contents = [];
440
- for (const f of fragments) contents.push({ id: f.id, content: await fs6.readFile(f.src, "utf8") });
946
+ for (const f of fragments) contents.push({ id: f.id, content: await fs10.readFile(f.src, "utf8") });
441
947
  const rendered = renderInstructions(ctx, profile, contents);
442
- const existing = await exists(instrPath) ? await fs6.readFile(instrPath, "utf8") : null;
948
+ const existing = await exists(instrPath) ? await fs10.readFile(instrPath, "utf8") : null;
443
949
  if (existing !== rendered) {
444
950
  const mark = existing === null ? "+" : "~";
445
951
  ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
446
952
  if (existing !== null) await backUp(instrRel);
447
- if (!opts.dryRun) await fs6.writeFile(instrPath, rendered);
953
+ if (!opts.dryRun) await fs10.writeFile(instrPath, rendered);
448
954
  } else {
449
955
  ctx.log(` = ${instrRel}`);
450
956
  }
@@ -453,12 +959,14 @@ async function restore(ctx, profileArg, opts = {}) {
453
959
  if (opts.dryRun) {
454
960
  ctx.log(`
455
961
  (dry-run) \uBCC0\uACBD \uC5C6\uC74C. \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}, \uBC31\uC5C5 \uC608\uC815 ${backedUp.length}`);
962
+ reportPending(ctx, pkgRes);
456
963
  return { profile, placed, removed: toRemove, backedUp, backupDir: null };
457
964
  }
458
965
  await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
459
966
  const bdir = backup && backedUp.length ? backupDir : null;
460
967
  ctx.log(`
461
- \uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
968
+ \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
+ reportPending(ctx, pkgRes);
462
970
  return { profile, placed, removed: toRemove, backedUp, backupDir: bdir };
463
971
  }
464
972
 
@@ -469,7 +977,7 @@ async function diff(ctx) {
469
977
  const m = await loadManifest(ctx);
470
978
  const out = [];
471
979
  for (const item of planProfile(ctx, m, state.profile)) {
472
- const changes = await diffTrees(abs(ctx, item.rel), item.src);
980
+ const changes = await diffTrees(abs(ctx, item.rel), item.src, ignoreOf(ctx));
473
981
  if (changes.length) out.push({ item, changes });
474
982
  }
475
983
  return out;
@@ -488,9 +996,12 @@ function formatDiff(diffs) {
488
996
  // src/core/status.ts
489
997
  async function status(ctx) {
490
998
  const state = await readState(ctx.adapter);
491
- if (!state) return { state: null, drifted: [] };
999
+ if (!state) return { state: null, drifted: [], packages: [] };
492
1000
  const d = await diff(ctx);
493
- return { state, drifted: d.map((x) => `${x.item.category}/${x.item.id}`) };
1001
+ const m = await loadManifest(ctx);
1002
+ const lock = await readLock(ctx.shed);
1003
+ const packages = await Promise.all(packagesOf(m, state.profile).map((p) => packageStatus(ctx, p, lock)));
1004
+ return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages };
494
1005
  }
495
1006
  function formatStatus(s, adapterRoot) {
496
1007
  if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
@@ -502,6 +1013,11 @@ function formatStatus(s, adapterRoot) {
502
1013
  `\uAD00\uB9AC \uC911 ${s.state.managed.length}\uAC1C \uACBD\uB85C (${adapterRoot})`
503
1014
  ];
504
1015
  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");
1016
+ const short2 = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
1017
+ for (const p of s.packages) {
1018
+ 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
+ lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
1020
+ }
505
1021
  return lines.join("\n");
506
1022
  }
507
1023
 
@@ -533,8 +1049,8 @@ async function save(ctx, ids = []) {
533
1049
  ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
534
1050
  continue;
535
1051
  }
536
- if (await hashTree(local) === await hashTree(it.src)) continue;
537
- await copyTree(local, it.src);
1052
+ if (await hashTree(local, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx))) continue;
1053
+ await copyTree(local, it.src, ignoreOf(ctx));
538
1054
  saved.push(`${it.category}/${it.id}`);
539
1055
  ctx.log(` \u2713 ${it.category}/${it.id} \u2192 \uCC3D\uACE0`);
540
1056
  }
@@ -543,21 +1059,115 @@ ${saved.length}\uAC1C \uBC18\uC601. \uCC3D\uACE0\uB97C \uCEE4\uBC0B/\uB3D9\uAE30
543
1059
  return saved;
544
1060
  }
545
1061
 
1062
+ // src/core/list.ts
1063
+ function listRows(m) {
1064
+ const usedBy = (cat, id) => Object.entries(m.profiles).filter(([, cats]) => (cats[cat] ?? []).includes(id)).map(([name]) => name);
1065
+ const rows = [];
1066
+ for (const [cat, comps] of Object.entries(m.components)) {
1067
+ for (const c of comps) rows.push({ kind: "component", category: cat, id: c.id, usedBy: usedBy(cat, c.id) });
1068
+ }
1069
+ for (const p of m.packages) rows.push({ kind: "package", category: PACKAGES, id: p.id, usedBy: usedBy(PACKAGES, p.id) });
1070
+ return rows;
1071
+ }
1072
+ function formatRows(rows, m) {
1073
+ if (!rows.length) return "(\uBE44\uC5B4 \uC788\uC74C)";
1074
+ const w = Math.max(...rows.map((r) => `${r.category}/${r.id}`.length));
1075
+ const lines = rows.map((r) => {
1076
+ const key = `${r.category}/${r.id}`.padEnd(w);
1077
+ const use = r.usedBy.length ? r.usedBy.join(", ") : "(\uBBF8\uC0AC\uC6A9)";
1078
+ return `${r.kind === "package" ? "\u2261" : " "} ${key} ${use}`;
1079
+ });
1080
+ const unused = rows.filter((r) => !r.usedBy.length).length;
1081
+ lines.push("", `${rows.length}\uAC1C, \uD504\uB85C\uD544 ${Object.keys(m.profiles).length}\uAC1C${unused ? `, \uBBF8\uC0AC\uC6A9 ${unused}\uAC1C \u2192 lshed prune` : ""}`);
1082
+ return lines.join("\n");
1083
+ }
1084
+
1085
+ // src/core/remove.ts
1086
+ import { promises as fs11 } from "fs";
1087
+ import path14 from "path";
1088
+ import YAML3, { isSeq, isMap } from "yaml";
1089
+ function resolveKey(m, raw) {
1090
+ const rows = listRows(m);
1091
+ const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
1092
+ const hits = rows.filter((r) => r.id === b && (a === void 0 || r.category === a));
1093
+ if (!hits.length) throw new Error(`"${raw}" \uB294 \uCC3D\uACE0\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4`);
1094
+ if (hits.length > 1) throw new Error(`"${raw}" \uAC00 \uBAA8\uD638\uD569\uB2C8\uB2E4: ${hits.map((h) => `${h.category}/${h.id}`).join(", ")}`);
1095
+ return { category: hits[0].category, id: hits[0].id };
1096
+ }
1097
+ async function remove(ctx, raw) {
1098
+ const m = await loadManifest(ctx);
1099
+ const { category, id } = resolveKey(m, raw);
1100
+ const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
1101
+ 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 fs11.readFile(manifestPath(ctx), "utf8");
1103
+ const doc = YAML3.parseDocument(text);
1104
+ let deleted;
1105
+ if (category === PACKAGES) {
1106
+ const seq = doc.get(PACKAGES);
1107
+ if (!isSeq(seq)) throw new Error("packages \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4");
1108
+ const idx = seq.items.findIndex((it) => isMap(it) && it.get("id") === id);
1109
+ seq.delete(idx);
1110
+ if (!seq.items.length) doc.delete(PACKAGES);
1111
+ const lock = await readLock(ctx.shed);
1112
+ if (lock.packages[id]) {
1113
+ delete lock.packages[id];
1114
+ await writeLock(ctx.shed, lock);
1115
+ }
1116
+ ctx.log(` - package ${id} (\uB9E4\uB2C8\uD398\uC2A4\uD2B8\xB7\uB77D\uC5D0\uC11C \uC81C\uAC70. \uB85C\uCEEC clone \uC740 \uADF8\uB300\uB85C)`);
1117
+ } else {
1118
+ const seq = doc.getIn(["components", category]);
1119
+ if (!isSeq(seq)) throw new Error(`components.${category} \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
1120
+ const idx = seq.items.findIndex((it) => isMap(it) && it.get("id") === id);
1121
+ seq.delete(idx);
1122
+ if (!seq.items.length) doc.deleteIn(["components", category]);
1123
+ const src = sourcePath(ctx, category, findComponent(m, category, id));
1124
+ const inside = !path14.relative(ctx.shed, src).startsWith("..");
1125
+ if (inside && await exists(src)) {
1126
+ await removeTree(src);
1127
+ deleted = src;
1128
+ }
1129
+ ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
1130
+ }
1131
+ await fs11.writeFile(manifestPath(ctx), doc.toString());
1132
+ return { category, id, deleted };
1133
+ }
1134
+ async function prune(ctx, opts = {}) {
1135
+ const m = await loadManifest(ctx);
1136
+ const unused = listRows(m).filter((r) => !r.usedBy.length);
1137
+ if (!unused.length) {
1138
+ ctx.log("\uBBF8\uC0AC\uC6A9 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
1139
+ return [];
1140
+ }
1141
+ if (!opts.yes) {
1142
+ ctx.log(`\uBBF8\uC0AC\uC6A9 ${unused.length}\uAC1C (\uC9C0\uC6B0\uB824\uBA74 --yes):`);
1143
+ for (const r of unused) ctx.log(` ${r.category}/${r.id}`);
1144
+ return [];
1145
+ }
1146
+ const removed = [];
1147
+ for (const r of unused) {
1148
+ await remove(ctx, `${r.category}/${r.id}`);
1149
+ removed.push(`${r.category}/${r.id}`);
1150
+ }
1151
+ ctx.log(`
1152
+ ${removed.length}\uAC1C \uC81C\uAC70. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC138\uC694: ${ctx.shed}`);
1153
+ return removed;
1154
+ }
1155
+
546
1156
  // src/cli.ts
547
1157
  var { version } = createRequire(import.meta.url)("../package.json");
548
1158
  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)");
549
1159
  function adapterFromOpts() {
550
1160
  const { root } = program.opts();
551
- return new ClaudeCodeAdapter(root ? path9.resolve(root) : void 0);
1161
+ return new ClaudeCodeAdapter(root ? path15.resolve(root) : void 0);
552
1162
  }
553
1163
  async function ctxFor(cmd) {
554
1164
  const adapter = adapterFromOpts();
555
1165
  const { shed: flag } = program.opts();
556
1166
  let shed = flag ?? process.env.LSHED_HOME;
557
1167
  if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
558
- if (!shed && cmd === "init") shed = path9.join(os2.homedir(), "lshed");
1168
+ if (!shed && cmd === "init") shed = path15.join(os2.homedir(), "lshed");
559
1169
  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.");
560
- return { adapter, shed: path9.resolve(shed), log: (l) => console.log(l) };
1170
+ return { adapter, shed: path15.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
561
1171
  }
562
1172
  async function run(fn) {
563
1173
  try {
@@ -567,22 +1177,42 @@ async function run(fn) {
567
1177
  process.exitCode = 1;
568
1178
  }
569
1179
  }
570
- program.command("init").description("scan the current environment into a shed and write lshed.yaml").option("--profile <name>", "name of the initial profile", "default").action((o) => run(async () => {
1180
+ program.command("init").description("scan the current environment into a shed and write lshed.yaml").option("--profile <name>", "name of the initial profile", "default").option("--exclude <id...>", "components to leave out (id or category/id)").action((o) => run(async () => {
571
1181
  const ctx = await ctxFor("init");
572
1182
  console.log(`\uC2A4\uCE94: ${ctx.adapter.root} \u2192 \uCC3D\uACE0: ${ctx.shed}`);
573
- await init(ctx, { profile: o.profile });
1183
+ await init(ctx, { profile: o.profile, exclude: o.exclude });
574
1184
  console.log(`
575
1185
  \uB2E4\uC74C: \uCC3D\uACE0\uB97C git \uC73C\uB85C \uAD00\uB9AC\uD558\uC138\uC694. cd ${ctx.shed} && git init`);
576
1186
  }));
577
- program.command("restore [profile]").description("apply a profile (defaults to the last applied one)").option("--dry-run", "print what would change without touching anything").option("--no-backup", "skip backing up files that get replaced or removed").action((profile, o) => run(async () => {
1187
+ program.command("restore [profile]").description("apply a profile (defaults to the last applied one)").option("--dry-run", "print what would change without touching anything").option("--no-backup", "skip backing up files that get replaced or removed").option("--yes", "run package install commands (they are shown, not run, without this)").action((profile, o) => run(async () => {
578
1188
  const ctx = await ctxFor("other");
579
- await restore(ctx, profile, { dryRun: o.dryRun, backup: o.backup });
1189
+ await restore(ctx, profile, { dryRun: o.dryRun, backup: o.backup, yes: o.yes });
1190
+ }));
1191
+ program.command("update [ids...]").description("pull packages to their latest upstream and refresh lshed.lock").option("--dry-run", "show what would be updated").option("--yes", "run package install commands after updating").action((ids, o) => run(async () => {
1192
+ const ctx = await ctxFor("other");
1193
+ const state = await readState(ctx.adapter);
1194
+ 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.");
1195
+ const m = await loadManifest(ctx);
1196
+ let pkgs = packagesOf(m, state.profile);
1197
+ if (ids.length) {
1198
+ pkgs = ids.map((id) => {
1199
+ const p = m.packages.find((x2) => x2.id === id);
1200
+ if (!p) throw new Error(`\uD328\uD0A4\uC9C0 "${id}" \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`);
1201
+ return p;
1202
+ });
1203
+ }
1204
+ if (!pkgs.length) {
1205
+ console.log("\uAC31\uC2E0\uD560 \uD328\uD0A4\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
1206
+ return;
1207
+ }
1208
+ const res = await updatePackages(ctx, pkgs, { dryRun: o.dryRun, yes: o.yes });
1209
+ reportPending(ctx, res);
580
1210
  }));
581
1211
  program.command("status").description("show the applied profile, managed paths and drift").action(() => run(async () => {
582
1212
  const adapter = adapterFromOpts();
583
1213
  const state = await readState(adapter);
584
1214
  if (!state) {
585
- console.log(formatStatus({ state: null, drifted: [] }, adapter.root));
1215
+ console.log(formatStatus({ state: null, drifted: [], packages: [] }, adapter.root));
586
1216
  return;
587
1217
  }
588
1218
  const ctx = await ctxFor("other");
@@ -596,6 +1226,20 @@ program.command("save [ids...]").description("copy local edits back into the she
596
1226
  const ctx = await ctxFor("other");
597
1227
  await save(ctx, ids);
598
1228
  }));
1229
+ program.command("list").description("everything in the shed and which profiles use it").option("--unused", "only things no profile uses").action((o) => run(async () => {
1230
+ const ctx = await ctxFor("other");
1231
+ const m = await loadManifest(ctx);
1232
+ const rows = listRows(m).filter((r) => !o.unused || !r.usedBy.length);
1233
+ console.log(o.unused && !rows.length ? "\uBBF8\uC0AC\uC6A9 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." : formatRows(rows, m));
1234
+ }));
1235
+ program.command("remove <key>").description("delete a component or package from the shed (refused while a profile uses it)").action((key) => run(async () => {
1236
+ const ctx = await ctxFor("other");
1237
+ await remove(ctx, key);
1238
+ }));
1239
+ program.command("prune").description("remove everything no profile uses").option("--yes", "actually delete; without it, just list").action((o) => run(async () => {
1240
+ const ctx = await ctxFor("other");
1241
+ await prune(ctx, { yes: o.yes });
1242
+ }));
599
1243
  program.command("scan").description("(debug) list components found in the agent config root").action(() => run(async () => {
600
1244
  const adapter = adapterFromOpts();
601
1245
  const found = await adapter.scan();