lshed 0.3.0 → 0.6.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 +26 -0
- package/README.md +231 -60
- package/dist/cli.js +730 -234
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import os3 from "os";
|
|
7
|
+
import path19 from "path";
|
|
8
8
|
|
|
9
9
|
// src/adapters/claude-code.ts
|
|
10
|
-
import { promises as
|
|
11
|
-
import
|
|
10
|
+
import { promises as fs4 } from "fs";
|
|
11
|
+
import path4 from "path";
|
|
12
12
|
import os from "os";
|
|
13
13
|
|
|
14
14
|
// src/installers/claude-plugin.ts
|
|
@@ -166,79 +166,14 @@ var pluginInstaller = {
|
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
// src/adapters/claude-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{ name: "agents", root: "agents", kind: "file" },
|
|
173
|
-
{ name: "commands", root: "commands", kind: "file" }
|
|
174
|
-
// instructions 는 단일 파일(CLAUDE.md)이라 스캔 대상이 아니라 생성 대상이다 (§3.3)
|
|
175
|
-
];
|
|
176
|
-
var ClaudeCodeAdapter = class {
|
|
177
|
-
name = "claude-code";
|
|
178
|
-
root;
|
|
179
|
-
constructor(root) {
|
|
180
|
-
this.root = root ?? path2.join(os.homedir(), ".claude");
|
|
181
|
-
}
|
|
182
|
-
categories() {
|
|
183
|
-
return CATEGORIES;
|
|
184
|
-
}
|
|
185
|
-
instructionsStrategy() {
|
|
186
|
-
return "import";
|
|
187
|
-
}
|
|
188
|
-
instructionsFileName() {
|
|
189
|
-
return "CLAUDE.md";
|
|
190
|
-
}
|
|
191
|
-
installers() {
|
|
192
|
-
return [marketplaceInstaller, pluginInstaller];
|
|
193
|
-
}
|
|
194
|
-
async scan() {
|
|
195
|
-
const out = [];
|
|
196
|
-
for (const cat of CATEGORIES) {
|
|
197
|
-
const dir = path2.join(this.root, cat.root);
|
|
198
|
-
let entries;
|
|
199
|
-
try {
|
|
200
|
-
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
201
|
-
} catch {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
for (const e of entries) {
|
|
205
|
-
if (e.name.startsWith(".")) continue;
|
|
206
|
-
const full = path2.join(dir, e.name);
|
|
207
|
-
let st;
|
|
208
|
-
try {
|
|
209
|
-
st = await fs2.stat(full);
|
|
210
|
-
} catch {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (cat.kind === "dir" && st.isDirectory()) {
|
|
214
|
-
out.push({ category: cat.name, id: e.name, path: full });
|
|
215
|
-
} else if (cat.kind === "file" && st.isFile() && e.name.endsWith(".md")) {
|
|
216
|
-
out.push({ category: cat.name, id: e.name.slice(0, -3), path: full });
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return out;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// src/core/context.ts
|
|
225
|
-
import { promises as fs6 } from "fs";
|
|
226
|
-
import path8 from "path";
|
|
227
|
-
import { spawn as spawn2 } from "child_process";
|
|
228
|
-
|
|
229
|
-
// src/installers/git.ts
|
|
230
|
-
import { promises as fs4 } from "fs";
|
|
231
|
-
import path5 from "path";
|
|
232
|
-
|
|
233
|
-
// src/git.ts
|
|
234
|
-
import { execFile, spawn } from "child_process";
|
|
235
|
-
import { promisify } from "util";
|
|
236
|
-
import path4 from "path";
|
|
169
|
+
// src/adapters/claude-mcp.ts
|
|
170
|
+
import { promises as fs3 } from "fs";
|
|
171
|
+
import path3 from "path";
|
|
237
172
|
|
|
238
173
|
// src/fsutil.ts
|
|
239
|
-
import { promises as
|
|
174
|
+
import { promises as fs2 } from "fs";
|
|
240
175
|
import { createHash } from "crypto";
|
|
241
|
-
import
|
|
176
|
+
import path2 from "path";
|
|
242
177
|
|
|
243
178
|
// src/ignore.ts
|
|
244
179
|
var DEFAULT_IGNORE = [
|
|
@@ -263,7 +198,7 @@ function isIgnored(rel, patterns) {
|
|
|
263
198
|
// src/fsutil.ts
|
|
264
199
|
async function exists(p) {
|
|
265
200
|
try {
|
|
266
|
-
await
|
|
201
|
+
await fs2.access(p);
|
|
267
202
|
return true;
|
|
268
203
|
} catch {
|
|
269
204
|
return false;
|
|
@@ -271,7 +206,7 @@ async function exists(p) {
|
|
|
271
206
|
}
|
|
272
207
|
async function isDir(p) {
|
|
273
208
|
try {
|
|
274
|
-
return (await
|
|
209
|
+
return (await fs2.stat(p)).isDirectory();
|
|
275
210
|
} catch {
|
|
276
211
|
return false;
|
|
277
212
|
}
|
|
@@ -280,48 +215,48 @@ async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
|
280
215
|
if (!await exists(root)) return [];
|
|
281
216
|
if (!await isDir(root)) return [""];
|
|
282
217
|
const out = [];
|
|
283
|
-
async function
|
|
284
|
-
const entries = await
|
|
218
|
+
async function walk2(dir, rel) {
|
|
219
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
285
220
|
for (const e of entries) {
|
|
286
221
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
287
222
|
if (isIgnored(r, ignore)) continue;
|
|
288
|
-
const full =
|
|
223
|
+
const full = path2.join(dir, e.name);
|
|
289
224
|
let st;
|
|
290
225
|
try {
|
|
291
|
-
st = await
|
|
226
|
+
st = await fs2.stat(full);
|
|
292
227
|
} catch {
|
|
293
228
|
continue;
|
|
294
229
|
}
|
|
295
|
-
if (st.isDirectory()) await
|
|
230
|
+
if (st.isDirectory()) await walk2(full, r);
|
|
296
231
|
else if (st.isFile()) out.push(r);
|
|
297
232
|
}
|
|
298
233
|
}
|
|
299
|
-
await
|
|
234
|
+
await walk2(root, "");
|
|
300
235
|
return out.sort();
|
|
301
236
|
}
|
|
302
237
|
async function hashFile(p) {
|
|
303
|
-
return createHash("sha256").update(await
|
|
238
|
+
return createHash("sha256").update(await fs2.readFile(p)).digest("hex");
|
|
304
239
|
}
|
|
305
240
|
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
306
241
|
if (!await exists(root)) return null;
|
|
307
242
|
const h = createHash("sha256");
|
|
308
243
|
for (const rel of await listFiles(root, ignore)) {
|
|
309
|
-
h.update(rel).update("\0").update(await
|
|
244
|
+
h.update(rel).update("\0").update(await fs2.readFile(rel ? path2.join(root, rel) : root)).update("\0");
|
|
310
245
|
}
|
|
311
246
|
return h.digest("hex");
|
|
312
247
|
}
|
|
313
248
|
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
314
|
-
await
|
|
315
|
-
await
|
|
316
|
-
const srcRoot =
|
|
317
|
-
await
|
|
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, {
|
|
318
253
|
recursive: true,
|
|
319
254
|
dereference: true,
|
|
320
255
|
filter: async (from) => {
|
|
321
|
-
const rel =
|
|
256
|
+
const rel = path2.relative(srcRoot, path2.resolve(from)).split(path2.sep).join("/");
|
|
322
257
|
if (isIgnored(rel, ignore)) return false;
|
|
323
258
|
try {
|
|
324
|
-
if ((await
|
|
259
|
+
if ((await fs2.lstat(from)).isSymbolicLink()) await fs2.stat(from);
|
|
325
260
|
} catch {
|
|
326
261
|
return false;
|
|
327
262
|
}
|
|
@@ -330,15 +265,15 @@ async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
|
330
265
|
});
|
|
331
266
|
}
|
|
332
267
|
async function removeTree(p) {
|
|
333
|
-
await
|
|
268
|
+
await fs2.rm(p, { recursive: true, force: true });
|
|
334
269
|
}
|
|
335
270
|
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
336
271
|
const l = new Set(await listFiles(local, ignore));
|
|
337
272
|
const s = new Set(await listFiles(shed, ignore));
|
|
338
273
|
const out = [];
|
|
339
274
|
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
340
|
-
const lp = f ?
|
|
341
|
-
const sp = f ?
|
|
275
|
+
const lp = f ? path2.join(local, f) : local;
|
|
276
|
+
const sp = f ? path2.join(shed, f) : shed;
|
|
342
277
|
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
343
278
|
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
344
279
|
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
@@ -346,13 +281,127 @@ async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
|
346
281
|
return out;
|
|
347
282
|
}
|
|
348
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
|
|
327
|
+
var CATEGORIES = [
|
|
328
|
+
{ name: "skills", root: "skills", kind: "dir" },
|
|
329
|
+
{ name: "agents", root: "agents", kind: "file" },
|
|
330
|
+
{ name: "commands", root: "commands", kind: "file" }
|
|
331
|
+
// instructions 는 단일 파일(CLAUDE.md)이라 스캔 대상이 아니라 생성 대상이다 (§3.3)
|
|
332
|
+
];
|
|
333
|
+
var ClaudeCodeAdapter = class {
|
|
334
|
+
name = "claude-code";
|
|
335
|
+
root;
|
|
336
|
+
mcp;
|
|
337
|
+
constructor(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];
|
|
343
|
+
}
|
|
344
|
+
categories() {
|
|
345
|
+
return CATEGORIES;
|
|
346
|
+
}
|
|
347
|
+
instructionsStrategy() {
|
|
348
|
+
return "import";
|
|
349
|
+
}
|
|
350
|
+
instructionsFileName() {
|
|
351
|
+
return "CLAUDE.md";
|
|
352
|
+
}
|
|
353
|
+
installers() {
|
|
354
|
+
return [marketplaceInstaller, pluginInstaller];
|
|
355
|
+
}
|
|
356
|
+
async scan() {
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const cat of CATEGORIES) {
|
|
359
|
+
const dir = path4.join(this.root, cat.root);
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const e of entries) {
|
|
367
|
+
if (e.name.startsWith(".")) continue;
|
|
368
|
+
const full = path4.join(dir, e.name);
|
|
369
|
+
let st;
|
|
370
|
+
try {
|
|
371
|
+
st = await fs4.stat(full);
|
|
372
|
+
} catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (cat.kind === "dir" && st.isDirectory()) {
|
|
376
|
+
out.push({ category: cat.name, id: e.name, path: full });
|
|
377
|
+
} else if (cat.kind === "file" && st.isFile() && e.name.endsWith(".md")) {
|
|
378
|
+
out.push({ category: cat.name, id: e.name.slice(0, -3), path: full });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/core/context.ts
|
|
387
|
+
import { promises as fs7 } from "fs";
|
|
388
|
+
import path9 from "path";
|
|
389
|
+
import { spawn as spawn2 } from "child_process";
|
|
390
|
+
|
|
391
|
+
// src/installers/git.ts
|
|
392
|
+
import { promises as fs5 } from "fs";
|
|
393
|
+
import path6 from "path";
|
|
394
|
+
|
|
349
395
|
// src/git.ts
|
|
396
|
+
import { execFile, spawn } from "child_process";
|
|
397
|
+
import { promisify } from "util";
|
|
398
|
+
import path5 from "path";
|
|
350
399
|
var x = promisify(execFile);
|
|
351
400
|
async function git(args, cwd) {
|
|
352
401
|
const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
|
|
353
402
|
return stdout.trim();
|
|
354
403
|
}
|
|
355
|
-
var isRepo = (dir) => exists(
|
|
404
|
+
var isRepo = (dir) => exists(path5.join(dir, ".git"));
|
|
356
405
|
var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
|
|
357
406
|
var head = (dir) => git(["rev-parse", "HEAD"], dir);
|
|
358
407
|
var branch = async (dir) => {
|
|
@@ -373,7 +422,7 @@ function runShell(cmd, cwd) {
|
|
|
373
422
|
// src/installers/git.ts
|
|
374
423
|
function dirOf(ctx, pkg) {
|
|
375
424
|
if (!pkg.into) throw new Error(`package ${pkg.id}: git \uACC4\uC5F4 \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
|
|
376
|
-
return
|
|
425
|
+
return path6.join(ctx.adapter.root, ...pkg.into.split("/"));
|
|
377
426
|
}
|
|
378
427
|
var gitInstaller = {
|
|
379
428
|
name: "git",
|
|
@@ -385,7 +434,7 @@ var gitInstaller = {
|
|
|
385
434
|
if (!await isRepo(f.path)) continue;
|
|
386
435
|
const url = await remoteUrl(f.path);
|
|
387
436
|
if (!url) continue;
|
|
388
|
-
const into =
|
|
437
|
+
const into = path6.relative(ctx.adapter.root, f.path).split(path6.sep).join("/");
|
|
389
438
|
out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), rev: await head(f.path), path: f.path });
|
|
390
439
|
}
|
|
391
440
|
return out;
|
|
@@ -399,7 +448,7 @@ var gitInstaller = {
|
|
|
399
448
|
const dir = dirOf(ctx, pkg);
|
|
400
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.`);
|
|
401
450
|
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
402
|
-
await
|
|
451
|
+
await fs5.mkdir(path6.dirname(dir), { recursive: true });
|
|
403
452
|
await clone(url, dir, ref);
|
|
404
453
|
if (locked) {
|
|
405
454
|
await resetHard(dir, locked).catch(() => {
|
|
@@ -444,6 +493,8 @@ var ManifestSchema = z.object({
|
|
|
444
493
|
agent: z.string().default("claude-code"),
|
|
445
494
|
/** 창고에 담지 않을 이름들. 기본값(DEFAULT_IGNORE)에 더해진다. */
|
|
446
495
|
ignore: z.array(z.string()).optional(),
|
|
496
|
+
/** 로컬에 있어도 창고에 넣지 않을 부품 ("id" 또는 "category/id"). init --exclude 가 적고 add/status 가 따른다. */
|
|
497
|
+
exclude: z.array(z.string()).optional(),
|
|
447
498
|
components: ComponentsSchema.default({}),
|
|
448
499
|
packages: z.array(PackageSchema).default([]),
|
|
449
500
|
profiles: z.record(z.string(), ProfileSchema).default({})
|
|
@@ -506,13 +557,8 @@ ${problems.map((p) => " " + p).join("\n")}`);
|
|
|
506
557
|
return m;
|
|
507
558
|
}
|
|
508
559
|
function effectiveSource(category, c, kind = "dir") {
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
function stringifyManifest(m) {
|
|
512
|
-
const out = { ...m };
|
|
513
|
-
if (!m.packages.length) delete out.packages;
|
|
514
|
-
if (!m.ignore?.length) delete out.ignore;
|
|
515
|
-
return YAML.stringify(out, { lineWidth: 0 });
|
|
560
|
+
const ext = kind === "file" ? ".md" : kind === "entry" ? ".json" : "";
|
|
561
|
+
return c.source ?? `file:./${category}/${c.id}${ext}`;
|
|
516
562
|
}
|
|
517
563
|
function packagesOf(m, profile) {
|
|
518
564
|
const ids = m.profiles[profile]?.[PACKAGES] ?? [];
|
|
@@ -520,10 +566,10 @@ function packagesOf(m, profile) {
|
|
|
520
566
|
}
|
|
521
567
|
|
|
522
568
|
// src/resolvers/file.ts
|
|
523
|
-
import
|
|
569
|
+
import path7 from "path";
|
|
524
570
|
function resolveSource(shed, raw) {
|
|
525
571
|
const s = parseSource(raw);
|
|
526
|
-
if (s.scheme === "file") return
|
|
572
|
+
if (s.scheme === "file") return path7.resolve(shed, s.path);
|
|
527
573
|
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.`);
|
|
528
574
|
}
|
|
529
575
|
function isSaveable(raw) {
|
|
@@ -531,8 +577,8 @@ function isSaveable(raw) {
|
|
|
531
577
|
}
|
|
532
578
|
|
|
533
579
|
// src/state.ts
|
|
534
|
-
import { promises as
|
|
535
|
-
import
|
|
580
|
+
import { promises as fs6 } from "fs";
|
|
581
|
+
import path8 from "path";
|
|
536
582
|
import { z as z2 } from "zod";
|
|
537
583
|
var StateSchema = z2.object({
|
|
538
584
|
profile: z2.string(),
|
|
@@ -543,11 +589,11 @@ var StateSchema = z2.object({
|
|
|
543
589
|
});
|
|
544
590
|
var LSHED_DIR = "lshed";
|
|
545
591
|
function statePath(adapter) {
|
|
546
|
-
return
|
|
592
|
+
return path8.join(adapter.root, LSHED_DIR, "state.json");
|
|
547
593
|
}
|
|
548
594
|
async function readState(adapter) {
|
|
549
595
|
try {
|
|
550
|
-
const raw = JSON.parse(await
|
|
596
|
+
const raw = JSON.parse(await fs6.readFile(statePath(adapter), "utf8"));
|
|
551
597
|
return StateSchema.parse(raw);
|
|
552
598
|
} catch (e) {
|
|
553
599
|
if (e.code === "ENOENT") return null;
|
|
@@ -556,8 +602,8 @@ async function readState(adapter) {
|
|
|
556
602
|
}
|
|
557
603
|
async function writeState(adapter, state) {
|
|
558
604
|
const p = statePath(adapter);
|
|
559
|
-
await
|
|
560
|
-
await
|
|
605
|
+
await fs6.mkdir(path8.dirname(p), { recursive: true });
|
|
606
|
+
await fs6.writeFile(p, JSON.stringify(state, null, 2) + "\n");
|
|
561
607
|
}
|
|
562
608
|
|
|
563
609
|
// src/core/context.ts
|
|
@@ -582,15 +628,15 @@ var MANIFEST_FILE = "lshed.yaml";
|
|
|
582
628
|
var INSTRUCTIONS = "instructions";
|
|
583
629
|
var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
|
|
584
630
|
function manifestPath(ctx) {
|
|
585
|
-
return
|
|
631
|
+
return path9.join(ctx.shed, MANIFEST_FILE);
|
|
586
632
|
}
|
|
587
633
|
function knownCategories(adapter) {
|
|
588
|
-
return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
|
|
634
|
+
return [...adapter.categories().map((c) => c.name), ...adapter.entries().map((e) => e.name), INSTRUCTIONS];
|
|
589
635
|
}
|
|
590
636
|
async function loadManifest(ctx) {
|
|
591
637
|
let text;
|
|
592
638
|
try {
|
|
593
|
-
text = await
|
|
639
|
+
text = await fs7.readFile(manifestPath(ctx), "utf8");
|
|
594
640
|
} catch {
|
|
595
641
|
throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
|
|
596
642
|
\uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
|
|
@@ -604,12 +650,22 @@ function ignoreOf(ctx) {
|
|
|
604
650
|
}
|
|
605
651
|
function targetRel(cat, id) {
|
|
606
652
|
if (cat === INSTRUCTIONS) return `${FRAGMENTS_DIR}/${id}.md`;
|
|
653
|
+
if (cat.kind === "entry") return `${cat.name}:${id}`;
|
|
607
654
|
return cat.kind === "dir" ? `${cat.root}/${id}` : `${cat.root}/${id}.md`;
|
|
608
655
|
}
|
|
656
|
+
function entryOf(ctx, rel) {
|
|
657
|
+
const i = rel.indexOf(":");
|
|
658
|
+
if (i <= 0) return void 0;
|
|
659
|
+
const cat = ctx.adapter.entries().find((e) => e.name === rel.slice(0, i));
|
|
660
|
+
return cat ? { cat, id: rel.slice(i + 1) } : void 0;
|
|
661
|
+
}
|
|
662
|
+
function kindOf(ctx, category) {
|
|
663
|
+
if (category === INSTRUCTIONS) return "file";
|
|
664
|
+
if (ctx.adapter.entries().some((e) => e.name === category)) return "entry";
|
|
665
|
+
return ctx.adapter.categories().find((k) => k.name === category)?.kind ?? "dir";
|
|
666
|
+
}
|
|
609
667
|
function sourcePath(ctx, category, c) {
|
|
610
|
-
|
|
611
|
-
const kind = category === INSTRUCTIONS ? "file" : cat?.kind ?? "dir";
|
|
612
|
-
return resolveSource(ctx.shed, effectiveSource(category, c, kind));
|
|
668
|
+
return resolveSource(ctx.shed, effectiveSource(category, c, kindOf(ctx, category)));
|
|
613
669
|
}
|
|
614
670
|
function findComponent(m, category, id) {
|
|
615
671
|
const c = (m.components[category] ?? []).find((x2) => x2.id === id);
|
|
@@ -625,28 +681,30 @@ function planProfile(ctx, m, profile) {
|
|
|
625
681
|
const items = [];
|
|
626
682
|
for (const [category, ids] of Object.entries(p)) {
|
|
627
683
|
if (category === PACKAGES) continue;
|
|
628
|
-
const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category);
|
|
684
|
+
const cat = category === INSTRUCTIONS ? INSTRUCTIONS : ctx.adapter.categories().find((k) => k.name === category) ?? ctx.adapter.entries().find((e) => e.name === category);
|
|
629
685
|
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`);
|
|
686
|
+
const entry = cat !== INSTRUCTIONS && cat.kind === "entry" ? cat : void 0;
|
|
630
687
|
for (const id of ids) {
|
|
631
688
|
const component = findComponent(m, category, id);
|
|
632
|
-
items.push({ category, id, rel: targetRel(cat, id), src: sourcePath(ctx, category, component), component });
|
|
689
|
+
items.push({ category, id, rel: targetRel(cat, id), src: sourcePath(ctx, category, component), component, entry });
|
|
633
690
|
}
|
|
634
691
|
}
|
|
635
692
|
return items;
|
|
636
693
|
}
|
|
637
694
|
function abs(ctx, rel) {
|
|
638
|
-
return
|
|
695
|
+
return path9.join(ctx.adapter.root, ...rel.split("/"));
|
|
639
696
|
}
|
|
640
697
|
|
|
641
698
|
// src/core/init.ts
|
|
642
|
-
import { promises as
|
|
643
|
-
import
|
|
699
|
+
import { promises as fs11 } from "fs";
|
|
700
|
+
import path16 from "path";
|
|
701
|
+
import YAML4 from "yaml";
|
|
644
702
|
|
|
645
703
|
// src/core/instructions.ts
|
|
646
|
-
import
|
|
704
|
+
import path10 from "path";
|
|
647
705
|
var MARKER = "<!-- generated by lshed";
|
|
648
706
|
function instructionsFile(ctx) {
|
|
649
|
-
return
|
|
707
|
+
return path10.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
|
|
650
708
|
}
|
|
651
709
|
function isGenerated(text) {
|
|
652
710
|
return text.trimStart().startsWith(MARKER);
|
|
@@ -664,13 +722,9 @@ ${f.content.trimEnd()}
|
|
|
664
722
|
`).join("\n");
|
|
665
723
|
}
|
|
666
724
|
|
|
667
|
-
// src/
|
|
725
|
+
// src/lock.ts
|
|
668
726
|
import { promises as fs8 } from "fs";
|
|
669
727
|
import path11 from "path";
|
|
670
|
-
|
|
671
|
-
// src/lock.ts
|
|
672
|
-
import { promises as fs7 } from "fs";
|
|
673
|
-
import path10 from "path";
|
|
674
728
|
import YAML2 from "yaml";
|
|
675
729
|
import { z as z3 } from "zod";
|
|
676
730
|
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 ?? "" }));
|
|
@@ -681,7 +735,7 @@ var LockSchema = z3.object({
|
|
|
681
735
|
var LOCK_FILE = "lshed.lock";
|
|
682
736
|
async function readLock(shed) {
|
|
683
737
|
try {
|
|
684
|
-
return LockSchema.parse(YAML2.parse(await
|
|
738
|
+
return LockSchema.parse(YAML2.parse(await fs8.readFile(path11.join(shed, LOCK_FILE), "utf8")));
|
|
685
739
|
} catch (e) {
|
|
686
740
|
if (e.code === "ENOENT") return { version: 1, packages: {} };
|
|
687
741
|
throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
@@ -689,10 +743,15 @@ async function readLock(shed) {
|
|
|
689
743
|
}
|
|
690
744
|
async function writeLock(shed, lock) {
|
|
691
745
|
const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
|
|
692
|
-
await
|
|
746
|
+
await fs8.writeFile(path11.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
|
|
693
747
|
}
|
|
694
748
|
|
|
749
|
+
// src/core/discover.ts
|
|
750
|
+
import path13 from "path";
|
|
751
|
+
|
|
695
752
|
// src/core/packages.ts
|
|
753
|
+
import { promises as fs9 } from "fs";
|
|
754
|
+
import path12 from "path";
|
|
696
755
|
async function detectPackages(ctx, found) {
|
|
697
756
|
const out = [];
|
|
698
757
|
for (const inst of installersFor(ctx)) out.push(...await inst.detect(ctx, found));
|
|
@@ -702,26 +761,26 @@ async function detectGenerated(found, pkgs) {
|
|
|
702
761
|
const out = /* @__PURE__ */ new Map();
|
|
703
762
|
const located = pkgs.filter((p) => p.path);
|
|
704
763
|
if (!located.length) return out;
|
|
705
|
-
const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await
|
|
764
|
+
const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await fs9.realpath(p.path) })));
|
|
706
765
|
for (const f of found) {
|
|
707
766
|
if (located.some((p) => p.path === f.path)) continue;
|
|
708
767
|
let entries;
|
|
709
768
|
try {
|
|
710
|
-
if (!(await
|
|
711
|
-
entries = await
|
|
769
|
+
if (!(await fs9.stat(f.path)).isDirectory()) continue;
|
|
770
|
+
entries = await fs9.readdir(f.path);
|
|
712
771
|
} catch {
|
|
713
772
|
continue;
|
|
714
773
|
}
|
|
715
774
|
for (const name of entries) {
|
|
716
|
-
const p =
|
|
775
|
+
const p = path12.join(f.path, name);
|
|
717
776
|
let target;
|
|
718
777
|
try {
|
|
719
|
-
if (!(await
|
|
720
|
-
target = await
|
|
778
|
+
if (!(await fs9.lstat(p)).isSymbolicLink()) continue;
|
|
779
|
+
target = await fs9.realpath(p).catch(async () => path12.resolve(f.path, await fs9.readlink(p)));
|
|
721
780
|
} catch {
|
|
722
781
|
continue;
|
|
723
782
|
}
|
|
724
|
-
const owner = roots.find((r) => target === r.real || target.startsWith(r.real +
|
|
783
|
+
const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path12.sep));
|
|
725
784
|
if (owner) {
|
|
726
785
|
out.set(`${f.category}/${f.id}`, owner.id);
|
|
727
786
|
break;
|
|
@@ -766,7 +825,7 @@ async function ensurePackages(ctx, pkgs, opts = {}) {
|
|
|
766
825
|
async function maybeInstall(ctx, pkg, dir, opts, res) {
|
|
767
826
|
if (!pkg.install) return;
|
|
768
827
|
if (opts.yes) {
|
|
769
|
-
ctx.log(` $ (${
|
|
828
|
+
ctx.log(` $ (${path12.relative(ctx.adapter.root, dir) || "."}) ${pkg.install}`);
|
|
770
829
|
await runShell(pkg.install, dir);
|
|
771
830
|
} else {
|
|
772
831
|
res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
|
|
@@ -807,93 +866,296 @@ async function updatePackages(ctx, pkgs, opts = {}) {
|
|
|
807
866
|
return res;
|
|
808
867
|
}
|
|
809
868
|
|
|
810
|
-
// src/core/
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const exclude = opts.exclude ?? [];
|
|
814
|
-
const skipped = [];
|
|
815
|
-
if (await exists(manifestPath(ctx))) {
|
|
816
|
-
throw new Error(`\uC774\uBBF8 \uCD08\uAE30\uD654\uB41C \uCC3D\uACE0\uC785\uB2C8\uB2E4: ${manifestPath(ctx)}
|
|
817
|
-
\uB2E4\uB978 \uD658\uACBD\uC758 \uC124\uC815\uC744 \uC774 \uCC3D\uACE0\uB85C \uAC00\uC838\uC624\uB824\uBA74 'lshed restore' \uD6C4 'lshed save' \uB97C \uC4F0\uC138\uC694.`);
|
|
818
|
-
}
|
|
819
|
-
const all = await ctx.adapter.scan();
|
|
820
|
-
const m = { version: 1, agent: ctx.adapter.name, components: {}, packages: [], profiles: { [profileName]: {} } };
|
|
869
|
+
// src/core/discover.ts
|
|
870
|
+
var keyOf = (f) => `${f.category}/${f.id}`;
|
|
871
|
+
async function discover(ctx, exclude = []) {
|
|
821
872
|
const isExcluded = (cat, id) => exclude.some((e) => e === id || e === `${cat}/${id}`);
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
873
|
+
const excluded = [];
|
|
874
|
+
const items = [];
|
|
875
|
+
const all = await ctx.adapter.scan();
|
|
876
|
+
const pkgs = await detectPackages(ctx, all);
|
|
877
|
+
const kept = pkgs.filter((p) => {
|
|
878
|
+
if (isExcluded("packages", p.id)) {
|
|
879
|
+
excluded.push(`packages/${p.id}`);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
return true;
|
|
883
|
+
});
|
|
884
|
+
const generated = await detectGenerated(all, kept);
|
|
885
|
+
for (const p of kept) items.push({ kind: "package", category: "packages", id: p.id, pkg: p });
|
|
834
886
|
for (const cat of ctx.adapter.categories()) {
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
887
|
+
for (const f of all.filter((f2) => f2.category === cat.name)) {
|
|
888
|
+
if (pkgs.some((p) => p.path === f.path) || generated.has(keyOf(f))) continue;
|
|
889
|
+
if (isExcluded(f.category, f.id)) {
|
|
890
|
+
excluded.push(keyOf(f));
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
items.push({ kind: "component", category: cat.name, id: f.id, path: f.path, cat });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
for (const cat of ctx.adapter.entries()) {
|
|
897
|
+
const all2 = await cat.read();
|
|
898
|
+
for (const id of Object.keys(all2).sort()) {
|
|
899
|
+
if (isExcluded(cat.name, id)) {
|
|
900
|
+
excluded.push(`${cat.name}/${id}`);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (!/^[\w.-]+$/.test(id)) {
|
|
904
|
+
ctx.log(` ! ${cat.name}/${id}: \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790\uAC00 \uC788\uC5B4 \uAC74\uB108\uB700`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
items.push({ kind: "entry", category: cat.name, id, value: all2[id], cat });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return { items, generated, excluded };
|
|
911
|
+
}
|
|
912
|
+
function notInManifest(m, d) {
|
|
913
|
+
return d.items.filter(
|
|
914
|
+
(f) => f.kind === "package" ? !m.packages.some((p) => p.id === f.id) : !(m.components[f.category] ?? []).some((c) => c.id === f.id)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
function inManifestNotInProfile(m, profile, d) {
|
|
918
|
+
const p = m.profiles[profile] ?? {};
|
|
919
|
+
return d.items.filter((f) => !notInManifest(m, d).includes(f)).filter((f) => !(p[f.category] ?? []).includes(f.id)).map(keyOf);
|
|
920
|
+
}
|
|
921
|
+
function shortRev(rev) {
|
|
922
|
+
return /^[0-9a-f]{40}$/.test(rev) ? rev.slice(0, 7) : rev;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/core/ingest.ts
|
|
926
|
+
import path15 from "path";
|
|
927
|
+
import { isSeq, isMap } from "yaml";
|
|
928
|
+
|
|
929
|
+
// src/core/entries.ts
|
|
930
|
+
import { promises as fs10 } from "fs";
|
|
931
|
+
import path14 from "path";
|
|
932
|
+
var SECRET_KEY_RE = /key|token|secret|pass|auth|credential|cookie|session/i;
|
|
933
|
+
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
934
|
+
function placeholdersIn(v) {
|
|
935
|
+
const out = /* @__PURE__ */ new Set();
|
|
936
|
+
walk(v, (s) => {
|
|
937
|
+
for (const m of s.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
938
|
+
return s;
|
|
939
|
+
});
|
|
940
|
+
return [...out];
|
|
941
|
+
}
|
|
942
|
+
function walk(v, onString, keyPath = []) {
|
|
943
|
+
if (typeof v === "string") return onString(v, keyPath);
|
|
944
|
+
if (Array.isArray(v)) return v.map((x2, i) => walk(x2, onString, [...keyPath, String(i)]));
|
|
945
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x2]) => [k, walk(x2, onString, [...keyPath, k])]));
|
|
946
|
+
return v;
|
|
947
|
+
}
|
|
948
|
+
var envName = (...parts) => parts.join("_").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
949
|
+
function mask(id, entry, cat) {
|
|
950
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
951
|
+
const out = { ...entry };
|
|
952
|
+
for (const sk of cat.secretKeys) {
|
|
953
|
+
const sect = out[sk];
|
|
954
|
+
if (!sect || typeof sect !== "object" || Array.isArray(sect)) continue;
|
|
955
|
+
const masked = {};
|
|
956
|
+
for (const [k, v] of Object.entries(sect)) {
|
|
957
|
+
if (typeof v !== "string" || !SECRET_KEY_RE.test(k) || PLACEHOLDER_RE.test(v)) {
|
|
958
|
+
masked[k] = v;
|
|
959
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const name = sk === "env" ? envName(k) : envName(id, k);
|
|
963
|
+
const scheme = /^(\w+) \S+$/.exec(v);
|
|
964
|
+
masked[k] = scheme ? `${scheme[1]} \${${name}}` : `\${${name}}`;
|
|
965
|
+
}
|
|
966
|
+
out[sk] = masked;
|
|
967
|
+
}
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
function suspiciousStrings(entry) {
|
|
971
|
+
const out = [];
|
|
972
|
+
walk(entry, (s, kp) => {
|
|
973
|
+
if (/^(sk-|ghp_|github_pat_|xox[abp]-|AKIA|glpat-|ntn_|secret_)[A-Za-z0-9_-]{8,}/.test(s)) out.push(kp.join("."));
|
|
974
|
+
return s;
|
|
975
|
+
});
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
function expand(entry, env = process.env) {
|
|
979
|
+
const missing = /* @__PURE__ */ new Set();
|
|
980
|
+
const value = walk(
|
|
981
|
+
entry,
|
|
982
|
+
(s) => s.replace(PLACEHOLDER_RE, (whole, name, def) => {
|
|
983
|
+
if (env[name] !== void 0) return env[name];
|
|
984
|
+
if (def !== void 0) return def;
|
|
985
|
+
missing.add(name);
|
|
986
|
+
return whole;
|
|
987
|
+
})
|
|
988
|
+
);
|
|
989
|
+
return { value, missing: [...missing] };
|
|
990
|
+
}
|
|
991
|
+
function stringMatches(shed, local) {
|
|
992
|
+
if (shed === local) return true;
|
|
993
|
+
if (!PLACEHOLDER_RE.test(shed)) {
|
|
994
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
998
|
+
const re = "^" + shed.split(PLACEHOLDER_RE).map((part, i) => i % 3 === 0 ? part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : i % 3 === 1 ? ".+" : "").join("") + "$";
|
|
999
|
+
return new RegExp(re).test(local);
|
|
1000
|
+
}
|
|
1001
|
+
function matches2(shed, local) {
|
|
1002
|
+
if (typeof shed === "string" && typeof local === "string") return stringMatches(shed, local);
|
|
1003
|
+
if (Array.isArray(shed) && Array.isArray(local)) return shed.length === local.length && shed.every((x2, i) => matches2(x2, local[i]));
|
|
1004
|
+
if (shed && local && typeof shed === "object" && typeof local === "object" && !Array.isArray(shed) && !Array.isArray(local)) {
|
|
1005
|
+
const a = Object.keys(shed).sort(), b = Object.keys(local).sort();
|
|
1006
|
+
return a.length === b.length && a.every((k, i) => k === b[i] && matches2(shed[k], local[k]));
|
|
1007
|
+
}
|
|
1008
|
+
return shed === local;
|
|
1009
|
+
}
|
|
1010
|
+
function remask(id, local, shed, cat) {
|
|
1011
|
+
const keep = (l, s) => {
|
|
1012
|
+
if (typeof l === "string" && typeof s === "string" && stringMatches(s, l)) return s;
|
|
1013
|
+
if (Array.isArray(l) && Array.isArray(s)) return l.map((x2, i) => keep(x2, s[i]));
|
|
1014
|
+
if (l && s && typeof l === "object" && typeof s === "object" && !Array.isArray(l) && !Array.isArray(s)) {
|
|
1015
|
+
return Object.fromEntries(Object.entries(l).map(([k, x2]) => [k, keep(x2, s[k])]));
|
|
839
1016
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1017
|
+
return l;
|
|
1018
|
+
};
|
|
1019
|
+
return mask(id, shed === null ? local : keep(local, shed), cat);
|
|
1020
|
+
}
|
|
1021
|
+
function diffEntry(shed, local) {
|
|
1022
|
+
const out = [];
|
|
1023
|
+
const go = (s, l, kp) => {
|
|
1024
|
+
if (s === void 0) {
|
|
1025
|
+
out.push({ status: "A", file: kp || "(entry)" });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (l === void 0) {
|
|
1029
|
+
out.push({ status: "D", file: kp || "(entry)" });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const objs = s && l && typeof s === "object" && typeof l === "object" && !Array.isArray(s) && !Array.isArray(l);
|
|
1033
|
+
if (objs) {
|
|
1034
|
+
for (const k of (/* @__PURE__ */ new Set([...Object.keys(s), ...Object.keys(l)])).values()) go(s[k], l[k], kp ? `${kp}.${k}` : k);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (!matches2(s, l)) out.push({ status: "M", file: kp || "(entry)" });
|
|
1038
|
+
};
|
|
1039
|
+
go(shed, local, "");
|
|
1040
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
1041
|
+
}
|
|
1042
|
+
async function readEntryFile(p) {
|
|
1043
|
+
try {
|
|
1044
|
+
return JSON.parse(await fs10.readFile(p, "utf8"));
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
if (e.code === "ENOENT") return null;
|
|
1047
|
+
throw new Error(`${p}: JSON \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function writeEntryFile(p, v) {
|
|
1051
|
+
await fs10.mkdir(path14.dirname(p), { recursive: true });
|
|
1052
|
+
await fs10.writeFile(p, JSON.stringify(v, null, 2) + "\n");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/core/ingest.ts
|
|
1056
|
+
function seqAt(doc, p) {
|
|
1057
|
+
let node = doc.getIn(p);
|
|
1058
|
+
if (!isSeq(node)) {
|
|
1059
|
+
node = doc.createNode([]);
|
|
1060
|
+
doc.setIn(p, node);
|
|
1061
|
+
}
|
|
1062
|
+
return node;
|
|
1063
|
+
}
|
|
1064
|
+
function pushUnique(seq, value) {
|
|
1065
|
+
if (!seq.items.some((it) => (isMap(it) ? it.get("id") : String(it)) === value)) seq.add(value);
|
|
1066
|
+
}
|
|
1067
|
+
async function ingest(ctx, doc, profile, items, lock) {
|
|
1068
|
+
const out = { managed: [], copied: 0, packages: [] };
|
|
1069
|
+
for (const f of items) {
|
|
1070
|
+
if (f.kind === "package") {
|
|
1071
|
+
const p = f.pkg;
|
|
1072
|
+
const node = doc.createNode(p.into ? { id: p.id, source: p.source, into: p.into } : { id: p.id, source: p.source });
|
|
1073
|
+
if (p.into) node.comment = " install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)";
|
|
1074
|
+
const seq = seqAt(doc, [PACKAGES]);
|
|
1075
|
+
if (!seq.items.some((it) => isMap(it) && it.get("id") === p.id)) seq.add(node);
|
|
1076
|
+
pushUnique(seqAt(doc, ["profiles", profile, PACKAGES]), p.id);
|
|
1077
|
+
lock.packages[p.id] = { source: p.source, rev: p.rev };
|
|
1078
|
+
out.packages.push(p.id);
|
|
1079
|
+
ctx.log(` \u2261 package ${p.id} ${p.source} @${shortRev(p.rev)} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (f.kind === "component") {
|
|
1083
|
+
const dst = path15.join(ctx.shed, f.cat.root, f.cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
845
1084
|
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1085
|
+
ctx.log(` + ${f.category}/${f.id}`);
|
|
1086
|
+
} else {
|
|
1087
|
+
const masked = mask(f.id, f.value, f.cat);
|
|
1088
|
+
await writeEntryFile(path15.join(ctx.shed, f.category, `${f.id}.json`), masked);
|
|
1089
|
+
const vars = placeholdersIn(masked);
|
|
1090
|
+
ctx.log(` + ${f.category}/${f.id}${vars.length ? ` (\uC2DC\uD06C\uB9BF \u2192 ${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1091
|
+
for (const where of suspiciousStrings(masked)) ctx.log(` ! ${where} \uAC00 \uC2DC\uD06C\uB9BF\uCC98\uB7FC \uBCF4\uC785\uB2C8\uB2E4. \uCC3D\uACE0\uC758 ${f.category}/${f.id}.json \uC5D0\uC11C \${VAR} \uB85C \uBC14\uAFB8\uC138\uC694`);
|
|
851
1092
|
}
|
|
1093
|
+
const comps = seqAt(doc, ["components", f.category]);
|
|
1094
|
+
if (!comps.items.some((it) => isMap(it) && it.get("id") === f.id)) comps.add(doc.createNode({ id: f.id }));
|
|
1095
|
+
pushUnique(seqAt(doc, ["profiles", profile, f.category]), f.id);
|
|
1096
|
+
out.managed.push(targetRel(f.cat, f.id));
|
|
1097
|
+
out.copied++;
|
|
852
1098
|
}
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
function tidy(doc) {
|
|
1102
|
+
const pk = doc.get(PACKAGES);
|
|
1103
|
+
if (isSeq(pk) && !pk.items.length) doc.delete(PACKAGES);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/core/init.ts
|
|
1107
|
+
var MANIFEST_HEADER = "# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed\n";
|
|
1108
|
+
async function init(ctx, opts = {}) {
|
|
1109
|
+
const profileName = opts.profile ?? "default";
|
|
1110
|
+
if (await exists(manifestPath(ctx))) {
|
|
1111
|
+
throw new Error(`\uC774\uBBF8 \uCD08\uAE30\uD654\uB41C \uCC3D\uACE0\uC785\uB2C8\uB2E4: ${manifestPath(ctx)}
|
|
1112
|
+
\uC774 \uD658\uACBD\uC5D0 \uC0C8\uB85C \uC0DD\uAE34 \uAC83\uC744 \uAE30\uC874 \uCC3D\uACE0\uC5D0 \uB123\uC73C\uB824\uBA74 'lshed add' \uB97C, \uB2E4\uB978 \uD658\uACBD\uC758 \uC124\uC815\uC744 \uAC00\uC838\uC624\uB824\uBA74 'lshed restore' \uD6C4 'lshed save' \uB97C \uC4F0\uC138\uC694.`);
|
|
1113
|
+
}
|
|
1114
|
+
const d = await discover(ctx, opts.exclude);
|
|
1115
|
+
for (const [key, by] of d.generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
|
|
1116
|
+
for (const key of d.excluded) ctx.log(` - ${key} (--exclude)`);
|
|
1117
|
+
const exclude = opts.exclude?.length ? { exclude: [...opts.exclude] } : {};
|
|
1118
|
+
const doc = new YAML4.Document({ version: 1, agent: ctx.adapter.name, ...exclude, components: {}, packages: [], profiles: { [profileName]: {} } });
|
|
1119
|
+
const lock = { version: 1, packages: {} };
|
|
1120
|
+
await fs11.mkdir(ctx.shed, { recursive: true });
|
|
1121
|
+
const res = await ingest(ctx, doc, profileName, d.items, lock);
|
|
1122
|
+
const managed = [...res.managed];
|
|
1123
|
+
let copied = res.copied;
|
|
853
1124
|
const instr = instructionsFile(ctx);
|
|
854
1125
|
if (await exists(instr)) {
|
|
855
|
-
const text = await
|
|
1126
|
+
const text = await fs11.readFile(instr, "utf8");
|
|
856
1127
|
if (!isGenerated(text)) {
|
|
857
|
-
const dst =
|
|
858
|
-
await
|
|
859
|
-
await
|
|
1128
|
+
const dst = path16.join(ctx.shed, INSTRUCTIONS, "main.md");
|
|
1129
|
+
await fs11.mkdir(path16.dirname(dst), { recursive: true });
|
|
1130
|
+
await fs11.writeFile(dst, text);
|
|
860
1131
|
const fragRel = targetRel(INSTRUCTIONS, "main");
|
|
861
1132
|
await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
|
|
862
1133
|
managed.push(fragRel);
|
|
863
|
-
|
|
864
|
-
|
|
1134
|
+
doc.setIn(["components", INSTRUCTIONS], [{ id: "main" }]);
|
|
1135
|
+
doc.setIn(["profiles", profileName, INSTRUCTIONS], ["main"]);
|
|
865
1136
|
copied++;
|
|
866
|
-
ctx.log(` + ${INSTRUCTIONS}/main (${
|
|
1137
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path16.basename(instr)})`);
|
|
867
1138
|
}
|
|
868
1139
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
yamlText = yamlText.replace(` into: ${p.into}
|
|
874
|
-
`, ` into: ${p.into}
|
|
875
|
-
# install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
|
|
876
|
-
`);
|
|
877
|
-
}
|
|
878
|
-
await fs9.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
|
|
879
|
-
` + yamlText);
|
|
880
|
-
if (pkgs.length) {
|
|
881
|
-
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
|
|
882
|
-
}
|
|
1140
|
+
tidy(doc);
|
|
1141
|
+
const yamlText = MANIFEST_HEADER + doc.toString({ lineWidth: 0 });
|
|
1142
|
+
await fs11.writeFile(manifestPath(ctx), yamlText);
|
|
1143
|
+
if (res.packages.length) await writeLock(ctx.shed, lock);
|
|
883
1144
|
await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
884
1145
|
const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
|
|
885
|
-
if (
|
|
886
|
-
if (generated.size) parts.push(`\uC0DD\uC131\uBB3C ${generated.size}\uAC1C \uAC74\uB108\uB700`);
|
|
887
|
-
if (
|
|
1146
|
+
if (res.packages.length) parts.push(`\uD328\uD0A4\uC9C0 ${res.packages.length}\uAC1C`);
|
|
1147
|
+
if (d.generated.size) parts.push(`\uC0DD\uC131\uBB3C ${d.generated.size}\uAC1C \uAC74\uB108\uB700`);
|
|
1148
|
+
if (d.excluded.length) parts.push(`\uC81C\uC678 ${d.excluded.length}\uAC1C`);
|
|
888
1149
|
ctx.log(`
|
|
889
1150
|
${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
|
|
890
|
-
if (
|
|
891
|
-
|
|
1151
|
+
if (d.items.some((f) => f.kind === "package" && f.pkg.into)) ctx.log(`git \uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
|
|
1152
|
+
const manifest = parseManifest(yamlText);
|
|
1153
|
+
return { manifest, copied, skipped: d.excluded, packages: res.packages, generated: [...d.generated.keys()] };
|
|
892
1154
|
}
|
|
893
1155
|
|
|
894
1156
|
// src/core/restore.ts
|
|
895
|
-
import { promises as
|
|
896
|
-
import
|
|
1157
|
+
import { promises as fs12 } from "fs";
|
|
1158
|
+
import path17 from "path";
|
|
897
1159
|
async function restore(ctx, profileArg, opts = {}) {
|
|
898
1160
|
const backup = opts.backup ?? true;
|
|
899
1161
|
const state = await readState(ctx.adapter);
|
|
@@ -912,22 +1174,59 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
912
1174
|
const oldManaged = new Set(state?.managed ?? []);
|
|
913
1175
|
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
914
1176
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
915
|
-
const backupDir =
|
|
1177
|
+
const backupDir = path17.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
916
1178
|
const backedUp = [];
|
|
917
1179
|
const placed = [];
|
|
1180
|
+
const missingEnv = [];
|
|
1181
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
1182
|
+
const entriesOf = async (cat) => {
|
|
1183
|
+
if (!localEntries.has(cat.name)) localEntries.set(cat.name, await cat.read());
|
|
1184
|
+
return localEntries.get(cat.name);
|
|
1185
|
+
};
|
|
918
1186
|
async function backUp(rel) {
|
|
1187
|
+
const en = entryOf(ctx, rel);
|
|
1188
|
+
if (en) {
|
|
1189
|
+
const cur = (await entriesOf(en.cat))[en.id];
|
|
1190
|
+
if (cur === void 0) return;
|
|
1191
|
+
backedUp.push(rel);
|
|
1192
|
+
if (opts.dryRun || !backup) return;
|
|
1193
|
+
await writeEntryFile(path17.join(backupDir, en.cat.name, `${en.id}.json`), cur);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
919
1196
|
const from = abs(ctx, rel);
|
|
920
1197
|
if (!await exists(from)) return;
|
|
921
1198
|
backedUp.push(rel);
|
|
922
1199
|
if (opts.dryRun || !backup) return;
|
|
923
|
-
await copyTree(from,
|
|
1200
|
+
await copyTree(from, path17.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
|
|
924
1201
|
}
|
|
925
1202
|
for (const rel of toRemove) {
|
|
926
1203
|
ctx.log(` - ${rel}`);
|
|
927
1204
|
await backUp(rel);
|
|
928
|
-
if (
|
|
1205
|
+
if (opts.dryRun) continue;
|
|
1206
|
+
const en = entryOf(ctx, rel);
|
|
1207
|
+
if (en) await en.cat.write(en.id, null);
|
|
1208
|
+
else await removeTree(abs(ctx, rel));
|
|
929
1209
|
}
|
|
930
1210
|
for (const it of plan) {
|
|
1211
|
+
if (it.entry) {
|
|
1212
|
+
const shed = await readEntryFile(it.src);
|
|
1213
|
+
const local = (await entriesOf(it.entry))[it.id];
|
|
1214
|
+
const vars = placeholdersIn(shed);
|
|
1215
|
+
const ex = expand(shed);
|
|
1216
|
+
if (ex.missing.length) missingEnv.push({ rel: it.rel, vars: ex.missing });
|
|
1217
|
+
const value = it.entry.expandsEnv ? shed : ex.value;
|
|
1218
|
+
const same2 = local !== void 0 && matches2(shed, local);
|
|
1219
|
+
const mark2 = same2 ? "=" : local !== void 0 ? "~" : "+";
|
|
1220
|
+
ctx.log(` ${mark2} ${it.rel}${vars.length ? ` (${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1221
|
+
if (same2) {
|
|
1222
|
+
placed.push(it.rel);
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (local !== void 0) await backUp(it.rel);
|
|
1226
|
+
if (!opts.dryRun) await it.entry.write(it.id, value);
|
|
1227
|
+
placed.push(it.rel);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
931
1230
|
const target = abs(ctx, it.rel);
|
|
932
1231
|
const same = await hashTree(target, ignoreOf(ctx)) === await hashTree(it.src, ignoreOf(ctx));
|
|
933
1232
|
const mark = same ? "=" : await exists(target) ? "~" : "+";
|
|
@@ -943,14 +1242,14 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
943
1242
|
const instrPath = instructionsFile(ctx);
|
|
944
1243
|
if (fragments.length) {
|
|
945
1244
|
const contents = [];
|
|
946
|
-
for (const f of fragments) contents.push({ id: f.id, content: await
|
|
1245
|
+
for (const f of fragments) contents.push({ id: f.id, content: await fs12.readFile(f.src, "utf8") });
|
|
947
1246
|
const rendered = renderInstructions(ctx, profile, contents);
|
|
948
|
-
const existing = await exists(instrPath) ? await
|
|
1247
|
+
const existing = await exists(instrPath) ? await fs12.readFile(instrPath, "utf8") : null;
|
|
949
1248
|
if (existing !== rendered) {
|
|
950
1249
|
const mark = existing === null ? "+" : "~";
|
|
951
1250
|
ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
|
|
952
1251
|
if (existing !== null) await backUp(instrRel);
|
|
953
|
-
if (!opts.dryRun) await
|
|
1252
|
+
if (!opts.dryRun) await fs12.writeFile(instrPath, rendered);
|
|
954
1253
|
} else {
|
|
955
1254
|
ctx.log(` = ${instrRel}`);
|
|
956
1255
|
}
|
|
@@ -960,14 +1259,22 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
960
1259
|
ctx.log(`
|
|
961
1260
|
(dry-run) \uBCC0\uACBD \uC5C6\uC74C. \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}, \uBC31\uC5C5 \uC608\uC815 ${backedUp.length}`);
|
|
962
1261
|
reportPending(ctx, pkgRes);
|
|
963
|
-
|
|
1262
|
+
reportMissingEnv(ctx, missingEnv);
|
|
1263
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: null, missingEnv };
|
|
964
1264
|
}
|
|
965
1265
|
await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
966
1266
|
const bdir = backup && backedUp.length ? backupDir : null;
|
|
967
1267
|
ctx.log(`
|
|
968
1268
|
\uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.installed.length ? `, \uD328\uD0A4\uC9C0 \uC124\uCE58 ${pkgRes.installed.length}` : ""}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
|
|
969
1269
|
reportPending(ctx, pkgRes);
|
|
970
|
-
|
|
1270
|
+
reportMissingEnv(ctx, missingEnv);
|
|
1271
|
+
return { profile, placed, removed: toRemove, backedUp, backupDir: bdir, missingEnv };
|
|
1272
|
+
}
|
|
1273
|
+
function reportMissingEnv(ctx, missing) {
|
|
1274
|
+
if (!missing.length) return;
|
|
1275
|
+
ctx.log(`
|
|
1276
|
+
\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):`);
|
|
1277
|
+
for (const m of missing) ctx.log(` ${m.rel}: ${m.vars.join(", ")}`);
|
|
971
1278
|
}
|
|
972
1279
|
|
|
973
1280
|
// src/core/diff.ts
|
|
@@ -976,8 +1283,16 @@ async function diff(ctx) {
|
|
|
976
1283
|
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.");
|
|
977
1284
|
const m = await loadManifest(ctx);
|
|
978
1285
|
const out = [];
|
|
1286
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
979
1287
|
for (const item of planProfile(ctx, m, state.profile)) {
|
|
980
|
-
|
|
1288
|
+
let changes;
|
|
1289
|
+
if (item.entry) {
|
|
1290
|
+
if (!localEntries.has(item.category)) localEntries.set(item.category, await item.entry.read());
|
|
1291
|
+
const shed = await readEntryFile(item.src);
|
|
1292
|
+
changes = shed === null ? [{ status: "A", file: "(entry)" }] : diffEntry(shed, localEntries.get(item.category)[item.id]);
|
|
1293
|
+
} else {
|
|
1294
|
+
changes = await diffTrees(abs(ctx, item.rel), item.src, ignoreOf(ctx));
|
|
1295
|
+
}
|
|
981
1296
|
if (changes.length) out.push({ item, changes });
|
|
982
1297
|
}
|
|
983
1298
|
return out;
|
|
@@ -993,15 +1308,83 @@ function formatDiff(diffs) {
|
|
|
993
1308
|
return lines.join("\n");
|
|
994
1309
|
}
|
|
995
1310
|
|
|
1311
|
+
// src/core/add.ts
|
|
1312
|
+
import { promises as fs13 } from "fs";
|
|
1313
|
+
import YAML5 from "yaml";
|
|
1314
|
+
async function candidates(ctx, m, profile, d) {
|
|
1315
|
+
d ??= await discover(ctx, m.exclude);
|
|
1316
|
+
return { fresh: notInManifest(m, d), notInProfile: inManifestNotInProfile(m, profile, d), generated: d.generated };
|
|
1317
|
+
}
|
|
1318
|
+
async function add(ctx, keys = [], opts = {}) {
|
|
1319
|
+
const state = await readState(ctx.adapter);
|
|
1320
|
+
if (!state) throw new Error("\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 'lshed init' \uB610\uB294 'lshed restore <profile>' \uC744 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
1321
|
+
const m = await loadManifest(ctx);
|
|
1322
|
+
const c = await candidates(ctx, m, state.profile);
|
|
1323
|
+
if (!keys.length && !opts.all) {
|
|
1324
|
+
if (!c.fresh.length) ctx.log("\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uC0C8 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1325
|
+
else {
|
|
1326
|
+
ctx.log(`\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uD56D\uBAA9 ${c.fresh.length}\uAC1C (\uB123\uC73C\uB824\uBA74 lshed add <key...> \uB610\uB294 --all):`);
|
|
1327
|
+
for (const f of c.fresh) ctx.log(` ${f.kind === "package" ? "\u2261" : " "} ${keyOf(f)}${f.kind === "package" ? ` ${f.pkg.source}` : ""}`);
|
|
1328
|
+
}
|
|
1329
|
+
hint(ctx, c, state.profile);
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
let chosen = c.fresh;
|
|
1333
|
+
if (keys.length) {
|
|
1334
|
+
chosen = keys.map((raw) => {
|
|
1335
|
+
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
1336
|
+
const hits = c.fresh.filter((f) => f.id === b && (a === void 0 || f.category === a));
|
|
1337
|
+
if (!hits.length) {
|
|
1338
|
+
const known = (m.components[a ?? ""] ?? []).some((x2) => x2.id === b) || Object.values(m.components).some((cs) => cs.some((x2) => x2.id === b)) || m.packages.some((p) => p.id === b);
|
|
1339
|
+
throw new Error(known ? `"${raw}" \uB294 \uC774\uBBF8 \uCC3D\uACE0\uC5D0 \uC788\uC2B5\uB2C8\uB2E4. \uD504\uB85C\uD544\uC5D0 \uB123\uC73C\uB824\uBA74 lshed.yaml \uC758 profiles \uB97C \uACE0\uCE58\uC138\uC694.` : `"${raw}" \uB294 \uB85C\uCEEC\uC5D0\uC11C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. 'lshed add' \uB85C \uD6C4\uBCF4\uB97C \uBCF4\uC138\uC694.`);
|
|
1340
|
+
}
|
|
1341
|
+
if (hits.length > 1) throw new Error(`"${raw}" \uAC00 \uBAA8\uD638\uD569\uB2C8\uB2E4: ${hits.map(keyOf).join(", ")}`);
|
|
1342
|
+
return hits[0];
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (!chosen.length) {
|
|
1346
|
+
ctx.log("\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uC0C8 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1347
|
+
hint(ctx, c, state.profile);
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
const doc = YAML5.parseDocument(await fs13.readFile(manifestPath(ctx), "utf8"));
|
|
1351
|
+
const lock = await readLock(ctx.shed);
|
|
1352
|
+
const res = await ingest(ctx, doc, state.profile, chosen, lock);
|
|
1353
|
+
tidy(doc);
|
|
1354
|
+
await fs13.writeFile(manifestPath(ctx), doc.toString({ lineWidth: 0 }));
|
|
1355
|
+
if (res.packages.length) await writeLock(ctx.shed, lock);
|
|
1356
|
+
const managed = [.../* @__PURE__ */ new Set([...state.managed, ...res.managed])].sort();
|
|
1357
|
+
await writeState(ctx.adapter, { ...state, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1358
|
+
const added = chosen.map(keyOf);
|
|
1359
|
+
ctx.log(`
|
|
1360
|
+
${added.length}\uAC1C\uB97C \uCC3D\uACE0\uC5D0 \uB123\uACE0 \uD504\uB85C\uD544 "${state.profile}" \uC5D0 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC138\uC694: ${ctx.shed}`);
|
|
1361
|
+
if (res.packages.some((id) => chosen.find((f) => f.id === id && f.kind === "package" && f.pkg.into))) ctx.log(`git \uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
|
|
1362
|
+
hint(ctx, { ...c, fresh: c.fresh.filter((f) => !chosen.includes(f)) }, state.profile);
|
|
1363
|
+
return added;
|
|
1364
|
+
}
|
|
1365
|
+
function hint(ctx, c, profile) {
|
|
1366
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
1367
|
+
for (const by of c.generated.values()) byPkg.set(by, (byPkg.get(by) ?? 0) + 1);
|
|
1368
|
+
for (const [by, n] of byPkg) ctx.log(` \xB7 \uD328\uD0A4\uC9C0 ${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 ${n}\uAC1C\uB294 \uB2F4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
|
|
1369
|
+
if (c.notInProfile.length) ctx.log(`\uCC3D\uACE0\uC5D0\uB294 \uC788\uC9C0\uB9CC \uD504\uB85C\uD544 "${profile}" \uC774 \uC548 \uC4F0\uB294 \uAC83 ${c.notInProfile.length}\uAC1C: ${c.notInProfile.join(", ")} \u2192 lshed.yaml \uC758 profiles \uC5D0 \uCD94\uAC00`);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
996
1372
|
// src/core/status.ts
|
|
997
1373
|
async function status(ctx) {
|
|
998
1374
|
const state = await readState(ctx.adapter);
|
|
999
|
-
if (!state) return { state: null, drifted: [], packages: [] };
|
|
1375
|
+
if (!state) return { state: null, drifted: [], packages: [], missingEnv: [], fresh: [] };
|
|
1000
1376
|
const d = await diff(ctx);
|
|
1001
1377
|
const m = await loadManifest(ctx);
|
|
1002
1378
|
const lock = await readLock(ctx.shed);
|
|
1003
1379
|
const packages = await Promise.all(packagesOf(m, state.profile).map((p) => packageStatus(ctx, p, lock)));
|
|
1004
|
-
|
|
1380
|
+
const missingEnv = [];
|
|
1381
|
+
for (const it of planProfile(ctx, m, state.profile).filter((p) => p.entry)) {
|
|
1382
|
+
const shed = await readEntryFile(it.src);
|
|
1383
|
+
const missing = shed === null ? [] : expand(shed).missing;
|
|
1384
|
+
if (missing.length) missingEnv.push({ rel: it.rel, vars: missing });
|
|
1385
|
+
}
|
|
1386
|
+
const fresh = (await candidates(ctx, m, state.profile)).fresh.map(keyOf);
|
|
1387
|
+
return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages, missingEnv, fresh };
|
|
1005
1388
|
}
|
|
1006
1389
|
function formatStatus(s, adapterRoot) {
|
|
1007
1390
|
if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
|
|
@@ -1018,6 +1401,8 @@ function formatStatus(s, adapterRoot) {
|
|
|
1018
1401
|
const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${short2(p.rev)} (\uB77D \uC5C6\uC74C)` : p.rev === p.locked ? `${short2(p.rev)} = lock` : `${short2(p.rev)} \u2260 lock ${short2(p.locked)} \u2192 lshed update`;
|
|
1019
1402
|
lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
|
|
1020
1403
|
}
|
|
1404
|
+
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`);
|
|
1405
|
+
if (s.fresh.length) lines.push(`\uCC3D\uACE0 \uBC16 ${s.fresh.length}\uAC1C: ${s.fresh.join(", ")} \u2192 lshed add`);
|
|
1021
1406
|
return lines.join("\n");
|
|
1022
1407
|
}
|
|
1023
1408
|
|
|
@@ -1037,13 +1422,27 @@ async function save(ctx, ids = []) {
|
|
|
1037
1422
|
});
|
|
1038
1423
|
}
|
|
1039
1424
|
const saved = [];
|
|
1425
|
+
const localEntries = /* @__PURE__ */ new Map();
|
|
1040
1426
|
for (const it of plan) {
|
|
1041
|
-
const
|
|
1042
|
-
const src = effectiveSource(it.category, it.component, kind);
|
|
1427
|
+
const src = effectiveSource(it.category, it.component, kindOf(ctx, it.category));
|
|
1043
1428
|
if (!isSaveable(src)) {
|
|
1044
1429
|
ctx.log(` ! ${it.category}/${it.id}: \uC6D0\uACA9 \uCD9C\uCC98(${src})\uB294 save \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
1045
1430
|
continue;
|
|
1046
1431
|
}
|
|
1432
|
+
if (it.entry) {
|
|
1433
|
+
if (!localEntries.has(it.category)) localEntries.set(it.category, await it.entry.read());
|
|
1434
|
+
const local2 = localEntries.get(it.category)[it.id];
|
|
1435
|
+
if (local2 === void 0) {
|
|
1436
|
+
ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const shed = await readEntryFile(it.src);
|
|
1440
|
+
if (shed !== null && matches2(shed, local2)) continue;
|
|
1441
|
+
await writeEntryFile(it.src, remask(it.id, local2, shed, it.entry));
|
|
1442
|
+
saved.push(`${it.category}/${it.id}`);
|
|
1443
|
+
ctx.log(` \u2713 ${it.category}/${it.id} \u2192 \uCC3D\uACE0 (\uC2DC\uD06C\uB9BF\uC740 \uC790\uB9AC\uD45C\uC2DC\uC790\uB85C)`);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1047
1446
|
const local = abs(ctx, it.rel);
|
|
1048
1447
|
if (!await exists(local)) {
|
|
1049
1448
|
ctx.log(` ! ${it.category}/${it.id}: \uB85C\uCEEC\uC5D0 \uC5C6\uC74C (\uAC74\uB108\uB700)`);
|
|
@@ -1083,9 +1482,9 @@ function formatRows(rows, m) {
|
|
|
1083
1482
|
}
|
|
1084
1483
|
|
|
1085
1484
|
// src/core/remove.ts
|
|
1086
|
-
import { promises as
|
|
1087
|
-
import
|
|
1088
|
-
import
|
|
1485
|
+
import { promises as fs14 } from "fs";
|
|
1486
|
+
import path18 from "path";
|
|
1487
|
+
import YAML6, { isSeq as isSeq2, isMap as isMap2 } from "yaml";
|
|
1089
1488
|
function resolveKey(m, raw) {
|
|
1090
1489
|
const rows = listRows(m);
|
|
1091
1490
|
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
@@ -1099,13 +1498,13 @@ async function remove(ctx, raw) {
|
|
|
1099
1498
|
const { category, id } = resolveKey(m, raw);
|
|
1100
1499
|
const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
|
|
1101
1500
|
if (users.length) throw new Error(`${category}/${id} \uB294 \uD504\uB85C\uD544 ${users.join(", ")} \uC774 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uD504\uB85C\uD544\uC5D0\uC11C \uBE7C\uC138\uC694.`);
|
|
1102
|
-
const text = await
|
|
1103
|
-
const doc =
|
|
1501
|
+
const text = await fs14.readFile(manifestPath(ctx), "utf8");
|
|
1502
|
+
const doc = YAML6.parseDocument(text);
|
|
1104
1503
|
let deleted;
|
|
1105
1504
|
if (category === PACKAGES) {
|
|
1106
1505
|
const seq = doc.get(PACKAGES);
|
|
1107
|
-
if (!
|
|
1108
|
-
const idx = seq.items.findIndex((it) =>
|
|
1506
|
+
if (!isSeq2(seq)) throw new Error("packages \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4");
|
|
1507
|
+
const idx = seq.items.findIndex((it) => isMap2(it) && it.get("id") === id);
|
|
1109
1508
|
seq.delete(idx);
|
|
1110
1509
|
if (!seq.items.length) doc.delete(PACKAGES);
|
|
1111
1510
|
const lock = await readLock(ctx.shed);
|
|
@@ -1116,19 +1515,19 @@ async function remove(ctx, raw) {
|
|
|
1116
1515
|
ctx.log(` - package ${id} (\uB9E4\uB2C8\uD398\uC2A4\uD2B8\xB7\uB77D\uC5D0\uC11C \uC81C\uAC70. \uB85C\uCEEC clone \uC740 \uADF8\uB300\uB85C)`);
|
|
1117
1516
|
} else {
|
|
1118
1517
|
const seq = doc.getIn(["components", category]);
|
|
1119
|
-
if (!
|
|
1120
|
-
const idx = seq.items.findIndex((it) =>
|
|
1518
|
+
if (!isSeq2(seq)) throw new Error(`components.${category} \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
1519
|
+
const idx = seq.items.findIndex((it) => isMap2(it) && it.get("id") === id);
|
|
1121
1520
|
seq.delete(idx);
|
|
1122
1521
|
if (!seq.items.length) doc.deleteIn(["components", category]);
|
|
1123
1522
|
const src = sourcePath(ctx, category, findComponent(m, category, id));
|
|
1124
|
-
const inside = !
|
|
1523
|
+
const inside = !path18.relative(ctx.shed, src).startsWith("..");
|
|
1125
1524
|
if (inside && await exists(src)) {
|
|
1126
1525
|
await removeTree(src);
|
|
1127
1526
|
deleted = src;
|
|
1128
1527
|
}
|
|
1129
1528
|
ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
|
|
1130
1529
|
}
|
|
1131
|
-
await
|
|
1530
|
+
await fs14.writeFile(manifestPath(ctx), doc.toString());
|
|
1132
1531
|
return { category, id, deleted };
|
|
1133
1532
|
}
|
|
1134
1533
|
async function prune(ctx, opts = {}) {
|
|
@@ -1153,21 +1552,105 @@ ${removed.length}\uAC1C \uC81C\uAC70. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC13
|
|
|
1153
1552
|
return removed;
|
|
1154
1553
|
}
|
|
1155
1554
|
|
|
1555
|
+
// src/core/sync.ts
|
|
1556
|
+
import os2 from "os";
|
|
1557
|
+
async function sync(ctx, opts = {}) {
|
|
1558
|
+
const shed = ctx.shed;
|
|
1559
|
+
const push = opts.push ?? true;
|
|
1560
|
+
if (!await isRepo(shed)) {
|
|
1561
|
+
throw new Error(`\uCC3D\uACE0\uAC00 git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${shed}
|
|
1562
|
+
cd ${shed} && git init && git add -A && git commit -m "my harness"
|
|
1563
|
+
\uC6D0\uACA9\uC5D0 \uB450\uB824\uBA74: git remote add origin <url> && git push -u origin HEAD`);
|
|
1564
|
+
}
|
|
1565
|
+
const res = { committed: [], pulled: 0, pushed: false, unsaved: [] };
|
|
1566
|
+
if (await readState(ctx.adapter)) {
|
|
1567
|
+
try {
|
|
1568
|
+
res.unsaved = (await diff(ctx)).map((d) => `${d.item.category}/${d.item.id}`);
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
if (res.unsaved.length) ctx.log(` ! \uB85C\uCEEC \uD3B8\uC9D1 ${res.unsaved.length}\uAC1C\uAC00 \uCC3D\uACE0\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4: ${res.unsaved.join(", ")} \u2192 lshed save \uD6C4 \uB2E4\uC2DC sync`);
|
|
1572
|
+
}
|
|
1573
|
+
const dirty = (await git(["status", "--porcelain", "--untracked-files=all"], shed)).split("\n").filter(Boolean).map((l) => l.slice(3).trim());
|
|
1574
|
+
if (dirty.length) {
|
|
1575
|
+
const msg = opts.message ?? defaultMessage(dirty, (await readState(ctx.adapter))?.profile);
|
|
1576
|
+
ctx.log(` ${opts.dryRun ? "(dry-run) " : ""}commit ${dirty.length}\uAC1C: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ` \uC678 ${dirty.length - 5}` : ""}`);
|
|
1577
|
+
if (!opts.dryRun) {
|
|
1578
|
+
await git(["add", "-A"], shed);
|
|
1579
|
+
await git(["commit", "--quiet", "-m", msg], shed);
|
|
1580
|
+
}
|
|
1581
|
+
res.committed = dirty;
|
|
1582
|
+
} else {
|
|
1583
|
+
ctx.log(" = \uCC3D\uACE0\uC5D0 \uCEE4\uBC0B\uD560 \uBCC0\uACBD \uC5C6\uC74C");
|
|
1584
|
+
}
|
|
1585
|
+
const remote = await git(["remote", "get-url", "origin"], shed).catch(() => null);
|
|
1586
|
+
if (!remote) {
|
|
1587
|
+
ctx.log(" \xB7 origin \uC774 \uC5C6\uC5B4 pull/push \uB294 \uAC74\uB108\uB700 (git remote add origin <url>)");
|
|
1588
|
+
return res;
|
|
1589
|
+
}
|
|
1590
|
+
if (opts.dryRun) {
|
|
1591
|
+
ctx.log(` (dry-run) pull --rebase, push \u2192 ${remote}`);
|
|
1592
|
+
return res;
|
|
1593
|
+
}
|
|
1594
|
+
const before = await git(["rev-parse", "HEAD"], shed);
|
|
1595
|
+
const branch2 = await git(["rev-parse", "--abbrev-ref", "HEAD"], shed);
|
|
1596
|
+
const hasUpstream = await git(["rev-parse", "--abbrev-ref", "@{upstream}"], shed).then(() => true, () => false);
|
|
1597
|
+
if (hasUpstream) {
|
|
1598
|
+
try {
|
|
1599
|
+
await git(["pull", "--rebase", "--quiet"], shed);
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
await git(["rebase", "--abort"], shed).catch(() => {
|
|
1602
|
+
});
|
|
1603
|
+
throw new Error(`pull \uC911 \uCDA9\uB3CC\uC774 \uB098\uC11C \uB418\uB3CC\uB838\uC2B5\uB2C8\uB2E4. \uCC3D\uACE0\uC5D0\uC11C \uC9C1\uC811 \uD574\uACB0\uD558\uC138\uC694:
|
|
1604
|
+
cd ${shed} && git pull --rebase
|
|
1605
|
+
(${firstLine(e.message)})`);
|
|
1606
|
+
}
|
|
1607
|
+
const after = await git(["rev-parse", "HEAD"], shed);
|
|
1608
|
+
if (after !== before) {
|
|
1609
|
+
const n = Number(await git(["rev-list", "--count", `${before}..${after}`], shed).catch(() => "0"));
|
|
1610
|
+
res.pulled = Math.max(0, n - (res.committed.length ? 1 : 0));
|
|
1611
|
+
if (res.pulled) ctx.log(` \u2193 \uC6D0\uACA9 \uCEE4\uBC0B ${res.pulled}\uAC1C \uBC1B\uC74C`);
|
|
1612
|
+
}
|
|
1613
|
+
} else {
|
|
1614
|
+
ctx.log(` \xB7 \uBE0C\uB79C\uCE58 ${branch2} \uC5D0 upstream \uC774 \uC5C6\uC5B4 pull \uC740 \uAC74\uB108\uB700`);
|
|
1615
|
+
}
|
|
1616
|
+
if (push) {
|
|
1617
|
+
const ahead = hasUpstream ? Number(await git(["rev-list", "--count", "@{upstream}..HEAD"], shed)) : 1;
|
|
1618
|
+
if (ahead > 0) {
|
|
1619
|
+
await git(hasUpstream ? ["push", "--quiet"] : ["push", "--quiet", "-u", "origin", branch2], shed);
|
|
1620
|
+
res.pushed = true;
|
|
1621
|
+
ctx.log(` \u2191 push ${hasUpstream ? `${ahead}\uAC1C \uCEE4\uBC0B` : `(upstream \uC124\uC815: origin/${branch2})`}`);
|
|
1622
|
+
} else {
|
|
1623
|
+
ctx.log(" = \uC6D0\uACA9\uACFC \uAC19\uC74C");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (res.pulled) ctx.log(`
|
|
1627
|
+
\uCC3D\uACE0\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 \uAE30\uAE30\uC5D0 \uC801\uC6A9\uD558\uB824\uBA74: lshed restore`);
|
|
1628
|
+
return res;
|
|
1629
|
+
}
|
|
1630
|
+
function defaultMessage(paths, profile) {
|
|
1631
|
+
const parts = [...new Set(paths.map((p) => p.split("/").slice(0, 2).join("/")))];
|
|
1632
|
+
const head2 = parts.slice(0, 3).join(", ") + (parts.length > 3 ? ` +${parts.length - 3}` : "");
|
|
1633
|
+
return `lshed sync: ${head2}
|
|
1634
|
+
|
|
1635
|
+
${os2.hostname()}${profile ? ` \xB7 profile ${profile}` : ""}`;
|
|
1636
|
+
}
|
|
1637
|
+
var firstLine = (s) => s.split("\n").find((l) => l.trim() && !l.startsWith("Command failed")) ?? s;
|
|
1638
|
+
|
|
1156
1639
|
// src/cli.ts
|
|
1157
1640
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
1158
1641
|
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)");
|
|
1159
1642
|
function adapterFromOpts() {
|
|
1160
1643
|
const { root } = program.opts();
|
|
1161
|
-
return new ClaudeCodeAdapter(root ?
|
|
1644
|
+
return new ClaudeCodeAdapter(root ? path19.resolve(root) : void 0);
|
|
1162
1645
|
}
|
|
1163
1646
|
async function ctxFor(cmd) {
|
|
1164
1647
|
const adapter = adapterFromOpts();
|
|
1165
1648
|
const { shed: flag } = program.opts();
|
|
1166
1649
|
let shed = flag ?? process.env.LSHED_HOME;
|
|
1167
1650
|
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
1168
|
-
if (!shed && cmd === "init") shed =
|
|
1651
|
+
if (!shed && cmd === "init") shed = path19.join(os3.homedir(), "lshed");
|
|
1169
1652
|
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.");
|
|
1170
|
-
return { adapter, shed:
|
|
1653
|
+
return { adapter, shed: path19.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
|
|
1171
1654
|
}
|
|
1172
1655
|
async function run(fn) {
|
|
1173
1656
|
try {
|
|
@@ -1212,7 +1695,7 @@ program.command("status").description("show the applied profile, managed paths a
|
|
|
1212
1695
|
const adapter = adapterFromOpts();
|
|
1213
1696
|
const state = await readState(adapter);
|
|
1214
1697
|
if (!state) {
|
|
1215
|
-
console.log(formatStatus({ state: null, drifted: [], packages: [] }, adapter.root));
|
|
1698
|
+
console.log(formatStatus({ state: null, drifted: [], packages: [], missingEnv: [], fresh: [] }, adapter.root));
|
|
1216
1699
|
return;
|
|
1217
1700
|
}
|
|
1218
1701
|
const ctx = await ctxFor("other");
|
|
@@ -1226,6 +1709,14 @@ program.command("save [ids...]").description("copy local edits back into the she
|
|
|
1226
1709
|
const ctx = await ctxFor("other");
|
|
1227
1710
|
await save(ctx, ids);
|
|
1228
1711
|
}));
|
|
1712
|
+
program.command("add [keys...]").description("put things that appeared locally since init into the shed and the current profile (lists candidates without keys)").option("--all", "add every candidate").action((keys, o) => run(async () => {
|
|
1713
|
+
const ctx = await ctxFor("other");
|
|
1714
|
+
await add(ctx, keys, { all: o.all });
|
|
1715
|
+
}));
|
|
1716
|
+
program.command("sync").description("commit the shed, pull --rebase and push (the shed must be a git repo with origin)").option("-m, --message <msg>", "commit message (default: names the changed parts)").option("--no-push", "commit and pull only").option("--dry-run", "show what would be committed and pushed").action((o) => run(async () => {
|
|
1717
|
+
const ctx = await ctxFor("other");
|
|
1718
|
+
await sync(ctx, { message: o.message, push: o.push, dryRun: o.dryRun });
|
|
1719
|
+
}));
|
|
1229
1720
|
program.command("list").description("everything in the shed and which profiles use it").option("--unused", "only things no profile uses").action((o) => run(async () => {
|
|
1230
1721
|
const ctx = await ctxFor("other");
|
|
1231
1722
|
const m = await loadManifest(ctx);
|
|
@@ -1244,6 +1735,11 @@ program.command("scan").description("(debug) list components found in the agent
|
|
|
1244
1735
|
const adapter = adapterFromOpts();
|
|
1245
1736
|
const found = await adapter.scan();
|
|
1246
1737
|
for (const c of found) console.log(`${c.category}/${c.id} ${c.path}`);
|
|
1247
|
-
|
|
1738
|
+
let n = found.length;
|
|
1739
|
+
for (const e of adapter.entries()) for (const id of Object.keys(await e.read())) {
|
|
1740
|
+
console.log(`${e.name}/${id} (entry)`);
|
|
1741
|
+
n++;
|
|
1742
|
+
}
|
|
1743
|
+
console.error(`${n}\uAC1C \uBC1C\uACAC (root: ${adapter.root})`);
|
|
1248
1744
|
}));
|
|
1249
1745
|
program.parseAsync();
|