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