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/CHANGELOG.md +39 -0
- package/README.md +65 -6
- package/dist/cli.js +806 -162
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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 ??
|
|
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 =
|
|
197
|
+
const dir = path2.join(this.root, cat.root);
|
|
38
198
|
let entries;
|
|
39
199
|
try {
|
|
40
|
-
entries = await
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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/
|
|
66
|
-
import {
|
|
67
|
-
import
|
|
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/
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
523
|
+
import path6 from "path";
|
|
155
524
|
function resolveSource(shed, raw) {
|
|
156
525
|
const s = parseSource(raw);
|
|
157
|
-
if (s.scheme === "file") return
|
|
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
|
|
166
|
-
import
|
|
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
|
|
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
|
|
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
|
|
191
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
|
638
|
+
return path8.join(ctx.adapter.root, ...rel.split("/"));
|
|
247
639
|
}
|
|
248
640
|
|
|
249
|
-
// src/
|
|
250
|
-
import { promises as
|
|
251
|
-
import
|
|
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
|
|
646
|
+
import path9 from "path";
|
|
318
647
|
var MARKER = "<!-- generated by lshed";
|
|
319
648
|
function instructionsFile(ctx) {
|
|
320
|
-
return
|
|
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
|
|
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
|
|
660
|
+
return head2 + fragments.map((f) => `@${FRAGMENTS_DIR}/${f.id}.md`).join("\n") + "\n";
|
|
332
661
|
}
|
|
333
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
855
|
+
const text = await fs9.readFile(instr, "utf8");
|
|
367
856
|
if (!isGenerated(text)) {
|
|
368
|
-
const dst =
|
|
369
|
-
await
|
|
370
|
-
await
|
|
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 (${
|
|
866
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path12.basename(instr)})`);
|
|
378
867
|
}
|
|
379
868
|
}
|
|
380
|
-
await
|
|
381
|
-
|
|
382
|
-
|
|
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)} (
|
|
386
|
-
|
|
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
|
|
391
|
-
import
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
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:
|
|
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();
|