lshed 0.0.0 → 0.2.1
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 +40 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/cli.js +1063 -0
- package/package.json +53 -7
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import os2 from "os";
|
|
7
|
+
import path13 from "path";
|
|
8
|
+
|
|
9
|
+
// src/adapters/claude-code.ts
|
|
10
|
+
import { promises as fs } from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
var CATEGORIES = [
|
|
14
|
+
{ name: "skills", root: "skills", kind: "dir" },
|
|
15
|
+
{ name: "agents", root: "agents", kind: "file" },
|
|
16
|
+
{ name: "commands", root: "commands", kind: "file" }
|
|
17
|
+
// instructions 는 단일 파일(CLAUDE.md)이라 스캔 대상이 아니라 생성 대상이다 (§3.3)
|
|
18
|
+
];
|
|
19
|
+
var ClaudeCodeAdapter = class {
|
|
20
|
+
name = "claude-code";
|
|
21
|
+
root;
|
|
22
|
+
constructor(root) {
|
|
23
|
+
this.root = root ?? path.join(os.homedir(), ".claude");
|
|
24
|
+
}
|
|
25
|
+
categories() {
|
|
26
|
+
return CATEGORIES;
|
|
27
|
+
}
|
|
28
|
+
instructionsStrategy() {
|
|
29
|
+
return "import";
|
|
30
|
+
}
|
|
31
|
+
instructionsFileName() {
|
|
32
|
+
return "CLAUDE.md";
|
|
33
|
+
}
|
|
34
|
+
async scan() {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const cat of CATEGORIES) {
|
|
37
|
+
const dir = path.join(this.root, cat.root);
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
if (e.name.startsWith(".")) continue;
|
|
46
|
+
const full = path.join(dir, e.name);
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
st = await fs.stat(full);
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (cat.kind === "dir" && st.isDirectory()) {
|
|
54
|
+
out.push({ category: cat.name, id: e.name, path: full });
|
|
55
|
+
} else if (cat.kind === "file" && st.isFile() && e.name.endsWith(".md")) {
|
|
56
|
+
out.push({ category: cat.name, id: e.name.slice(0, -3), path: full });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/core/init.ts
|
|
65
|
+
import { promises as fs7 } from "fs";
|
|
66
|
+
import path10 from "path";
|
|
67
|
+
|
|
68
|
+
// src/core/context.ts
|
|
69
|
+
import { promises as fs3 } from "fs";
|
|
70
|
+
import path4 from "path";
|
|
71
|
+
|
|
72
|
+
// src/manifest.ts
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
import YAML from "yaml";
|
|
75
|
+
|
|
76
|
+
// src/source.ts
|
|
77
|
+
var GITHUB_RE = /^([\w.-]+)\/([\w.-]+)(?:@([^#]+))?(?:#(.+))?$/;
|
|
78
|
+
function parseSource(raw) {
|
|
79
|
+
const idx = raw.indexOf(":");
|
|
80
|
+
if (idx <= 0) {
|
|
81
|
+
throw new Error(`source\uC5D0 \uC2A4\uD0B4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}" (\uC608: file:./skills/x, github:user/repo@v1)`);
|
|
82
|
+
}
|
|
83
|
+
const scheme = raw.slice(0, idx);
|
|
84
|
+
const rest = raw.slice(idx + 1);
|
|
85
|
+
switch (scheme) {
|
|
86
|
+
case "file":
|
|
87
|
+
if (!rest) throw new Error(`file: \uB4A4\uC5D0 \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
|
|
88
|
+
return { scheme, path: rest };
|
|
89
|
+
case "github": {
|
|
90
|
+
const m = GITHUB_RE.exec(rest);
|
|
91
|
+
if (!m) throw new Error(`github: \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: "${raw}" (\uC608: github:user/repo@v1.0#sub/path)`);
|
|
92
|
+
const [, owner, repo, ref, subpath] = m;
|
|
93
|
+
return { scheme, owner, repo, ref, subpath };
|
|
94
|
+
}
|
|
95
|
+
case "git": {
|
|
96
|
+
const hash = rest.lastIndexOf("#");
|
|
97
|
+
const url = hash >= 0 ? rest.slice(0, hash) : rest;
|
|
98
|
+
const ref = hash >= 0 ? rest.slice(hash + 1) : void 0;
|
|
99
|
+
if (!url) throw new Error(`git: \uB4A4\uC5D0 URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
|
|
100
|
+
return { scheme, url, ref: ref || void 0 };
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function formatSource(s) {
|
|
107
|
+
switch (s.scheme) {
|
|
108
|
+
case "file":
|
|
109
|
+
return `file:${s.path}`;
|
|
110
|
+
case "github":
|
|
111
|
+
return `github:${s.owner}/${s.repo}${s.ref ? `@${s.ref}` : ""}${s.subpath ? `#${s.subpath}` : ""}`;
|
|
112
|
+
case "git":
|
|
113
|
+
return `git:${s.url}${s.ref ? `#${s.ref}` : ""}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function cloneTarget(s) {
|
|
117
|
+
if (s.scheme === "github") return { url: `https://github.com/${s.owner}/${s.repo}.git`, ref: s.ref };
|
|
118
|
+
if (s.scheme === "git") return { url: s.url, ref: s.ref };
|
|
119
|
+
throw new Error(`file: \uCD9C\uCC98\uB294 clone \uB300\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
120
|
+
}
|
|
121
|
+
function sourceFromRemote(url, ref) {
|
|
122
|
+
const m = /^(?:https?:\/\/github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
|
|
123
|
+
if (m) return formatSource({ scheme: "github", owner: m[1], repo: m[2], ref });
|
|
124
|
+
return formatSource({ scheme: "git", url, ref });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/manifest.ts
|
|
128
|
+
var ComponentSchema = z.object({
|
|
129
|
+
id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
|
|
130
|
+
source: z.string().optional(),
|
|
131
|
+
tags: z.array(z.string()).optional()
|
|
132
|
+
});
|
|
133
|
+
var PackageSchema = z.object({
|
|
134
|
+
id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
|
|
135
|
+
source: z.string(),
|
|
136
|
+
into: z.string().regex(/^[^/\\][^\\]*$/, "into \uB294 \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C (POSIX)"),
|
|
137
|
+
install: z.string().optional()
|
|
138
|
+
});
|
|
139
|
+
var PACKAGES = "packages";
|
|
140
|
+
var ComponentsSchema = z.record(z.string(), z.array(ComponentSchema));
|
|
141
|
+
var ProfileSchema = z.record(z.string(), z.array(z.string()));
|
|
142
|
+
var ManifestSchema = z.object({
|
|
143
|
+
version: z.literal(1),
|
|
144
|
+
agent: z.string().default("claude-code"),
|
|
145
|
+
/** 창고에 담지 않을 이름들. 기본값(DEFAULT_IGNORE)에 더해진다. */
|
|
146
|
+
ignore: z.array(z.string()).optional(),
|
|
147
|
+
components: ComponentsSchema.default({}),
|
|
148
|
+
packages: z.array(PackageSchema).default([]),
|
|
149
|
+
profiles: z.record(z.string(), ProfileSchema).default({})
|
|
150
|
+
});
|
|
151
|
+
var ManifestError = class extends Error {
|
|
152
|
+
};
|
|
153
|
+
function parseManifest(text, knownCategories2) {
|
|
154
|
+
const raw = YAML.parse(text);
|
|
155
|
+
const result = ManifestSchema.safeParse(raw);
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
const lines = result.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
158
|
+
throw new ManifestError(`lshed.yaml \uD615\uC2DD \uC624\uB958:
|
|
159
|
+
${lines.join("\n")}`);
|
|
160
|
+
}
|
|
161
|
+
const m = result.data;
|
|
162
|
+
const problems = [];
|
|
163
|
+
for (const [cat, comps] of Object.entries(m.components)) {
|
|
164
|
+
if (knownCategories2 && !knownCategories2.includes(cat)) {
|
|
165
|
+
problems.push(`\uC54C \uC218 \uC5C6\uB294 \uCE74\uD14C\uACE0\uB9AC "${cat}" (\uC5B4\uB311\uD130 ${m.agent}: ${knownCategories2.join(", ")})`);
|
|
166
|
+
}
|
|
167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const c of comps) {
|
|
169
|
+
if (seen.has(c.id)) problems.push(`${cat}: id "${c.id}" \uC911\uBCF5`);
|
|
170
|
+
seen.add(c.id);
|
|
171
|
+
try {
|
|
172
|
+
parseSource(effectiveSource(cat, c));
|
|
173
|
+
} catch (e) {
|
|
174
|
+
problems.push(`${cat}/${c.id}: ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const pkgIds = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const p of m.packages) {
|
|
180
|
+
if (pkgIds.has(p.id)) problems.push(`packages: id "${p.id}" \uC911\uBCF5`);
|
|
181
|
+
pkgIds.add(p.id);
|
|
182
|
+
try {
|
|
183
|
+
if (parseSource(p.source).scheme === "file") problems.push(`packages/${p.id}: \uD328\uD0A4\uC9C0 \uCD9C\uCC98\uB294 github: \uB610\uB294 git: \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
problems.push(`packages/${p.id}: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const [profile, cats] of Object.entries(m.profiles)) {
|
|
189
|
+
for (const [cat, ids] of Object.entries(cats)) {
|
|
190
|
+
if (cat === PACKAGES) {
|
|
191
|
+
for (const id of ids) if (!pkgIds.has(id)) problems.push(`profiles.${profile}.packages: "${id}" \uB294 packages \uC5D0 \uC5C6\uC74C`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const available = new Set((m.components[cat] ?? []).map((c) => c.id));
|
|
195
|
+
for (const id of ids) {
|
|
196
|
+
if (!available.has(id)) problems.push(`profiles.${profile}.${cat}: "${id}" \uB294 components\uC5D0 \uC5C6\uC74C`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (problems.length) throw new ManifestError(`lshed.yaml \uCC38\uC870 \uC624\uB958:
|
|
201
|
+
${problems.map((p) => " " + p).join("\n")}`);
|
|
202
|
+
return m;
|
|
203
|
+
}
|
|
204
|
+
function effectiveSource(category, c, kind = "dir") {
|
|
205
|
+
return c.source ?? `file:./${category}/${c.id}${kind === "file" ? ".md" : ""}`;
|
|
206
|
+
}
|
|
207
|
+
function stringifyManifest(m) {
|
|
208
|
+
const out = { ...m };
|
|
209
|
+
if (!m.packages.length) delete out.packages;
|
|
210
|
+
if (!m.ignore?.length) delete out.ignore;
|
|
211
|
+
return YAML.stringify(out, { lineWidth: 0 });
|
|
212
|
+
}
|
|
213
|
+
function packagesOf(m, profile) {
|
|
214
|
+
const ids = m.profiles[profile]?.[PACKAGES] ?? [];
|
|
215
|
+
return ids.map((id) => m.packages.find((p) => p.id === id));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/resolvers/file.ts
|
|
219
|
+
import path2 from "path";
|
|
220
|
+
function resolveSource(shed, raw) {
|
|
221
|
+
const s = parseSource(raw);
|
|
222
|
+
if (s.scheme === "file") return path2.resolve(shed, s.path);
|
|
223
|
+
throw new Error(`"${raw}": ${s.scheme}: \uCD9C\uCC98\uB294 v0.2\uC5D0\uC11C \uC9C0\uC6D0\uB429\uB2C8\uB2E4. \uC9C0\uAE08\uC740 file: \uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
224
|
+
}
|
|
225
|
+
function isSaveable(raw) {
|
|
226
|
+
return parseSource(raw).scheme === "file";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/state.ts
|
|
230
|
+
import { promises as fs2 } from "fs";
|
|
231
|
+
import path3 from "path";
|
|
232
|
+
import { z as z2 } from "zod";
|
|
233
|
+
var StateSchema = z2.object({
|
|
234
|
+
profile: z2.string(),
|
|
235
|
+
shed: z2.string(),
|
|
236
|
+
/** 어댑터 루트 기준 상대 경로 (POSIX 구분자). lshed가 놓은 것만. */
|
|
237
|
+
managed: z2.array(z2.string()),
|
|
238
|
+
appliedAt: z2.string()
|
|
239
|
+
});
|
|
240
|
+
var LSHED_DIR = "lshed";
|
|
241
|
+
function statePath(adapter) {
|
|
242
|
+
return path3.join(adapter.root, LSHED_DIR, "state.json");
|
|
243
|
+
}
|
|
244
|
+
async function readState(adapter) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = JSON.parse(await fs2.readFile(statePath(adapter), "utf8"));
|
|
247
|
+
return StateSchema.parse(raw);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (e.code === "ENOENT") return null;
|
|
250
|
+
throw new Error(`state.json \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (${statePath(adapter)}): ${e.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function writeState(adapter, state) {
|
|
254
|
+
const p = statePath(adapter);
|
|
255
|
+
await fs2.mkdir(path3.dirname(p), { recursive: true });
|
|
256
|
+
await fs2.writeFile(p, JSON.stringify(state, null, 2) + "\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/ignore.ts
|
|
260
|
+
var DEFAULT_IGNORE = [
|
|
261
|
+
"node_modules",
|
|
262
|
+
".git",
|
|
263
|
+
"__pycache__",
|
|
264
|
+
".venv",
|
|
265
|
+
".mypy_cache",
|
|
266
|
+
".pytest_cache",
|
|
267
|
+
".DS_Store",
|
|
268
|
+
"*.log"
|
|
269
|
+
];
|
|
270
|
+
function matches(name, pattern) {
|
|
271
|
+
if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
|
|
272
|
+
return name === pattern;
|
|
273
|
+
}
|
|
274
|
+
function isIgnored(rel, patterns) {
|
|
275
|
+
if (!rel) return false;
|
|
276
|
+
return rel.split("/").some((seg) => patterns.some((p) => matches(seg, p)));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/core/context.ts
|
|
280
|
+
var MANIFEST_FILE = "lshed.yaml";
|
|
281
|
+
var INSTRUCTIONS = "instructions";
|
|
282
|
+
var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
|
|
283
|
+
function manifestPath(ctx) {
|
|
284
|
+
return path4.join(ctx.shed, MANIFEST_FILE);
|
|
285
|
+
}
|
|
286
|
+
function knownCategories(adapter) {
|
|
287
|
+
return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
|
|
288
|
+
}
|
|
289
|
+
async function loadManifest(ctx) {
|
|
290
|
+
let text;
|
|
291
|
+
try {
|
|
292
|
+
text = await fs3.readFile(manifestPath(ctx), "utf8");
|
|
293
|
+
} catch {
|
|
294
|
+
throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
|
|
295
|
+
\uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
|
|
296
|
+
}
|
|
297
|
+
const m = parseManifest(text, knownCategories(ctx.adapter));
|
|
298
|
+
ctx.ignore = [...DEFAULT_IGNORE, ...m.ignore ?? []];
|
|
299
|
+
return m;
|
|
300
|
+
}
|
|
301
|
+
function ignoreOf(ctx) {
|
|
302
|
+
return ctx.ignore ?? DEFAULT_IGNORE;
|
|
303
|
+
}
|
|
304
|
+
function targetRel(cat, id) {
|
|
305
|
+
if (cat === INSTRUCTIONS) return `${FRAGMENTS_DIR}/${id}.md`;
|
|
306
|
+
return cat.kind === "dir" ? `${cat.root}/${id}` : `${cat.root}/${id}.md`;
|
|
307
|
+
}
|
|
308
|
+
function sourcePath(ctx, category, c) {
|
|
309
|
+
const cat = ctx.adapter.categories().find((k) => k.name === category);
|
|
310
|
+
const kind = category === INSTRUCTIONS ? "file" : cat?.kind ?? "dir";
|
|
311
|
+
return resolveSource(ctx.shed, effectiveSource(category, c, kind));
|
|
312
|
+
}
|
|
313
|
+
function findComponent(m, category, id) {
|
|
314
|
+
const c = (m.components[category] ?? []).find((x2) => x2.id === id);
|
|
315
|
+
if (!c) throw new Error(`${category}/${id} \uB294 components \uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
316
|
+
return c;
|
|
317
|
+
}
|
|
318
|
+
function planProfile(ctx, m, profile) {
|
|
319
|
+
const p = m.profiles[profile];
|
|
320
|
+
if (!p) {
|
|
321
|
+
const names = Object.keys(m.profiles);
|
|
322
|
+
throw new Error(`\uD504\uB85C\uD544 "${profile}" \uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC788\uB294 \uD504\uB85C\uD544: ${names.length ? names.join(", ") : "(\uC5C6\uC74C)"}`);
|
|
323
|
+
}
|
|
324
|
+
const items = [];
|
|
325
|
+
for (const [category, ids] of Object.entries(p)) {
|
|
326
|
+
if (category === PACKAGES) continue;
|
|
327
|
+
const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category);
|
|
328
|
+
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`);
|
|
329
|
+
for (const id of ids) {
|
|
330
|
+
const component = findComponent(m, category, id);
|
|
331
|
+
items.push({ category, id, rel: targetRel(cat, id), src: sourcePath(ctx, category, component), component });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return items;
|
|
335
|
+
}
|
|
336
|
+
function abs(ctx, rel) {
|
|
337
|
+
return path4.join(ctx.adapter.root, ...rel.split("/"));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/fsutil.ts
|
|
341
|
+
import { promises as fs4 } from "fs";
|
|
342
|
+
import { createHash } from "crypto";
|
|
343
|
+
import path5 from "path";
|
|
344
|
+
async function exists(p) {
|
|
345
|
+
try {
|
|
346
|
+
await fs4.access(p);
|
|
347
|
+
return true;
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function isDir(p) {
|
|
353
|
+
try {
|
|
354
|
+
return (await fs4.stat(p)).isDirectory();
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
360
|
+
if (!await exists(root)) return [];
|
|
361
|
+
if (!await isDir(root)) return [""];
|
|
362
|
+
const out = [];
|
|
363
|
+
async function walk(dir, rel) {
|
|
364
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
365
|
+
for (const e of entries) {
|
|
366
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
367
|
+
if (isIgnored(r, ignore)) continue;
|
|
368
|
+
const full = path5.join(dir, e.name);
|
|
369
|
+
let st;
|
|
370
|
+
try {
|
|
371
|
+
st = await fs4.stat(full);
|
|
372
|
+
} catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (st.isDirectory()) await walk(full, r);
|
|
376
|
+
else if (st.isFile()) out.push(r);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
await walk(root, "");
|
|
380
|
+
return out.sort();
|
|
381
|
+
}
|
|
382
|
+
async function hashFile(p) {
|
|
383
|
+
return createHash("sha256").update(await fs4.readFile(p)).digest("hex");
|
|
384
|
+
}
|
|
385
|
+
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
386
|
+
if (!await exists(root)) return null;
|
|
387
|
+
const h = createHash("sha256");
|
|
388
|
+
for (const rel of await listFiles(root, ignore)) {
|
|
389
|
+
h.update(rel).update("\0").update(await fs4.readFile(rel ? path5.join(root, rel) : root)).update("\0");
|
|
390
|
+
}
|
|
391
|
+
return h.digest("hex");
|
|
392
|
+
}
|
|
393
|
+
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
394
|
+
await fs4.rm(dst, { recursive: true, force: true });
|
|
395
|
+
await fs4.mkdir(path5.dirname(dst), { recursive: true });
|
|
396
|
+
const srcRoot = path5.resolve(src);
|
|
397
|
+
await fs4.cp(src, dst, {
|
|
398
|
+
recursive: true,
|
|
399
|
+
dereference: true,
|
|
400
|
+
filter: async (from) => {
|
|
401
|
+
const rel = path5.relative(srcRoot, path5.resolve(from)).split(path5.sep).join("/");
|
|
402
|
+
if (isIgnored(rel, ignore)) return false;
|
|
403
|
+
try {
|
|
404
|
+
if ((await fs4.lstat(from)).isSymbolicLink()) await fs4.stat(from);
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function removeTree(p) {
|
|
413
|
+
await fs4.rm(p, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
416
|
+
const l = new Set(await listFiles(local, ignore));
|
|
417
|
+
const s = new Set(await listFiles(shed, ignore));
|
|
418
|
+
const out = [];
|
|
419
|
+
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
420
|
+
const lp = f ? path5.join(local, f) : local;
|
|
421
|
+
const sp = f ? path5.join(shed, f) : shed;
|
|
422
|
+
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
423
|
+
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
424
|
+
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/core/instructions.ts
|
|
430
|
+
import path6 from "path";
|
|
431
|
+
var MARKER = "<!-- generated by lshed";
|
|
432
|
+
function instructionsFile(ctx) {
|
|
433
|
+
return path6.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
|
|
434
|
+
}
|
|
435
|
+
function isGenerated(text) {
|
|
436
|
+
return text.trimStart().startsWith(MARKER);
|
|
437
|
+
}
|
|
438
|
+
function renderInstructions(ctx, profile, fragments) {
|
|
439
|
+
const head2 = `${MARKER}; profile: ${profile} -->
|
|
440
|
+
<!-- Do not edit this file. Edit the fragments in your shed and run 'lshed restore'. -->
|
|
441
|
+
|
|
442
|
+
`;
|
|
443
|
+
if (ctx.adapter.instructionsStrategy() === "import") {
|
|
444
|
+
return head2 + fragments.map((f) => `@${FRAGMENTS_DIR}/${f.id}.md`).join("\n") + "\n";
|
|
445
|
+
}
|
|
446
|
+
return head2 + fragments.map((f) => `<!-- ${f.id} -->
|
|
447
|
+
${f.content.trimEnd()}
|
|
448
|
+
`).join("\n");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/core/packages.ts
|
|
452
|
+
import { promises as fs6 } from "fs";
|
|
453
|
+
import path9 from "path";
|
|
454
|
+
|
|
455
|
+
// src/git.ts
|
|
456
|
+
import { execFile, spawn } from "child_process";
|
|
457
|
+
import { promisify } from "util";
|
|
458
|
+
import path7 from "path";
|
|
459
|
+
var x = promisify(execFile);
|
|
460
|
+
async function git(args, cwd) {
|
|
461
|
+
const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
|
|
462
|
+
return stdout.trim();
|
|
463
|
+
}
|
|
464
|
+
var isRepo = (dir) => exists(path7.join(dir, ".git"));
|
|
465
|
+
var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
|
|
466
|
+
var head = (dir) => git(["rev-parse", "HEAD"], dir);
|
|
467
|
+
var branch = async (dir) => {
|
|
468
|
+
const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
|
|
469
|
+
return b === "HEAD" ? void 0 : b;
|
|
470
|
+
};
|
|
471
|
+
var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
|
|
472
|
+
var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
|
|
473
|
+
var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
|
|
474
|
+
function runShell(cmd, cwd) {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
|
|
477
|
+
p.on("error", reject);
|
|
478
|
+
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/lock.ts
|
|
483
|
+
import { promises as fs5 } from "fs";
|
|
484
|
+
import path8 from "path";
|
|
485
|
+
import YAML2 from "yaml";
|
|
486
|
+
import { z as z3 } from "zod";
|
|
487
|
+
var LockSchema = z3.object({
|
|
488
|
+
version: z3.literal(1),
|
|
489
|
+
packages: z3.record(z3.string(), z3.object({ source: z3.string(), commit: z3.string() })).default({})
|
|
490
|
+
});
|
|
491
|
+
var LOCK_FILE = "lshed.lock";
|
|
492
|
+
async function readLock(shed) {
|
|
493
|
+
try {
|
|
494
|
+
return LockSchema.parse(YAML2.parse(await fs5.readFile(path8.join(shed, LOCK_FILE), "utf8")));
|
|
495
|
+
} catch (e) {
|
|
496
|
+
if (e.code === "ENOENT") return { version: 1, packages: {} };
|
|
497
|
+
throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function writeLock(shed, lock) {
|
|
501
|
+
const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
|
|
502
|
+
await fs5.writeFile(path8.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/core/packages.ts
|
|
506
|
+
async function detectPackages(ctx, found) {
|
|
507
|
+
const out = [];
|
|
508
|
+
for (const f of found) {
|
|
509
|
+
if (!await isRepo(f.path)) continue;
|
|
510
|
+
const url = await remoteUrl(f.path);
|
|
511
|
+
if (!url) continue;
|
|
512
|
+
const into = path9.relative(ctx.adapter.root, f.path).split(path9.sep).join("/");
|
|
513
|
+
out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), commit: await head(f.path), path: f.path });
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
async function detectGenerated(found, pkgs) {
|
|
518
|
+
const out = /* @__PURE__ */ new Map();
|
|
519
|
+
if (!pkgs.length) return out;
|
|
520
|
+
const roots = await Promise.all(pkgs.map(async (p) => ({ id: p.id, real: await fs6.realpath(p.path) })));
|
|
521
|
+
for (const f of found) {
|
|
522
|
+
if (pkgs.some((p) => p.path === f.path)) continue;
|
|
523
|
+
let entries;
|
|
524
|
+
try {
|
|
525
|
+
if (!(await fs6.stat(f.path)).isDirectory()) continue;
|
|
526
|
+
entries = await fs6.readdir(f.path);
|
|
527
|
+
} catch {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
for (const name of entries) {
|
|
531
|
+
const p = path9.join(f.path, name);
|
|
532
|
+
let target;
|
|
533
|
+
try {
|
|
534
|
+
if (!(await fs6.lstat(p)).isSymbolicLink()) continue;
|
|
535
|
+
target = await fs6.realpath(p).catch(async () => path9.resolve(f.path, await fs6.readlink(p)));
|
|
536
|
+
} catch {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path9.sep));
|
|
540
|
+
if (owner) {
|
|
541
|
+
out.set(`${f.category}/${f.id}`, owner.id);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
async function packageStatus(ctx, pkg, lock) {
|
|
549
|
+
const dir = abs(ctx, pkg.into);
|
|
550
|
+
const present = await isRepo(dir);
|
|
551
|
+
return { pkg, dir, present, head: present ? await head(dir) : void 0, locked: lock.packages[pkg.id]?.commit };
|
|
552
|
+
}
|
|
553
|
+
async function ensurePackages(ctx, pkgs, opts = {}) {
|
|
554
|
+
const lock = await readLock(ctx.shed);
|
|
555
|
+
const res = { cloned: [], pendingInstalls: [], lockChanged: false };
|
|
556
|
+
for (const pkg of pkgs) {
|
|
557
|
+
const st = await packageStatus(ctx, pkg, lock);
|
|
558
|
+
if (st.present) {
|
|
559
|
+
const note = st.locked && st.head !== st.locked ? ` (HEAD ${st.head.slice(0, 7)} \u2260 lock ${st.locked.slice(0, 7)})` : "";
|
|
560
|
+
ctx.log(` = package ${pkg.id}${note}`);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (await exists(st.dir)) throw new Error(`package ${pkg.id}: ${st.dir} \uAC00 \uC788\uC9C0\uB9CC git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uCE58\uC6B0\uAC70\uB098 into \uB97C \uBC14\uAFB8\uC138\uC694.`);
|
|
564
|
+
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
565
|
+
ctx.log(` + package ${pkg.id} (clone ${url}${ref ? ` @${ref}` : ""}${st.locked ? ` \u2192 ${st.locked.slice(0, 7)}` : ""})`);
|
|
566
|
+
if (opts.dryRun) continue;
|
|
567
|
+
await fs6.mkdir(path9.dirname(st.dir), { recursive: true });
|
|
568
|
+
await clone(url, st.dir, ref);
|
|
569
|
+
if (st.locked) {
|
|
570
|
+
await resetHard(st.dir, st.locked).catch(() => {
|
|
571
|
+
throw new Error(`package ${pkg.id}: \uB77D\uC758 \uCEE4\uBC0B ${st.locked.slice(0, 7)} \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 'lshed update ${pkg.id}' \uB85C \uB77D\uC744 \uAC31\uC2E0\uD558\uC138\uC694.`);
|
|
572
|
+
});
|
|
573
|
+
} else {
|
|
574
|
+
lock.packages[pkg.id] = { source: pkg.source, commit: await head(st.dir) };
|
|
575
|
+
res.lockChanged = true;
|
|
576
|
+
}
|
|
577
|
+
res.cloned.push(pkg.id);
|
|
578
|
+
if (pkg.install) await maybeInstall(ctx, pkg, st.dir, opts, res);
|
|
579
|
+
}
|
|
580
|
+
if (res.lockChanged) await writeLock(ctx.shed, lock);
|
|
581
|
+
return res;
|
|
582
|
+
}
|
|
583
|
+
async function maybeInstall(ctx, pkg, dir, opts, res) {
|
|
584
|
+
if (!pkg.install) return;
|
|
585
|
+
if (opts.yes) {
|
|
586
|
+
ctx.log(` $ (${pkg.into}) ${pkg.install}`);
|
|
587
|
+
await runShell(pkg.install, dir);
|
|
588
|
+
} else {
|
|
589
|
+
res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function reportPending(ctx, res) {
|
|
593
|
+
if (!res.pendingInstalls.length) return;
|
|
594
|
+
ctx.log(`
|
|
595
|
+
\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:`);
|
|
596
|
+
for (const p of res.pendingInstalls) ctx.log(` cd ${p.dir} && ${p.cmd}`);
|
|
597
|
+
}
|
|
598
|
+
async function updatePackages(ctx, pkgs, opts = {}) {
|
|
599
|
+
const lock = await readLock(ctx.shed);
|
|
600
|
+
const res = { cloned: [], pendingInstalls: [], lockChanged: false };
|
|
601
|
+
for (const pkg of pkgs) {
|
|
602
|
+
const st = await packageStatus(ctx, pkg, lock);
|
|
603
|
+
if (!st.present) {
|
|
604
|
+
ctx.log(` ! package ${pkg.id}: \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC74C. \uBA3C\uC800 restore \uD558\uC138\uC694`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (opts.dryRun) {
|
|
608
|
+
ctx.log(` ~ package ${pkg.id} (git pull --ff-only)`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
await pullFf(st.dir);
|
|
612
|
+
const now = await head(st.dir);
|
|
613
|
+
const before = lock.packages[pkg.id]?.commit;
|
|
614
|
+
if (now !== before) {
|
|
615
|
+
lock.packages[pkg.id] = { source: pkg.source, commit: now };
|
|
616
|
+
res.lockChanged = true;
|
|
617
|
+
ctx.log(` \u2191 package ${pkg.id} ${before ? before.slice(0, 7) : "(\uC5C6\uC74C)"} \u2192 ${now.slice(0, 7)}`);
|
|
618
|
+
await maybeInstall(ctx, pkg, st.dir, opts, res);
|
|
619
|
+
} else {
|
|
620
|
+
ctx.log(` = package ${pkg.id} ${now.slice(0, 7)} (\uCD5C\uC2E0)`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (res.lockChanged) await writeLock(ctx.shed, lock);
|
|
624
|
+
return res;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/core/init.ts
|
|
628
|
+
async function init(ctx, opts = {}) {
|
|
629
|
+
const profileName = opts.profile ?? "default";
|
|
630
|
+
const exclude = opts.exclude ?? [];
|
|
631
|
+
const skipped = [];
|
|
632
|
+
if (await exists(manifestPath(ctx))) {
|
|
633
|
+
throw new Error(`\uC774\uBBF8 \uCD08\uAE30\uD654\uB41C \uCC3D\uACE0\uC785\uB2C8\uB2E4: ${manifestPath(ctx)}
|
|
634
|
+
\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.`);
|
|
635
|
+
}
|
|
636
|
+
const all = await ctx.adapter.scan();
|
|
637
|
+
const m = { version: 1, agent: ctx.adapter.name, components: {}, packages: [], profiles: { [profileName]: {} } };
|
|
638
|
+
const isExcluded = (cat, id) => exclude.some((e) => e === id || e === `${cat}/${id}`);
|
|
639
|
+
const pkgs = (await detectPackages(ctx, all)).filter((p) => !isExcluded("", p.id));
|
|
640
|
+
const generated = await detectGenerated(all, pkgs);
|
|
641
|
+
const found = all.filter((f) => !pkgs.some((p) => p.path === f.path) && !generated.has(`${f.category}/${f.id}`));
|
|
642
|
+
for (const p of pkgs) {
|
|
643
|
+
m.packages.push({ id: p.id, source: p.source, into: p.into });
|
|
644
|
+
ctx.log(` \u2261 package ${p.id} ${p.source} @${p.commit.slice(0, 7)} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
|
|
645
|
+
}
|
|
646
|
+
if (pkgs.length) m.profiles[profileName][PACKAGES] = pkgs.map((p) => p.id);
|
|
647
|
+
for (const [key, by] of generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
|
|
648
|
+
const managed = [];
|
|
649
|
+
let copied = 0;
|
|
650
|
+
for (const cat of ctx.adapter.categories()) {
|
|
651
|
+
const mine = found.filter((f) => f.category === cat.name && !isExcluded(f.category, f.id));
|
|
652
|
+
for (const f of found.filter((f2) => f2.category === cat.name && isExcluded(f2.category, f2.id))) {
|
|
653
|
+
skipped.push(`${f.category}/${f.id}`);
|
|
654
|
+
ctx.log(` - ${f.category}/${f.id} (--exclude)`);
|
|
655
|
+
}
|
|
656
|
+
if (!mine.length) continue;
|
|
657
|
+
m.components[cat.name] = [];
|
|
658
|
+
m.profiles[profileName][cat.name] = [];
|
|
659
|
+
for (const f of mine) {
|
|
660
|
+
const dst = path10.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
661
|
+
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
662
|
+
m.components[cat.name].push({ id: f.id });
|
|
663
|
+
m.profiles[profileName][cat.name].push(f.id);
|
|
664
|
+
managed.push(targetRel(cat, f.id));
|
|
665
|
+
copied++;
|
|
666
|
+
ctx.log(` + ${cat.name}/${f.id}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const instr = instructionsFile(ctx);
|
|
670
|
+
if (await exists(instr)) {
|
|
671
|
+
const text = await fs7.readFile(instr, "utf8");
|
|
672
|
+
if (!isGenerated(text)) {
|
|
673
|
+
const dst = path10.join(ctx.shed, INSTRUCTIONS, "main.md");
|
|
674
|
+
await fs7.mkdir(path10.dirname(dst), { recursive: true });
|
|
675
|
+
await fs7.writeFile(dst, text);
|
|
676
|
+
const fragRel = targetRel(INSTRUCTIONS, "main");
|
|
677
|
+
await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
|
|
678
|
+
managed.push(fragRel);
|
|
679
|
+
m.components[INSTRUCTIONS] = [{ id: "main" }];
|
|
680
|
+
m.profiles[profileName][INSTRUCTIONS] = ["main"];
|
|
681
|
+
copied++;
|
|
682
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path10.basename(instr)})`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
await fs7.mkdir(ctx.shed, { recursive: true });
|
|
686
|
+
let yamlText = stringifyManifest(m);
|
|
687
|
+
for (const p of pkgs) {
|
|
688
|
+
yamlText = yamlText.replace(` into: ${p.into}
|
|
689
|
+
`, ` into: ${p.into}
|
|
690
|
+
# install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
|
|
691
|
+
`);
|
|
692
|
+
}
|
|
693
|
+
await fs7.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
|
|
694
|
+
` + yamlText);
|
|
695
|
+
if (pkgs.length) {
|
|
696
|
+
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, commit: p.commit }])) });
|
|
697
|
+
}
|
|
698
|
+
await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
699
|
+
const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
|
|
700
|
+
if (pkgs.length) parts.push(`\uD328\uD0A4\uC9C0 ${pkgs.length}\uAC1C`);
|
|
701
|
+
if (generated.size) parts.push(`\uC0DD\uC131\uBB3C ${generated.size}\uAC1C \uAC74\uB108\uB700`);
|
|
702
|
+
if (skipped.length) parts.push(`\uC81C\uC678 ${skipped.length}\uAC1C`);
|
|
703
|
+
ctx.log(`
|
|
704
|
+
${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
|
|
705
|
+
if (pkgs.length) ctx.log(`\uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
|
|
706
|
+
return { manifest: m, copied, skipped, packages: pkgs.map((p) => p.id), generated: [...generated.keys()] };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/core/restore.ts
|
|
710
|
+
import { promises as fs8 } from "fs";
|
|
711
|
+
import path11 from "path";
|
|
712
|
+
async function restore(ctx, profileArg, opts = {}) {
|
|
713
|
+
const backup = opts.backup ?? true;
|
|
714
|
+
const state = await readState(ctx.adapter);
|
|
715
|
+
const profile = profileArg ?? state?.profile;
|
|
716
|
+
if (!profile) throw new Error("\uD504\uB85C\uD544\uC744 \uC9C0\uC815\uD558\uC138\uC694: lshed restore <profile> (\uC774\uC804\uC5D0 \uC801\uC6A9\uD55C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4)");
|
|
717
|
+
const m = await loadManifest(ctx);
|
|
718
|
+
const plan = planProfile(ctx, m, profile);
|
|
719
|
+
for (const it of plan) {
|
|
720
|
+
if (!await exists(it.src)) throw new Error(`${it.category}/${it.id}: \uCC3D\uACE0\uC5D0 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${it.src}`);
|
|
721
|
+
}
|
|
722
|
+
const pkgRes = await ensurePackages(ctx, packagesOf(m, profile), { dryRun: opts.dryRun, yes: opts.yes });
|
|
723
|
+
const instrRel = ctx.adapter.instructionsFileName();
|
|
724
|
+
const fragments = plan.filter((p) => p.category === INSTRUCTIONS);
|
|
725
|
+
const newManaged = new Set(plan.map((p) => p.rel));
|
|
726
|
+
if (fragments.length) newManaged.add(instrRel);
|
|
727
|
+
const oldManaged = new Set(state?.managed ?? []);
|
|
728
|
+
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
729
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
730
|
+
const backupDir = path11.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
731
|
+
const backedUp = [];
|
|
732
|
+
const placed = [];
|
|
733
|
+
async function backUp(rel) {
|
|
734
|
+
const from = abs(ctx, rel);
|
|
735
|
+
if (!await exists(from)) return;
|
|
736
|
+
backedUp.push(rel);
|
|
737
|
+
if (opts.dryRun || !backup) return;
|
|
738
|
+
await copyTree(from, path11.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
|
|
739
|
+
}
|
|
740
|
+
for (const rel of toRemove) {
|
|
741
|
+
ctx.log(` - ${rel}`);
|
|
742
|
+
await backUp(rel);
|
|
743
|
+
if (!opts.dryRun) await removeTree(abs(ctx, rel));
|
|
744
|
+
}
|
|
745
|
+
for (const it of plan) {
|
|
746
|
+
const target = abs(ctx, it.rel);
|
|
747
|
+
const same = await hashTree(target, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx));
|
|
748
|
+
const mark = same ? "=" : await exists(target) ? "~" : "+";
|
|
749
|
+
ctx.log(` ${mark} ${it.rel}`);
|
|
750
|
+
if (same) {
|
|
751
|
+
placed.push(it.rel);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (await exists(target)) await backUp(it.rel);
|
|
755
|
+
if (!opts.dryRun) await copyTree(it.src, target, ignoreOf(ctx));
|
|
756
|
+
placed.push(it.rel);
|
|
757
|
+
}
|
|
758
|
+
const instrPath = instructionsFile(ctx);
|
|
759
|
+
if (fragments.length) {
|
|
760
|
+
const contents = [];
|
|
761
|
+
for (const f of fragments) contents.push({ id: f.id, content: await fs8.readFile(f.src, "utf8") });
|
|
762
|
+
const rendered = renderInstructions(ctx, profile, contents);
|
|
763
|
+
const existing = await exists(instrPath) ? await fs8.readFile(instrPath, "utf8") : null;
|
|
764
|
+
if (existing !== rendered) {
|
|
765
|
+
const mark = existing === null ? "+" : "~";
|
|
766
|
+
ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
|
|
767
|
+
if (existing !== null) await backUp(instrRel);
|
|
768
|
+
if (!opts.dryRun) await fs8.writeFile(instrPath, rendered);
|
|
769
|
+
} else {
|
|
770
|
+
ctx.log(` = ${instrRel}`);
|
|
771
|
+
}
|
|
772
|
+
placed.push(instrRel);
|
|
773
|
+
}
|
|
774
|
+
if (opts.dryRun) {
|
|
775
|
+
ctx.log(`
|
|
776
|
+
(dry-run) \uBCC0\uACBD \uC5C6\uC74C. \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}, \uBC31\uC5C5 \uC608\uC815 ${backedUp.length}`);
|
|
777
|
+
reportPending(ctx, pkgRes);
|
|
778
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: null };
|
|
779
|
+
}
|
|
780
|
+
await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
781
|
+
const bdir = backup && backedUp.length ? backupDir : null;
|
|
782
|
+
ctx.log(`
|
|
783
|
+
\uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.cloned.length ? `, \uD328\uD0A4\uC9C0 clone ${pkgRes.cloned.length}` : ""}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
|
|
784
|
+
reportPending(ctx, pkgRes);
|
|
785
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: bdir };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/core/diff.ts
|
|
789
|
+
async function diff(ctx) {
|
|
790
|
+
const state = await readState(ctx.adapter);
|
|
791
|
+
if (!state) throw new Error("\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 'lshed restore <profile>' \uC744 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
792
|
+
const m = await loadManifest(ctx);
|
|
793
|
+
const out = [];
|
|
794
|
+
for (const item of planProfile(ctx, m, state.profile)) {
|
|
795
|
+
const changes = await diffTrees(abs(ctx, item.rel), item.src, ignoreOf(ctx));
|
|
796
|
+
if (changes.length) out.push({ item, changes });
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
function formatDiff(diffs) {
|
|
801
|
+
if (!diffs.length) return "\uB85C\uCEEC\uACFC \uCC3D\uACE0\uAC00 \uC77C\uCE58\uD569\uB2C8\uB2E4.";
|
|
802
|
+
const lines = [];
|
|
803
|
+
for (const d of diffs) {
|
|
804
|
+
lines.push(`${d.item.category}/${d.item.id}`);
|
|
805
|
+
for (const c of d.changes) lines.push(` ${c.status} ${c.file || "(file)"}`);
|
|
806
|
+
}
|
|
807
|
+
lines.push("", "A: \uB85C\uCEEC\uC5D0\uB9CC \uC788\uC74C M: \uB0B4\uC6A9 \uB2E4\uB984 D: \uCC3D\uACE0\uC5D0\uB9CC \uC788\uC74C", "\uB85C\uCEEC \uD3B8\uC9D1\uC744 \uCC3D\uACE0\uC5D0 \uBC18\uC601\uD558\uB824\uBA74: lshed save");
|
|
808
|
+
return lines.join("\n");
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/core/status.ts
|
|
812
|
+
async function status(ctx) {
|
|
813
|
+
const state = await readState(ctx.adapter);
|
|
814
|
+
if (!state) return { state: null, drifted: [], packages: [] };
|
|
815
|
+
const d = await diff(ctx);
|
|
816
|
+
const m = await loadManifest(ctx);
|
|
817
|
+
const lock = await readLock(ctx.shed);
|
|
818
|
+
const packages = await Promise.all(packagesOf(m, state.profile).map((p) => packageStatus(ctx, p, lock)));
|
|
819
|
+
return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages };
|
|
820
|
+
}
|
|
821
|
+
function formatStatus(s, adapterRoot) {
|
|
822
|
+
if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
|
|
823
|
+
lshed init --shed <dir> \uB610\uB294 lshed restore <profile>`;
|
|
824
|
+
const lines = [
|
|
825
|
+
`\uD504\uB85C\uD544 ${s.state.profile}`,
|
|
826
|
+
`\uCC3D\uACE0 ${s.state.shed}`,
|
|
827
|
+
`\uC801\uC6A9 ${s.state.appliedAt}`,
|
|
828
|
+
`\uAD00\uB9AC \uC911 ${s.state.managed.length}\uAC1C \uACBD\uB85C (${adapterRoot})`
|
|
829
|
+
];
|
|
830
|
+
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");
|
|
831
|
+
for (const p of s.packages) {
|
|
832
|
+
const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${p.head.slice(0, 7)} (\uB77D \uC5C6\uC74C)` : p.head === p.locked ? `${p.head.slice(0, 7)} = lock` : `${p.head.slice(0, 7)} \u2260 lock ${p.locked.slice(0, 7)} \u2192 lshed update`;
|
|
833
|
+
lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
|
|
834
|
+
}
|
|
835
|
+
return lines.join("\n");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/core/save.ts
|
|
839
|
+
async function save(ctx, ids = []) {
|
|
840
|
+
const state = await readState(ctx.adapter);
|
|
841
|
+
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.");
|
|
842
|
+
const m = await loadManifest(ctx);
|
|
843
|
+
let plan = planProfile(ctx, m, state.profile);
|
|
844
|
+
if (ids.length) {
|
|
845
|
+
plan = ids.map((raw) => {
|
|
846
|
+
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
847
|
+
const hits = plan.filter((p) => p.id === b && (a === void 0 || p.category === a));
|
|
848
|
+
if (!hits.length) throw new Error(`"${raw}" \uB294 \uD604\uC7AC \uD504\uB85C\uD544(${state.profile})\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
849
|
+
if (hits.length > 1) throw new Error(`"${raw}" \uAC00 \uBAA8\uD638\uD569\uB2C8\uB2E4: ${hits.map((h) => `${h.category}/${h.id}`).join(", ")}`);
|
|
850
|
+
return hits[0];
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
const saved = [];
|
|
854
|
+
for (const it of plan) {
|
|
855
|
+
const kind = it.category === INSTRUCTIONS ? "file" : ctx.adapter.categories().find((c) => c.name === it.category).kind;
|
|
856
|
+
const src = effectiveSource(it.category, it.component, kind);
|
|
857
|
+
if (!isSaveable(src)) {
|
|
858
|
+
ctx.log(` ! ${it.category}/${it.id}: \uC6D0\uACA9 \uCD9C\uCC98(${src})\uB294 save \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const local = abs(ctx, it.rel);
|
|
862
|
+
if (!await exists(local)) {
|
|
863
|
+
ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (await hashTree(local, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx))) continue;
|
|
867
|
+
await copyTree(local, it.src, ignoreOf(ctx));
|
|
868
|
+
saved.push(`${it.category}/${it.id}`);
|
|
869
|
+
ctx.log(` \u2713 ${it.category}/${it.id} \u2192 \uCC3D\uACE0`);
|
|
870
|
+
}
|
|
871
|
+
ctx.log(saved.length ? `
|
|
872
|
+
${saved.length}\uAC1C \uBC18\uC601. \uCC3D\uACE0\uB97C \uCEE4\uBC0B/\uB3D9\uAE30\uD654\uD558\uC138\uC694: ${ctx.shed}` : "\uBC18\uC601\uD560 \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
873
|
+
return saved;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/core/list.ts
|
|
877
|
+
function listRows(m) {
|
|
878
|
+
const usedBy = (cat, id) => Object.entries(m.profiles).filter(([, cats]) => (cats[cat] ?? []).includes(id)).map(([name]) => name);
|
|
879
|
+
const rows = [];
|
|
880
|
+
for (const [cat, comps] of Object.entries(m.components)) {
|
|
881
|
+
for (const c of comps) rows.push({ kind: "component", category: cat, id: c.id, usedBy: usedBy(cat, c.id) });
|
|
882
|
+
}
|
|
883
|
+
for (const p of m.packages) rows.push({ kind: "package", category: PACKAGES, id: p.id, usedBy: usedBy(PACKAGES, p.id) });
|
|
884
|
+
return rows;
|
|
885
|
+
}
|
|
886
|
+
function formatRows(rows, m) {
|
|
887
|
+
if (!rows.length) return "(\uBE44\uC5B4 \uC788\uC74C)";
|
|
888
|
+
const w = Math.max(...rows.map((r) => `${r.category}/${r.id}`.length));
|
|
889
|
+
const lines = rows.map((r) => {
|
|
890
|
+
const key = `${r.category}/${r.id}`.padEnd(w);
|
|
891
|
+
const use = r.usedBy.length ? r.usedBy.join(", ") : "(\uBBF8\uC0AC\uC6A9)";
|
|
892
|
+
return `${r.kind === "package" ? "\u2261" : " "} ${key} ${use}`;
|
|
893
|
+
});
|
|
894
|
+
const unused = rows.filter((r) => !r.usedBy.length).length;
|
|
895
|
+
lines.push("", `${rows.length}\uAC1C, \uD504\uB85C\uD544 ${Object.keys(m.profiles).length}\uAC1C${unused ? `, \uBBF8\uC0AC\uC6A9 ${unused}\uAC1C \u2192 lshed prune` : ""}`);
|
|
896
|
+
return lines.join("\n");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/core/remove.ts
|
|
900
|
+
import { promises as fs9 } from "fs";
|
|
901
|
+
import path12 from "path";
|
|
902
|
+
import YAML3, { isSeq, isMap } from "yaml";
|
|
903
|
+
function resolveKey(m, raw) {
|
|
904
|
+
const rows = listRows(m);
|
|
905
|
+
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
906
|
+
const hits = rows.filter((r) => r.id === b && (a === void 0 || r.category === a));
|
|
907
|
+
if (!hits.length) throw new Error(`"${raw}" \uB294 \uCC3D\uACE0\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
908
|
+
if (hits.length > 1) throw new Error(`"${raw}" \uAC00 \uBAA8\uD638\uD569\uB2C8\uB2E4: ${hits.map((h) => `${h.category}/${h.id}`).join(", ")}`);
|
|
909
|
+
return { category: hits[0].category, id: hits[0].id };
|
|
910
|
+
}
|
|
911
|
+
async function remove(ctx, raw) {
|
|
912
|
+
const m = await loadManifest(ctx);
|
|
913
|
+
const { category, id } = resolveKey(m, raw);
|
|
914
|
+
const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
|
|
915
|
+
if (users.length) throw new Error(`${category}/${id} \uB294 \uD504\uB85C\uD544 ${users.join(", ")} \uC774 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uD504\uB85C\uD544\uC5D0\uC11C \uBE7C\uC138\uC694.`);
|
|
916
|
+
const text = await fs9.readFile(manifestPath(ctx), "utf8");
|
|
917
|
+
const doc = YAML3.parseDocument(text);
|
|
918
|
+
let deleted;
|
|
919
|
+
if (category === PACKAGES) {
|
|
920
|
+
const seq = doc.get(PACKAGES);
|
|
921
|
+
if (!isSeq(seq)) throw new Error("packages \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4");
|
|
922
|
+
const idx = seq.items.findIndex((it) => isMap(it) && it.get("id") === id);
|
|
923
|
+
seq.delete(idx);
|
|
924
|
+
if (!seq.items.length) doc.delete(PACKAGES);
|
|
925
|
+
const lock = await readLock(ctx.shed);
|
|
926
|
+
if (lock.packages[id]) {
|
|
927
|
+
delete lock.packages[id];
|
|
928
|
+
await writeLock(ctx.shed, lock);
|
|
929
|
+
}
|
|
930
|
+
ctx.log(` - package ${id} (\uB9E4\uB2C8\uD398\uC2A4\uD2B8\xB7\uB77D\uC5D0\uC11C \uC81C\uAC70. \uB85C\uCEEC clone \uC740 \uADF8\uB300\uB85C)`);
|
|
931
|
+
} else {
|
|
932
|
+
const seq = doc.getIn(["components", category]);
|
|
933
|
+
if (!isSeq(seq)) throw new Error(`components.${category} \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
934
|
+
const idx = seq.items.findIndex((it) => isMap(it) && it.get("id") === id);
|
|
935
|
+
seq.delete(idx);
|
|
936
|
+
if (!seq.items.length) doc.deleteIn(["components", category]);
|
|
937
|
+
const src = sourcePath(ctx, category, findComponent(m, category, id));
|
|
938
|
+
const inside = !path12.relative(ctx.shed, src).startsWith("..");
|
|
939
|
+
if (inside && await exists(src)) {
|
|
940
|
+
await removeTree(src);
|
|
941
|
+
deleted = src;
|
|
942
|
+
}
|
|
943
|
+
ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
|
|
944
|
+
}
|
|
945
|
+
await fs9.writeFile(manifestPath(ctx), doc.toString());
|
|
946
|
+
return { category, id, deleted };
|
|
947
|
+
}
|
|
948
|
+
async function prune(ctx, opts = {}) {
|
|
949
|
+
const m = await loadManifest(ctx);
|
|
950
|
+
const unused = listRows(m).filter((r) => !r.usedBy.length);
|
|
951
|
+
if (!unused.length) {
|
|
952
|
+
ctx.log("\uBBF8\uC0AC\uC6A9 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
if (!opts.yes) {
|
|
956
|
+
ctx.log(`\uBBF8\uC0AC\uC6A9 ${unused.length}\uAC1C (\uC9C0\uC6B0\uB824\uBA74 --yes):`);
|
|
957
|
+
for (const r of unused) ctx.log(` ${r.category}/${r.id}`);
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
const removed = [];
|
|
961
|
+
for (const r of unused) {
|
|
962
|
+
await remove(ctx, `${r.category}/${r.id}`);
|
|
963
|
+
removed.push(`${r.category}/${r.id}`);
|
|
964
|
+
}
|
|
965
|
+
ctx.log(`
|
|
966
|
+
${removed.length}\uAC1C \uC81C\uAC70. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC138\uC694: ${ctx.shed}`);
|
|
967
|
+
return removed;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/cli.ts
|
|
971
|
+
var { version } = createRequire(import.meta.url)("../package.json");
|
|
972
|
+
var program = new Command().name("lshed").description("Keep your coding-agent harness (skills, agents, commands, instructions) in a shed and restore it anywhere by profile.").version(version).option("--shed <dir>", "shed directory (default: $LSHED_HOME, then the shed recorded by the last restore)").option("--root <dir>", "agent config root (default: ~/.claude)");
|
|
973
|
+
function adapterFromOpts() {
|
|
974
|
+
const { root } = program.opts();
|
|
975
|
+
return new ClaudeCodeAdapter(root ? path13.resolve(root) : void 0);
|
|
976
|
+
}
|
|
977
|
+
async function ctxFor(cmd) {
|
|
978
|
+
const adapter = adapterFromOpts();
|
|
979
|
+
const { shed: flag } = program.opts();
|
|
980
|
+
let shed = flag ?? process.env.LSHED_HOME;
|
|
981
|
+
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
982
|
+
if (!shed && cmd === "init") shed = path13.join(os2.homedir(), "lshed");
|
|
983
|
+
if (!shed) throw new Error("\uCC3D\uACE0 \uC704\uCE58\uB97C \uBAA8\uB985\uB2C8\uB2E4. --shed <dir> \uB610\uB294 LSHED_HOME \uC744 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
984
|
+
return { adapter, shed: path13.resolve(shed), log: (l) => console.log(l) };
|
|
985
|
+
}
|
|
986
|
+
async function run(fn) {
|
|
987
|
+
try {
|
|
988
|
+
await fn();
|
|
989
|
+
} catch (e) {
|
|
990
|
+
console.error(`\uC624\uB958: ${e.message}`);
|
|
991
|
+
process.exitCode = 1;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
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 () => {
|
|
995
|
+
const ctx = await ctxFor("init");
|
|
996
|
+
console.log(`\uC2A4\uCE94: ${ctx.adapter.root} \u2192 \uCC3D\uACE0: ${ctx.shed}`);
|
|
997
|
+
await init(ctx, { profile: o.profile, exclude: o.exclude });
|
|
998
|
+
console.log(`
|
|
999
|
+
\uB2E4\uC74C: \uCC3D\uACE0\uB97C git \uC73C\uB85C \uAD00\uB9AC\uD558\uC138\uC694. cd ${ctx.shed} && git init`);
|
|
1000
|
+
}));
|
|
1001
|
+
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 () => {
|
|
1002
|
+
const ctx = await ctxFor("other");
|
|
1003
|
+
await restore(ctx, profile, { dryRun: o.dryRun, backup: o.backup, yes: o.yes });
|
|
1004
|
+
}));
|
|
1005
|
+
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 () => {
|
|
1006
|
+
const ctx = await ctxFor("other");
|
|
1007
|
+
const state = await readState(ctx.adapter);
|
|
1008
|
+
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.");
|
|
1009
|
+
const m = await loadManifest(ctx);
|
|
1010
|
+
let pkgs = packagesOf(m, state.profile);
|
|
1011
|
+
if (ids.length) {
|
|
1012
|
+
pkgs = ids.map((id) => {
|
|
1013
|
+
const p = m.packages.find((x2) => x2.id === id);
|
|
1014
|
+
if (!p) throw new Error(`\uD328\uD0A4\uC9C0 "${id}" \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
1015
|
+
return p;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
if (!pkgs.length) {
|
|
1019
|
+
console.log("\uAC31\uC2E0\uD560 \uD328\uD0A4\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const res = await updatePackages(ctx, pkgs, { dryRun: o.dryRun, yes: o.yes });
|
|
1023
|
+
reportPending(ctx, res);
|
|
1024
|
+
}));
|
|
1025
|
+
program.command("status").description("show the applied profile, managed paths and drift").action(() => run(async () => {
|
|
1026
|
+
const adapter = adapterFromOpts();
|
|
1027
|
+
const state = await readState(adapter);
|
|
1028
|
+
if (!state) {
|
|
1029
|
+
console.log(formatStatus({ state: null, drifted: [], packages: [] }, adapter.root));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const ctx = await ctxFor("other");
|
|
1033
|
+
console.log(formatStatus(await status(ctx), adapter.root));
|
|
1034
|
+
}));
|
|
1035
|
+
program.command("diff").description("list files that differ between the local harness and the shed").action(() => run(async () => {
|
|
1036
|
+
const ctx = await ctxFor("other");
|
|
1037
|
+
console.log(formatDiff(await diff(ctx)));
|
|
1038
|
+
}));
|
|
1039
|
+
program.command("save [ids...]").description("copy local edits back into the shed (file: sources only)").action((ids) => run(async () => {
|
|
1040
|
+
const ctx = await ctxFor("other");
|
|
1041
|
+
await save(ctx, ids);
|
|
1042
|
+
}));
|
|
1043
|
+
program.command("list").description("everything in the shed and which profiles use it").option("--unused", "only things no profile uses").action((o) => run(async () => {
|
|
1044
|
+
const ctx = await ctxFor("other");
|
|
1045
|
+
const m = await loadManifest(ctx);
|
|
1046
|
+
const rows = listRows(m).filter((r) => !o.unused || !r.usedBy.length);
|
|
1047
|
+
console.log(o.unused && !rows.length ? "\uBBF8\uC0AC\uC6A9 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." : formatRows(rows, m));
|
|
1048
|
+
}));
|
|
1049
|
+
program.command("remove <key>").description("delete a component or package from the shed (refused while a profile uses it)").action((key) => run(async () => {
|
|
1050
|
+
const ctx = await ctxFor("other");
|
|
1051
|
+
await remove(ctx, key);
|
|
1052
|
+
}));
|
|
1053
|
+
program.command("prune").description("remove everything no profile uses").option("--yes", "actually delete; without it, just list").action((o) => run(async () => {
|
|
1054
|
+
const ctx = await ctxFor("other");
|
|
1055
|
+
await prune(ctx, { yes: o.yes });
|
|
1056
|
+
}));
|
|
1057
|
+
program.command("scan").description("(debug) list components found in the agent config root").action(() => run(async () => {
|
|
1058
|
+
const adapter = adapterFromOpts();
|
|
1059
|
+
const found = await adapter.scan();
|
|
1060
|
+
for (const c of found) console.log(`${c.category}/${c.id} ${c.path}`);
|
|
1061
|
+
console.error(`${found.length}\uAC1C \uBC1C\uACAC (root: ${adapter.root})`);
|
|
1062
|
+
}));
|
|
1063
|
+
program.parseAsync();
|