lshed 0.4.0 → 0.7.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 +25 -0
- package/README.md +247 -72
- package/dist/cli.js +462 -205
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import os4 from "os";
|
|
7
|
+
import path19 from "path";
|
|
8
8
|
|
|
9
9
|
// src/adapters/claude-code.ts
|
|
10
10
|
import { promises as fs4 } from "fs";
|
|
@@ -166,14 +166,62 @@ var pluginInstaller = {
|
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
// src/adapters/
|
|
170
|
-
import { promises as
|
|
171
|
-
import
|
|
169
|
+
// src/adapters/json-entries.ts
|
|
170
|
+
import { promises as fs2 } from "fs";
|
|
171
|
+
import path2 from "path";
|
|
172
|
+
var JsonEntries = class {
|
|
173
|
+
constructor(spec) {
|
|
174
|
+
this.spec = spec;
|
|
175
|
+
this.name = spec.name;
|
|
176
|
+
this.secretKeys = spec.secretKeys;
|
|
177
|
+
this.secretRootIds = spec.secretRootIds;
|
|
178
|
+
this.expandsEnv = spec.expandsEnv;
|
|
179
|
+
}
|
|
180
|
+
spec;
|
|
181
|
+
kind = "entry";
|
|
182
|
+
name;
|
|
183
|
+
secretKeys;
|
|
184
|
+
secretRootIds;
|
|
185
|
+
expandsEnv;
|
|
186
|
+
file() {
|
|
187
|
+
return this.spec.file();
|
|
188
|
+
}
|
|
189
|
+
async load() {
|
|
190
|
+
const p = await this.file();
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(await fs2.readFile(p, "utf8"));
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (e.code === "ENOENT") return {};
|
|
195
|
+
throw new Error(`${p} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
section(all) {
|
|
199
|
+
const s = this.spec.under ? all[this.spec.under] : all;
|
|
200
|
+
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
201
|
+
}
|
|
202
|
+
async read() {
|
|
203
|
+
const out = { ...this.section(await this.load()) };
|
|
204
|
+
for (const k of this.spec.skip ?? []) delete out[k];
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
async write(id, value) {
|
|
208
|
+
const p = await this.file();
|
|
209
|
+
const all = await this.load();
|
|
210
|
+
const sect = { ...this.section(all) };
|
|
211
|
+
if (value === null) delete sect[id];
|
|
212
|
+
else sect[id] = value;
|
|
213
|
+
const next = this.spec.under ? { ...all, [this.spec.under]: sect } : sect;
|
|
214
|
+
await fs2.mkdir(path2.dirname(p), { recursive: true });
|
|
215
|
+
const tmp = `${p}.lshed-${process.pid}.tmp`;
|
|
216
|
+
await fs2.writeFile(tmp, JSON.stringify(next, null, 2) + "\n");
|
|
217
|
+
await fs2.rename(tmp, p);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
172
220
|
|
|
173
221
|
// src/fsutil.ts
|
|
174
|
-
import { promises as
|
|
222
|
+
import { promises as fs3 } from "fs";
|
|
175
223
|
import { createHash } from "crypto";
|
|
176
|
-
import
|
|
224
|
+
import path3 from "path";
|
|
177
225
|
|
|
178
226
|
// src/ignore.ts
|
|
179
227
|
var DEFAULT_IGNORE = [
|
|
@@ -198,7 +246,7 @@ function isIgnored(rel, patterns) {
|
|
|
198
246
|
// src/fsutil.ts
|
|
199
247
|
async function exists(p) {
|
|
200
248
|
try {
|
|
201
|
-
await
|
|
249
|
+
await fs3.access(p);
|
|
202
250
|
return true;
|
|
203
251
|
} catch {
|
|
204
252
|
return false;
|
|
@@ -206,7 +254,7 @@ async function exists(p) {
|
|
|
206
254
|
}
|
|
207
255
|
async function isDir(p) {
|
|
208
256
|
try {
|
|
209
|
-
return (await
|
|
257
|
+
return (await fs3.stat(p)).isDirectory();
|
|
210
258
|
} catch {
|
|
211
259
|
return false;
|
|
212
260
|
}
|
|
@@ -216,14 +264,14 @@ async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
|
216
264
|
if (!await isDir(root)) return [""];
|
|
217
265
|
const out = [];
|
|
218
266
|
async function walk2(dir, rel) {
|
|
219
|
-
const entries = await
|
|
267
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
220
268
|
for (const e of entries) {
|
|
221
269
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
222
270
|
if (isIgnored(r, ignore)) continue;
|
|
223
|
-
const full =
|
|
271
|
+
const full = path3.join(dir, e.name);
|
|
224
272
|
let st;
|
|
225
273
|
try {
|
|
226
|
-
st = await
|
|
274
|
+
st = await fs3.stat(full);
|
|
227
275
|
} catch {
|
|
228
276
|
continue;
|
|
229
277
|
}
|
|
@@ -235,28 +283,28 @@ async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
|
235
283
|
return out.sort();
|
|
236
284
|
}
|
|
237
285
|
async function hashFile(p) {
|
|
238
|
-
return createHash("sha256").update(await
|
|
286
|
+
return createHash("sha256").update(await fs3.readFile(p)).digest("hex");
|
|
239
287
|
}
|
|
240
288
|
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
241
289
|
if (!await exists(root)) return null;
|
|
242
290
|
const h = createHash("sha256");
|
|
243
291
|
for (const rel of await listFiles(root, ignore)) {
|
|
244
|
-
h.update(rel).update("\0").update(await
|
|
292
|
+
h.update(rel).update("\0").update(await fs3.readFile(rel ? path3.join(root, rel) : root)).update("\0");
|
|
245
293
|
}
|
|
246
294
|
return h.digest("hex");
|
|
247
295
|
}
|
|
248
296
|
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
249
|
-
await
|
|
250
|
-
await
|
|
251
|
-
const srcRoot =
|
|
252
|
-
await
|
|
297
|
+
await fs3.rm(dst, { recursive: true, force: true });
|
|
298
|
+
await fs3.mkdir(path3.dirname(dst), { recursive: true });
|
|
299
|
+
const srcRoot = path3.resolve(src);
|
|
300
|
+
await fs3.cp(src, dst, {
|
|
253
301
|
recursive: true,
|
|
254
302
|
dereference: true,
|
|
255
303
|
filter: async (from) => {
|
|
256
|
-
const rel =
|
|
304
|
+
const rel = path3.relative(srcRoot, path3.resolve(from)).split(path3.sep).join("/");
|
|
257
305
|
if (isIgnored(rel, ignore)) return false;
|
|
258
306
|
try {
|
|
259
|
-
if ((await
|
|
307
|
+
if ((await fs3.lstat(from)).isSymbolicLink()) await fs3.stat(from);
|
|
260
308
|
} catch {
|
|
261
309
|
return false;
|
|
262
310
|
}
|
|
@@ -265,15 +313,15 @@ async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
|
265
313
|
});
|
|
266
314
|
}
|
|
267
315
|
async function removeTree(p) {
|
|
268
|
-
await
|
|
316
|
+
await fs3.rm(p, { recursive: true, force: true });
|
|
269
317
|
}
|
|
270
318
|
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
271
319
|
const l = new Set(await listFiles(local, ignore));
|
|
272
320
|
const s = new Set(await listFiles(shed, ignore));
|
|
273
321
|
const out = [];
|
|
274
322
|
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
275
|
-
const lp = f ?
|
|
276
|
-
const sp = f ?
|
|
323
|
+
const lp = f ? path3.join(local, f) : local;
|
|
324
|
+
const sp = f ? path3.join(shed, f) : shed;
|
|
277
325
|
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
278
326
|
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
279
327
|
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
@@ -281,48 +329,6 @@ async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
|
281
329
|
return out;
|
|
282
330
|
}
|
|
283
331
|
|
|
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
332
|
// src/adapters/claude-code.ts
|
|
327
333
|
var CATEGORIES = [
|
|
328
334
|
{ name: "skills", root: "skills", kind: "dir" },
|
|
@@ -333,13 +339,42 @@ var CATEGORIES = [
|
|
|
333
339
|
var ClaudeCodeAdapter = class {
|
|
334
340
|
name = "claude-code";
|
|
335
341
|
root;
|
|
336
|
-
|
|
342
|
+
entryCats;
|
|
337
343
|
constructor(root) {
|
|
338
344
|
this.root = root ?? process.env.CLAUDE_CONFIG_DIR ?? path4.join(os.homedir(), ".claude");
|
|
339
|
-
|
|
345
|
+
const root_ = this.root;
|
|
346
|
+
this.entryCats = [
|
|
347
|
+
/**
|
|
348
|
+
* 사용자 범위 MCP (§7.4): ~/.claude 의 형제 ~/.claude.json 의 mcpServers. CLAUDE_CONFIG_DIR 처럼 루트 안에 있으면 그것.
|
|
349
|
+
* Claude Code 가 ${VAR} 를 모든 범위에서 스스로 확장하므로 자리표시자를 그대로 둔다.
|
|
350
|
+
*/
|
|
351
|
+
new JsonEntries({
|
|
352
|
+
name: "mcp",
|
|
353
|
+
file: async () => {
|
|
354
|
+
const inside = path4.join(root_, ".claude.json");
|
|
355
|
+
return await exists(inside) ? inside : `${root_}.json`;
|
|
356
|
+
},
|
|
357
|
+
under: "mcpServers",
|
|
358
|
+
secretKeys: ["env", "headers"],
|
|
359
|
+
expandsEnv: true
|
|
360
|
+
}),
|
|
361
|
+
/**
|
|
362
|
+
* settings.json (§7.6): 최상위 키 하나 = 항목 하나 (hooks, permissions, env, model, ...).
|
|
363
|
+
* 병합하지 않는다. 키를 통째로 소유하고, 로컬 편집은 diff/save 로 되가져온다.
|
|
364
|
+
* enabledPlugins 는 플러그인 설치기가 만드는 상태라 담지 않는다. ${VAR} 는 Claude Code 가 안 채우므로 restore 가 채운다.
|
|
365
|
+
*/
|
|
366
|
+
new JsonEntries({
|
|
367
|
+
name: "settings",
|
|
368
|
+
file: async () => path4.join(root_, "settings.json"),
|
|
369
|
+
secretKeys: [],
|
|
370
|
+
secretRootIds: ["env"],
|
|
371
|
+
expandsEnv: false,
|
|
372
|
+
skip: ["enabledPlugins"]
|
|
373
|
+
})
|
|
374
|
+
];
|
|
340
375
|
}
|
|
341
376
|
entries() {
|
|
342
|
-
return
|
|
377
|
+
return this.entryCats;
|
|
343
378
|
}
|
|
344
379
|
categories() {
|
|
345
380
|
return CATEGORIES;
|
|
@@ -493,6 +528,8 @@ var ManifestSchema = z.object({
|
|
|
493
528
|
agent: z.string().default("claude-code"),
|
|
494
529
|
/** 창고에 담지 않을 이름들. 기본값(DEFAULT_IGNORE)에 더해진다. */
|
|
495
530
|
ignore: z.array(z.string()).optional(),
|
|
531
|
+
/** 로컬에 있어도 창고에 넣지 않을 부품 ("id" 또는 "category/id"). init --exclude 가 적고 add/status 가 따른다. */
|
|
532
|
+
exclude: z.array(z.string()).optional(),
|
|
496
533
|
components: ComponentsSchema.default({}),
|
|
497
534
|
packages: z.array(PackageSchema).default([]),
|
|
498
535
|
profiles: z.record(z.string(), ProfileSchema).default({})
|
|
@@ -558,12 +595,6 @@ function effectiveSource(category, c, kind = "dir") {
|
|
|
558
595
|
const ext = kind === "file" ? ".md" : kind === "entry" ? ".json" : "";
|
|
559
596
|
return c.source ?? `file:./${category}/${c.id}${ext}`;
|
|
560
597
|
}
|
|
561
|
-
function stringifyManifest(m) {
|
|
562
|
-
const out = { ...m };
|
|
563
|
-
if (!m.packages.length) delete out.packages;
|
|
564
|
-
if (!m.ignore?.length) delete out.ignore;
|
|
565
|
-
return YAML.stringify(out, { lineWidth: 0 });
|
|
566
|
-
}
|
|
567
598
|
function packagesOf(m, profile) {
|
|
568
599
|
const ids = m.profiles[profile]?.[PACKAGES] ?? [];
|
|
569
600
|
return ids.map((id) => m.packages.find((p) => p.id === id));
|
|
@@ -701,7 +732,8 @@ function abs(ctx, rel) {
|
|
|
701
732
|
|
|
702
733
|
// src/core/init.ts
|
|
703
734
|
import { promises as fs11 } from "fs";
|
|
704
|
-
import
|
|
735
|
+
import path16 from "path";
|
|
736
|
+
import YAML4 from "yaml";
|
|
705
737
|
|
|
706
738
|
// src/core/instructions.ts
|
|
707
739
|
import path10 from "path";
|
|
@@ -725,10 +757,6 @@ ${f.content.trimEnd()}
|
|
|
725
757
|
`).join("\n");
|
|
726
758
|
}
|
|
727
759
|
|
|
728
|
-
// src/core/packages.ts
|
|
729
|
-
import { promises as fs9 } from "fs";
|
|
730
|
-
import path12 from "path";
|
|
731
|
-
|
|
732
760
|
// src/lock.ts
|
|
733
761
|
import { promises as fs8 } from "fs";
|
|
734
762
|
import path11 from "path";
|
|
@@ -753,7 +781,12 @@ async function writeLock(shed, lock) {
|
|
|
753
781
|
await fs8.writeFile(path11.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
|
|
754
782
|
}
|
|
755
783
|
|
|
784
|
+
// src/core/discover.ts
|
|
785
|
+
import path13 from "path";
|
|
786
|
+
|
|
756
787
|
// src/core/packages.ts
|
|
788
|
+
import { promises as fs9 } from "fs";
|
|
789
|
+
import path12 from "path";
|
|
757
790
|
async function detectPackages(ctx, found) {
|
|
758
791
|
const out = [];
|
|
759
792
|
for (const inst of installersFor(ctx)) out.push(...await inst.detect(ctx, found));
|
|
@@ -868,10 +901,76 @@ async function updatePackages(ctx, pkgs, opts = {}) {
|
|
|
868
901
|
return res;
|
|
869
902
|
}
|
|
870
903
|
|
|
904
|
+
// src/core/discover.ts
|
|
905
|
+
var keyOf = (f) => `${f.category}/${f.id}`;
|
|
906
|
+
async function discover(ctx, exclude = []) {
|
|
907
|
+
const isExcluded = (cat, id) => exclude.some((e) => e === id || e === `${cat}/${id}`);
|
|
908
|
+
const excluded = [];
|
|
909
|
+
const items = [];
|
|
910
|
+
const all = await ctx.adapter.scan();
|
|
911
|
+
const pkgs = await detectPackages(ctx, all);
|
|
912
|
+
const kept = pkgs.filter((p) => {
|
|
913
|
+
if (isExcluded("packages", p.id)) {
|
|
914
|
+
excluded.push(`packages/${p.id}`);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
});
|
|
919
|
+
const generated = await detectGenerated(all, kept);
|
|
920
|
+
for (const p of kept) items.push({ kind: "package", category: "packages", id: p.id, pkg: p });
|
|
921
|
+
for (const cat of ctx.adapter.categories()) {
|
|
922
|
+
for (const f of all.filter((f2) => f2.category === cat.name)) {
|
|
923
|
+
if (pkgs.some((p) => p.path === f.path) || generated.has(keyOf(f))) continue;
|
|
924
|
+
if (isExcluded(f.category, f.id)) {
|
|
925
|
+
excluded.push(keyOf(f));
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
items.push({ kind: "component", category: cat.name, id: f.id, path: f.path, cat });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
for (const cat of ctx.adapter.entries()) {
|
|
932
|
+
const all2 = await cat.read();
|
|
933
|
+
for (const id of Object.keys(all2).sort()) {
|
|
934
|
+
if (isExcluded(cat.name, id)) {
|
|
935
|
+
excluded.push(`${cat.name}/${id}`);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (!/^[\w.-]+$/.test(id)) {
|
|
939
|
+
ctx.log(` ! ${cat.name}/${id}: \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790\uAC00 \uC788\uC5B4 \uAC74\uB108\uB700`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const owner = kept.find((p) => p.path && JSON.stringify(all2[id]).includes(p.path));
|
|
943
|
+
const warn = owner ? `\uD328\uD0A4\uC9C0 ${owner.id} \uC548\uC744 \uAC00\uB9AC\uD0B5\uB2C8\uB2E4. \uADF8 \uC124\uCE58\uAC00 \uB9CC\uB4E0 \uAC83\uC774\uBA74 exclude \uD558\uC138\uC694: ${cat.name}/${id}` : void 0;
|
|
944
|
+
items.push({ kind: "entry", category: cat.name, id, value: all2[id], cat, warn });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return { items, generated, excluded };
|
|
948
|
+
}
|
|
949
|
+
function notInManifest(m, d) {
|
|
950
|
+
return d.items.filter(
|
|
951
|
+
(f) => f.kind === "package" ? !m.packages.some((p) => p.id === f.id) : !(m.components[f.category] ?? []).some((c) => c.id === f.id)
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
function inManifestNotInProfile(m, profile, d) {
|
|
955
|
+
const p = m.profiles[profile] ?? {};
|
|
956
|
+
return d.items.filter((f) => !notInManifest(m, d).includes(f)).filter((f) => !(p[f.category] ?? []).includes(f.id)).map(keyOf);
|
|
957
|
+
}
|
|
958
|
+
function shortRev(rev) {
|
|
959
|
+
return /^[0-9a-f]{40}$/.test(rev) ? rev.slice(0, 7) : rev;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/core/ingest.ts
|
|
963
|
+
import path15 from "path";
|
|
964
|
+
import { isSeq, isMap } from "yaml";
|
|
965
|
+
|
|
871
966
|
// src/core/entries.ts
|
|
872
967
|
import { promises as fs10 } from "fs";
|
|
873
|
-
import
|
|
874
|
-
|
|
968
|
+
import os2 from "os";
|
|
969
|
+
import path14 from "path";
|
|
970
|
+
var SECRET_WORDS = /* @__PURE__ */ new Set(["key", "apikey", "token", "secret", "password", "passwd", "auth", "authorization", "credential", "credentials", "cookie", "session"]);
|
|
971
|
+
function isSecretKey(k) {
|
|
972
|
+
return k.split(/[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).some((w) => SECRET_WORDS.has(w.toLowerCase()));
|
|
973
|
+
}
|
|
875
974
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
876
975
|
function placeholdersIn(v) {
|
|
877
976
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -888,27 +987,39 @@ function walk(v, onString, keyPath = []) {
|
|
|
888
987
|
return v;
|
|
889
988
|
}
|
|
890
989
|
var envName = (...parts) => parts.join("_").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
891
|
-
function mask(id, entry, cat) {
|
|
990
|
+
function mask(id, entry, cat, home = os2.homedir()) {
|
|
991
|
+
entry = portable(entry, home);
|
|
892
992
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
893
|
-
const
|
|
894
|
-
for (const sk of cat.secretKeys) {
|
|
895
|
-
const sect = out[sk];
|
|
896
|
-
if (!sect || typeof sect !== "object" || Array.isArray(sect)) continue;
|
|
993
|
+
const maskSection = (sect, envLike) => {
|
|
897
994
|
const masked = {};
|
|
898
995
|
for (const [k, v] of Object.entries(sect)) {
|
|
899
|
-
if (typeof v !== "string" || !
|
|
996
|
+
if (typeof v !== "string" || !isSecretKey(k) || PLACEHOLDER_RE.test(v)) {
|
|
900
997
|
masked[k] = v;
|
|
901
998
|
PLACEHOLDER_RE.lastIndex = 0;
|
|
902
999
|
continue;
|
|
903
1000
|
}
|
|
904
|
-
const name =
|
|
1001
|
+
const name = envLike ? envName(k) : envName(id, k);
|
|
905
1002
|
const scheme = /^(\w+) \S+$/.exec(v);
|
|
906
1003
|
masked[k] = scheme ? `${scheme[1]} \${${name}}` : `\${${name}}`;
|
|
907
1004
|
}
|
|
908
|
-
|
|
1005
|
+
return masked;
|
|
1006
|
+
};
|
|
1007
|
+
if (cat.secretRootIds?.includes(id)) return maskSection(entry, true);
|
|
1008
|
+
const out = { ...entry };
|
|
1009
|
+
for (const sk of cat.secretKeys) {
|
|
1010
|
+
const sect = out[sk];
|
|
1011
|
+
if (!sect || typeof sect !== "object" || Array.isArray(sect)) continue;
|
|
1012
|
+
out[sk] = maskSection(sect, sk === "env");
|
|
909
1013
|
}
|
|
910
1014
|
return out;
|
|
911
1015
|
}
|
|
1016
|
+
function portable(entry, home = os2.homedir()) {
|
|
1017
|
+
if (!home || home === "/") return entry;
|
|
1018
|
+
return walk(entry, (s) => s === home || s.startsWith(home + "/") ? "${HOME}" + s.slice(home.length) : s);
|
|
1019
|
+
}
|
|
1020
|
+
function envWithHome(env = process.env) {
|
|
1021
|
+
return { HOME: os2.homedir(), ...env };
|
|
1022
|
+
}
|
|
912
1023
|
function suspiciousStrings(entry) {
|
|
913
1024
|
const out = [];
|
|
914
1025
|
walk(entry, (s, kp) => {
|
|
@@ -949,7 +1060,7 @@ function matches2(shed, local) {
|
|
|
949
1060
|
}
|
|
950
1061
|
return shed === local;
|
|
951
1062
|
}
|
|
952
|
-
function remask(id, local, shed, cat) {
|
|
1063
|
+
function remask(id, local, shed, cat, home = os2.homedir()) {
|
|
953
1064
|
const keep = (l, s) => {
|
|
954
1065
|
if (typeof l === "string" && typeof s === "string" && stringMatches(s, l)) return s;
|
|
955
1066
|
if (Array.isArray(l) && Array.isArray(s)) return l.map((x2, i) => keep(x2, s[i]));
|
|
@@ -958,7 +1069,7 @@ function remask(id, local, shed, cat) {
|
|
|
958
1069
|
}
|
|
959
1070
|
return l;
|
|
960
1071
|
};
|
|
961
|
-
return mask(id, shed === null ? local : keep(local, shed), cat);
|
|
1072
|
+
return mask(id, shed === null ? local : keep(local, portable(shed, home)), cat, home);
|
|
962
1073
|
}
|
|
963
1074
|
function diffEntry(shed, local) {
|
|
964
1075
|
const out = [];
|
|
@@ -990,124 +1101,115 @@ async function readEntryFile(p) {
|
|
|
990
1101
|
}
|
|
991
1102
|
}
|
|
992
1103
|
async function writeEntryFile(p, v) {
|
|
993
|
-
await fs10.mkdir(
|
|
1104
|
+
await fs10.mkdir(path14.dirname(p), { recursive: true });
|
|
994
1105
|
await fs10.writeFile(p, JSON.stringify(v, null, 2) + "\n");
|
|
995
1106
|
}
|
|
996
1107
|
|
|
1108
|
+
// src/core/ingest.ts
|
|
1109
|
+
function seqAt(doc, p) {
|
|
1110
|
+
let node = doc.getIn(p);
|
|
1111
|
+
if (!isSeq(node)) {
|
|
1112
|
+
node = doc.createNode([]);
|
|
1113
|
+
doc.setIn(p, node);
|
|
1114
|
+
}
|
|
1115
|
+
return node;
|
|
1116
|
+
}
|
|
1117
|
+
function pushUnique(seq, value) {
|
|
1118
|
+
if (!seq.items.some((it) => (isMap(it) ? it.get("id") : String(it)) === value)) seq.add(value);
|
|
1119
|
+
}
|
|
1120
|
+
async function ingest(ctx, doc, profile, items, lock) {
|
|
1121
|
+
const out = { managed: [], copied: 0, packages: [] };
|
|
1122
|
+
for (const f of items) {
|
|
1123
|
+
if (f.kind === "package") {
|
|
1124
|
+
const p = f.pkg;
|
|
1125
|
+
const node = doc.createNode(p.into ? { id: p.id, source: p.source, into: p.into } : { id: p.id, source: p.source });
|
|
1126
|
+
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)";
|
|
1127
|
+
const seq = seqAt(doc, [PACKAGES]);
|
|
1128
|
+
if (!seq.items.some((it) => isMap(it) && it.get("id") === p.id)) seq.add(node);
|
|
1129
|
+
pushUnique(seqAt(doc, ["profiles", profile, PACKAGES]), p.id);
|
|
1130
|
+
lock.packages[p.id] = { source: p.source, rev: p.rev };
|
|
1131
|
+
out.packages.push(p.id);
|
|
1132
|
+
ctx.log(` \u2261 package ${p.id} ${p.source} @${shortRev(p.rev)} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (f.kind === "component") {
|
|
1136
|
+
const dst = path15.join(ctx.shed, f.cat.root, f.cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
1137
|
+
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
1138
|
+
ctx.log(` + ${f.category}/${f.id}`);
|
|
1139
|
+
} else {
|
|
1140
|
+
const masked = mask(f.id, f.value, f.cat);
|
|
1141
|
+
await writeEntryFile(path15.join(ctx.shed, f.category, `${f.id}.json`), masked);
|
|
1142
|
+
const vars = placeholdersIn(masked).filter((v) => v !== "HOME");
|
|
1143
|
+
ctx.log(` + ${f.category}/${f.id}${vars.length ? ` (\uC2DC\uD06C\uB9BF \u2192 ${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1144
|
+
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`);
|
|
1145
|
+
if (f.warn) ctx.log(` ! ${f.warn}`);
|
|
1146
|
+
}
|
|
1147
|
+
const comps = seqAt(doc, ["components", f.category]);
|
|
1148
|
+
if (!comps.items.some((it) => isMap(it) && it.get("id") === f.id)) comps.add(doc.createNode({ id: f.id }));
|
|
1149
|
+
pushUnique(seqAt(doc, ["profiles", profile, f.category]), f.id);
|
|
1150
|
+
out.managed.push(targetRel(f.cat, f.id));
|
|
1151
|
+
out.copied++;
|
|
1152
|
+
}
|
|
1153
|
+
return out;
|
|
1154
|
+
}
|
|
1155
|
+
function tidy(doc) {
|
|
1156
|
+
const pk = doc.get(PACKAGES);
|
|
1157
|
+
if (isSeq(pk) && !pk.items.length) doc.delete(PACKAGES);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
997
1160
|
// src/core/init.ts
|
|
1161
|
+
var MANIFEST_HEADER = "# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed\n";
|
|
998
1162
|
async function init(ctx, opts = {}) {
|
|
999
1163
|
const profileName = opts.profile ?? "default";
|
|
1000
|
-
const exclude = opts.exclude ?? [];
|
|
1001
|
-
const skipped = [];
|
|
1002
1164
|
if (await exists(manifestPath(ctx))) {
|
|
1003
1165
|
throw new Error(`\uC774\uBBF8 \uCD08\uAE30\uD654\uB41C \uCC3D\uACE0\uC785\uB2C8\uB2E4: ${manifestPath(ctx)}
|
|
1004
|
-
\
|
|
1005
|
-
}
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
1010
|
-
const
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
if (pkgs.length) m.profiles[profileName][PACKAGES] = pkgs.map((p) => p.id);
|
|
1018
|
-
for (const [key, by] of generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
|
|
1019
|
-
const managed = [];
|
|
1020
|
-
let copied = 0;
|
|
1021
|
-
for (const cat of ctx.adapter.categories()) {
|
|
1022
|
-
const mine = found.filter((f) => f.category === cat.name && !isExcluded(f.category, f.id));
|
|
1023
|
-
for (const f of found.filter((f2) => f2.category === cat.name && isExcluded(f2.category, f2.id))) {
|
|
1024
|
-
skipped.push(`${f.category}/${f.id}`);
|
|
1025
|
-
ctx.log(` - ${f.category}/${f.id} (--exclude)`);
|
|
1026
|
-
}
|
|
1027
|
-
if (!mine.length) continue;
|
|
1028
|
-
m.components[cat.name] = [];
|
|
1029
|
-
m.profiles[profileName][cat.name] = [];
|
|
1030
|
-
for (const f of mine) {
|
|
1031
|
-
const dst = path14.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
1032
|
-
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
1033
|
-
m.components[cat.name].push({ id: f.id });
|
|
1034
|
-
m.profiles[profileName][cat.name].push(f.id);
|
|
1035
|
-
managed.push(targetRel(cat, f.id));
|
|
1036
|
-
copied++;
|
|
1037
|
-
ctx.log(` + ${cat.name}/${f.id}`);
|
|
1038
|
-
}
|
|
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
|
-
}
|
|
1166
|
+
\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.`);
|
|
1167
|
+
}
|
|
1168
|
+
const d = await discover(ctx, opts.exclude);
|
|
1169
|
+
for (const [key, by] of d.generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
|
|
1170
|
+
for (const key of d.excluded) ctx.log(` - ${key} (--exclude)`);
|
|
1171
|
+
const exclude = opts.exclude?.length ? { exclude: [...opts.exclude] } : {};
|
|
1172
|
+
const doc = new YAML4.Document({ version: 1, agent: ctx.adapter.name, ...exclude, components: {}, packages: [], profiles: { [profileName]: {} } });
|
|
1173
|
+
const lock = { version: 1, packages: {} };
|
|
1174
|
+
await fs11.mkdir(ctx.shed, { recursive: true });
|
|
1175
|
+
const res = await ingest(ctx, doc, profileName, d.items, lock);
|
|
1176
|
+
const managed = [...res.managed];
|
|
1177
|
+
let copied = res.copied;
|
|
1067
1178
|
const instr = instructionsFile(ctx);
|
|
1068
1179
|
if (await exists(instr)) {
|
|
1069
1180
|
const text = await fs11.readFile(instr, "utf8");
|
|
1070
1181
|
if (!isGenerated(text)) {
|
|
1071
|
-
const dst =
|
|
1072
|
-
await fs11.mkdir(
|
|
1182
|
+
const dst = path16.join(ctx.shed, INSTRUCTIONS, "main.md");
|
|
1183
|
+
await fs11.mkdir(path16.dirname(dst), { recursive: true });
|
|
1073
1184
|
await fs11.writeFile(dst, text);
|
|
1074
1185
|
const fragRel = targetRel(INSTRUCTIONS, "main");
|
|
1075
1186
|
await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
|
|
1076
1187
|
managed.push(fragRel);
|
|
1077
|
-
|
|
1078
|
-
|
|
1188
|
+
doc.setIn(["components", INSTRUCTIONS], [{ id: "main" }]);
|
|
1189
|
+
doc.setIn(["profiles", profileName, INSTRUCTIONS], ["main"]);
|
|
1079
1190
|
copied++;
|
|
1080
|
-
ctx.log(` + ${INSTRUCTIONS}/main (${
|
|
1191
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path16.basename(instr)})`);
|
|
1081
1192
|
}
|
|
1082
1193
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
yamlText = yamlText.replace(` into: ${p.into}
|
|
1088
|
-
`, ` into: ${p.into}
|
|
1089
|
-
# install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
|
|
1090
|
-
`);
|
|
1091
|
-
}
|
|
1092
|
-
await fs11.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
|
|
1093
|
-
` + yamlText);
|
|
1094
|
-
if (pkgs.length) {
|
|
1095
|
-
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
|
|
1096
|
-
}
|
|
1194
|
+
tidy(doc);
|
|
1195
|
+
const yamlText = MANIFEST_HEADER + doc.toString({ lineWidth: 0 });
|
|
1196
|
+
await fs11.writeFile(manifestPath(ctx), yamlText);
|
|
1197
|
+
if (res.packages.length) await writeLock(ctx.shed, lock);
|
|
1097
1198
|
await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1098
1199
|
const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
|
|
1099
|
-
if (
|
|
1100
|
-
if (generated.size) parts.push(`\uC0DD\uC131\uBB3C ${generated.size}\uAC1C \uAC74\uB108\uB700`);
|
|
1101
|
-
if (
|
|
1200
|
+
if (res.packages.length) parts.push(`\uD328\uD0A4\uC9C0 ${res.packages.length}\uAC1C`);
|
|
1201
|
+
if (d.generated.size) parts.push(`\uC0DD\uC131\uBB3C ${d.generated.size}\uAC1C \uAC74\uB108\uB700`);
|
|
1202
|
+
if (d.excluded.length) parts.push(`\uC81C\uC678 ${d.excluded.length}\uAC1C`);
|
|
1102
1203
|
ctx.log(`
|
|
1103
1204
|
${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1205
|
+
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.`);
|
|
1206
|
+
const manifest = parseManifest(yamlText);
|
|
1207
|
+
return { manifest, copied, skipped: d.excluded, packages: res.packages, generated: [...d.generated.keys()] };
|
|
1106
1208
|
}
|
|
1107
1209
|
|
|
1108
1210
|
// src/core/restore.ts
|
|
1109
1211
|
import { promises as fs12 } from "fs";
|
|
1110
|
-
import
|
|
1212
|
+
import path17 from "path";
|
|
1111
1213
|
async function restore(ctx, profileArg, opts = {}) {
|
|
1112
1214
|
const backup = opts.backup ?? true;
|
|
1113
1215
|
const state = await readState(ctx.adapter);
|
|
@@ -1126,7 +1228,7 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1126
1228
|
const oldManaged = new Set(state?.managed ?? []);
|
|
1127
1229
|
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
1128
1230
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1129
|
-
const backupDir =
|
|
1231
|
+
const backupDir = path17.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
1130
1232
|
const backedUp = [];
|
|
1131
1233
|
const placed = [];
|
|
1132
1234
|
const missingEnv = [];
|
|
@@ -1142,14 +1244,14 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1142
1244
|
if (cur === void 0) return;
|
|
1143
1245
|
backedUp.push(rel);
|
|
1144
1246
|
if (opts.dryRun || !backup) return;
|
|
1145
|
-
await writeEntryFile(
|
|
1247
|
+
await writeEntryFile(path17.join(backupDir, en.cat.name, `${en.id}.json`), cur);
|
|
1146
1248
|
return;
|
|
1147
1249
|
}
|
|
1148
1250
|
const from = abs(ctx, rel);
|
|
1149
1251
|
if (!await exists(from)) return;
|
|
1150
1252
|
backedUp.push(rel);
|
|
1151
1253
|
if (opts.dryRun || !backup) return;
|
|
1152
|
-
await copyTree(from,
|
|
1254
|
+
await copyTree(from, path17.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
|
|
1153
1255
|
}
|
|
1154
1256
|
for (const rel of toRemove) {
|
|
1155
1257
|
ctx.log(` - ${rel}`);
|
|
@@ -1163,8 +1265,8 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1163
1265
|
if (it.entry) {
|
|
1164
1266
|
const shed = await readEntryFile(it.src);
|
|
1165
1267
|
const local = (await entriesOf(it.entry))[it.id];
|
|
1166
|
-
const vars = placeholdersIn(shed);
|
|
1167
|
-
const ex = expand(shed);
|
|
1268
|
+
const vars = placeholdersIn(shed).filter((v) => v !== "HOME");
|
|
1269
|
+
const ex = expand(shed, envWithHome());
|
|
1168
1270
|
if (ex.missing.length) missingEnv.push({ rel: it.rel, vars: ex.missing });
|
|
1169
1271
|
const value = it.entry.expandsEnv ? shed : ex.value;
|
|
1170
1272
|
const same2 = local !== void 0 && matches2(shed, local);
|
|
@@ -1260,10 +1362,71 @@ function formatDiff(diffs) {
|
|
|
1260
1362
|
return lines.join("\n");
|
|
1261
1363
|
}
|
|
1262
1364
|
|
|
1365
|
+
// src/core/add.ts
|
|
1366
|
+
import { promises as fs13 } from "fs";
|
|
1367
|
+
import YAML5 from "yaml";
|
|
1368
|
+
async function candidates(ctx, m, profile, d) {
|
|
1369
|
+
d ??= await discover(ctx, m.exclude);
|
|
1370
|
+
return { fresh: notInManifest(m, d), notInProfile: inManifestNotInProfile(m, profile, d), generated: d.generated };
|
|
1371
|
+
}
|
|
1372
|
+
async function add(ctx, keys = [], opts = {}) {
|
|
1373
|
+
const state = await readState(ctx.adapter);
|
|
1374
|
+
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.");
|
|
1375
|
+
const m = await loadManifest(ctx);
|
|
1376
|
+
const c = await candidates(ctx, m, state.profile);
|
|
1377
|
+
if (!keys.length && !opts.all) {
|
|
1378
|
+
if (!c.fresh.length) ctx.log("\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uC0C8 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1379
|
+
else {
|
|
1380
|
+
ctx.log(`\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uD56D\uBAA9 ${c.fresh.length}\uAC1C (\uB123\uC73C\uB824\uBA74 lshed add <key...> \uB610\uB294 --all):`);
|
|
1381
|
+
for (const f of c.fresh) ctx.log(` ${f.kind === "package" ? "\u2261" : " "} ${keyOf(f)}${f.kind === "package" ? ` ${f.pkg.source}` : f.kind === "entry" && f.warn ? ` ! ${f.warn}` : ""}`);
|
|
1382
|
+
}
|
|
1383
|
+
hint(ctx, c, state.profile);
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
let chosen = c.fresh;
|
|
1387
|
+
if (keys.length) {
|
|
1388
|
+
chosen = keys.map((raw) => {
|
|
1389
|
+
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
1390
|
+
const hits = c.fresh.filter((f) => f.id === b && (a === void 0 || f.category === a));
|
|
1391
|
+
if (!hits.length) {
|
|
1392
|
+
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);
|
|
1393
|
+
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.`);
|
|
1394
|
+
}
|
|
1395
|
+
if (hits.length > 1) throw new Error(`"${raw}" \uAC00 \uBAA8\uD638\uD569\uB2C8\uB2E4: ${hits.map(keyOf).join(", ")}`);
|
|
1396
|
+
return hits[0];
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
if (!chosen.length) {
|
|
1400
|
+
ctx.log("\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uC0C8 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1401
|
+
hint(ctx, c, state.profile);
|
|
1402
|
+
return [];
|
|
1403
|
+
}
|
|
1404
|
+
const doc = YAML5.parseDocument(await fs13.readFile(manifestPath(ctx), "utf8"));
|
|
1405
|
+
const lock = await readLock(ctx.shed);
|
|
1406
|
+
const res = await ingest(ctx, doc, state.profile, chosen, lock);
|
|
1407
|
+
tidy(doc);
|
|
1408
|
+
await fs13.writeFile(manifestPath(ctx), doc.toString({ lineWidth: 0 }));
|
|
1409
|
+
if (res.packages.length) await writeLock(ctx.shed, lock);
|
|
1410
|
+
const managed = [.../* @__PURE__ */ new Set([...state.managed, ...res.managed])].sort();
|
|
1411
|
+
await writeState(ctx.adapter, { ...state, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1412
|
+
const added = chosen.map(keyOf);
|
|
1413
|
+
ctx.log(`
|
|
1414
|
+
${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}`);
|
|
1415
|
+
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.`);
|
|
1416
|
+
hint(ctx, { ...c, fresh: c.fresh.filter((f) => !chosen.includes(f)) }, state.profile);
|
|
1417
|
+
return added;
|
|
1418
|
+
}
|
|
1419
|
+
function hint(ctx, c, profile) {
|
|
1420
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
1421
|
+
for (const by of c.generated.values()) byPkg.set(by, (byPkg.get(by) ?? 0) + 1);
|
|
1422
|
+
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`);
|
|
1423
|
+
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`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1263
1426
|
// src/core/status.ts
|
|
1264
1427
|
async function status(ctx) {
|
|
1265
1428
|
const state = await readState(ctx.adapter);
|
|
1266
|
-
if (!state) return { state: null, drifted: [], packages: [], missingEnv: [] };
|
|
1429
|
+
if (!state) return { state: null, drifted: [], packages: [], missingEnv: [], fresh: [] };
|
|
1267
1430
|
const d = await diff(ctx);
|
|
1268
1431
|
const m = await loadManifest(ctx);
|
|
1269
1432
|
const lock = await readLock(ctx.shed);
|
|
@@ -1271,10 +1434,11 @@ async function status(ctx) {
|
|
|
1271
1434
|
const missingEnv = [];
|
|
1272
1435
|
for (const it of planProfile(ctx, m, state.profile).filter((p) => p.entry)) {
|
|
1273
1436
|
const shed = await readEntryFile(it.src);
|
|
1274
|
-
const missing = shed === null ? [] : expand(shed).missing;
|
|
1437
|
+
const missing = shed === null ? [] : expand(shed, envWithHome()).missing;
|
|
1275
1438
|
if (missing.length) missingEnv.push({ rel: it.rel, vars: missing });
|
|
1276
1439
|
}
|
|
1277
|
-
|
|
1440
|
+
const fresh = (await candidates(ctx, m, state.profile)).fresh.map(keyOf);
|
|
1441
|
+
return { state, drifted: d.map((x2) => `${x2.item.category}/${x2.item.id}`), packages, missingEnv, fresh };
|
|
1278
1442
|
}
|
|
1279
1443
|
function formatStatus(s, adapterRoot) {
|
|
1280
1444
|
if (!s.state) return `\uC801\uC6A9\uB41C \uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (${adapterRoot}).
|
|
@@ -1292,6 +1456,7 @@ function formatStatus(s, adapterRoot) {
|
|
|
1292
1456
|
lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
|
|
1293
1457
|
}
|
|
1294
1458
|
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`);
|
|
1459
|
+
if (s.fresh.length) lines.push(`\uCC3D\uACE0 \uBC16 ${s.fresh.length}\uAC1C: ${s.fresh.join(", ")} \u2192 lshed add`);
|
|
1295
1460
|
return lines.join("\n");
|
|
1296
1461
|
}
|
|
1297
1462
|
|
|
@@ -1371,9 +1536,9 @@ function formatRows(rows, m) {
|
|
|
1371
1536
|
}
|
|
1372
1537
|
|
|
1373
1538
|
// src/core/remove.ts
|
|
1374
|
-
import { promises as
|
|
1375
|
-
import
|
|
1376
|
-
import
|
|
1539
|
+
import { promises as fs14 } from "fs";
|
|
1540
|
+
import path18 from "path";
|
|
1541
|
+
import YAML6, { isSeq as isSeq2, isMap as isMap2 } from "yaml";
|
|
1377
1542
|
function resolveKey(m, raw) {
|
|
1378
1543
|
const rows = listRows(m);
|
|
1379
1544
|
const [a, b] = raw.includes("/") ? raw.split("/", 2) : [void 0, raw];
|
|
@@ -1387,13 +1552,13 @@ async function remove(ctx, raw) {
|
|
|
1387
1552
|
const { category, id } = resolveKey(m, raw);
|
|
1388
1553
|
const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
|
|
1389
1554
|
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.`);
|
|
1390
|
-
const text = await
|
|
1391
|
-
const doc =
|
|
1555
|
+
const text = await fs14.readFile(manifestPath(ctx), "utf8");
|
|
1556
|
+
const doc = YAML6.parseDocument(text);
|
|
1392
1557
|
let deleted;
|
|
1393
1558
|
if (category === PACKAGES) {
|
|
1394
1559
|
const seq = doc.get(PACKAGES);
|
|
1395
|
-
if (!
|
|
1396
|
-
const idx = seq.items.findIndex((it) =>
|
|
1560
|
+
if (!isSeq2(seq)) throw new Error("packages \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4");
|
|
1561
|
+
const idx = seq.items.findIndex((it) => isMap2(it) && it.get("id") === id);
|
|
1397
1562
|
seq.delete(idx);
|
|
1398
1563
|
if (!seq.items.length) doc.delete(PACKAGES);
|
|
1399
1564
|
const lock = await readLock(ctx.shed);
|
|
@@ -1404,19 +1569,19 @@ async function remove(ctx, raw) {
|
|
|
1404
1569
|
ctx.log(` - package ${id} (\uB9E4\uB2C8\uD398\uC2A4\uD2B8\xB7\uB77D\uC5D0\uC11C \uC81C\uAC70. \uB85C\uCEEC clone \uC740 \uADF8\uB300\uB85C)`);
|
|
1405
1570
|
} else {
|
|
1406
1571
|
const seq = doc.getIn(["components", category]);
|
|
1407
|
-
if (!
|
|
1408
|
-
const idx = seq.items.findIndex((it) =>
|
|
1572
|
+
if (!isSeq2(seq)) throw new Error(`components.${category} \uAC00 \uBAA9\uB85D\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
1573
|
+
const idx = seq.items.findIndex((it) => isMap2(it) && it.get("id") === id);
|
|
1409
1574
|
seq.delete(idx);
|
|
1410
1575
|
if (!seq.items.length) doc.deleteIn(["components", category]);
|
|
1411
1576
|
const src = sourcePath(ctx, category, findComponent(m, category, id));
|
|
1412
|
-
const inside = !
|
|
1577
|
+
const inside = !path18.relative(ctx.shed, src).startsWith("..");
|
|
1413
1578
|
if (inside && await exists(src)) {
|
|
1414
1579
|
await removeTree(src);
|
|
1415
1580
|
deleted = src;
|
|
1416
1581
|
}
|
|
1417
1582
|
ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
|
|
1418
1583
|
}
|
|
1419
|
-
await
|
|
1584
|
+
await fs14.writeFile(manifestPath(ctx), doc.toString());
|
|
1420
1585
|
return { category, id, deleted };
|
|
1421
1586
|
}
|
|
1422
1587
|
async function prune(ctx, opts = {}) {
|
|
@@ -1441,21 +1606,105 @@ ${removed.length}\uAC1C \uC81C\uAC70. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC13
|
|
|
1441
1606
|
return removed;
|
|
1442
1607
|
}
|
|
1443
1608
|
|
|
1609
|
+
// src/core/sync.ts
|
|
1610
|
+
import os3 from "os";
|
|
1611
|
+
async function sync(ctx, opts = {}) {
|
|
1612
|
+
const shed = ctx.shed;
|
|
1613
|
+
const push = opts.push ?? true;
|
|
1614
|
+
if (!await isRepo(shed)) {
|
|
1615
|
+
throw new Error(`\uCC3D\uACE0\uAC00 git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${shed}
|
|
1616
|
+
cd ${shed} && git init && git add -A && git commit -m "my harness"
|
|
1617
|
+
\uC6D0\uACA9\uC5D0 \uB450\uB824\uBA74: git remote add origin <url> && git push -u origin HEAD`);
|
|
1618
|
+
}
|
|
1619
|
+
const res = { committed: [], pulled: 0, pushed: false, unsaved: [] };
|
|
1620
|
+
if (await readState(ctx.adapter)) {
|
|
1621
|
+
try {
|
|
1622
|
+
res.unsaved = (await diff(ctx)).map((d) => `${d.item.category}/${d.item.id}`);
|
|
1623
|
+
} catch {
|
|
1624
|
+
}
|
|
1625
|
+
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`);
|
|
1626
|
+
}
|
|
1627
|
+
const dirty = (await git(["status", "--porcelain", "--untracked-files=all"], shed)).split("\n").filter(Boolean).map((l) => l.slice(3).trim());
|
|
1628
|
+
if (dirty.length) {
|
|
1629
|
+
const msg = opts.message ?? defaultMessage(dirty, (await readState(ctx.adapter))?.profile);
|
|
1630
|
+
ctx.log(` ${opts.dryRun ? "(dry-run) " : ""}commit ${dirty.length}\uAC1C: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ` \uC678 ${dirty.length - 5}` : ""}`);
|
|
1631
|
+
if (!opts.dryRun) {
|
|
1632
|
+
await git(["add", "-A"], shed);
|
|
1633
|
+
await git(["commit", "--quiet", "-m", msg], shed);
|
|
1634
|
+
}
|
|
1635
|
+
res.committed = dirty;
|
|
1636
|
+
} else {
|
|
1637
|
+
ctx.log(" = \uCC3D\uACE0\uC5D0 \uCEE4\uBC0B\uD560 \uBCC0\uACBD \uC5C6\uC74C");
|
|
1638
|
+
}
|
|
1639
|
+
const remote = await git(["remote", "get-url", "origin"], shed).catch(() => null);
|
|
1640
|
+
if (!remote) {
|
|
1641
|
+
ctx.log(" \xB7 origin \uC774 \uC5C6\uC5B4 pull/push \uB294 \uAC74\uB108\uB700 (git remote add origin <url>)");
|
|
1642
|
+
return res;
|
|
1643
|
+
}
|
|
1644
|
+
if (opts.dryRun) {
|
|
1645
|
+
ctx.log(` (dry-run) pull --rebase, push \u2192 ${remote}`);
|
|
1646
|
+
return res;
|
|
1647
|
+
}
|
|
1648
|
+
const before = await git(["rev-parse", "HEAD"], shed);
|
|
1649
|
+
const branch2 = await git(["rev-parse", "--abbrev-ref", "HEAD"], shed);
|
|
1650
|
+
const hasUpstream = await git(["rev-parse", "--abbrev-ref", "@{upstream}"], shed).then(() => true, () => false);
|
|
1651
|
+
if (hasUpstream) {
|
|
1652
|
+
try {
|
|
1653
|
+
await git(["pull", "--rebase", "--quiet"], shed);
|
|
1654
|
+
} catch (e) {
|
|
1655
|
+
await git(["rebase", "--abort"], shed).catch(() => {
|
|
1656
|
+
});
|
|
1657
|
+
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:
|
|
1658
|
+
cd ${shed} && git pull --rebase
|
|
1659
|
+
(${firstLine(e.message)})`);
|
|
1660
|
+
}
|
|
1661
|
+
const after = await git(["rev-parse", "HEAD"], shed);
|
|
1662
|
+
if (after !== before) {
|
|
1663
|
+
const n = Number(await git(["rev-list", "--count", `${before}..${after}`], shed).catch(() => "0"));
|
|
1664
|
+
res.pulled = Math.max(0, n - (res.committed.length ? 1 : 0));
|
|
1665
|
+
if (res.pulled) ctx.log(` \u2193 \uC6D0\uACA9 \uCEE4\uBC0B ${res.pulled}\uAC1C \uBC1B\uC74C`);
|
|
1666
|
+
}
|
|
1667
|
+
} else {
|
|
1668
|
+
ctx.log(` \xB7 \uBE0C\uB79C\uCE58 ${branch2} \uC5D0 upstream \uC774 \uC5C6\uC5B4 pull \uC740 \uAC74\uB108\uB700`);
|
|
1669
|
+
}
|
|
1670
|
+
if (push) {
|
|
1671
|
+
const ahead = hasUpstream ? Number(await git(["rev-list", "--count", "@{upstream}..HEAD"], shed)) : 1;
|
|
1672
|
+
if (ahead > 0) {
|
|
1673
|
+
await git(hasUpstream ? ["push", "--quiet"] : ["push", "--quiet", "-u", "origin", branch2], shed);
|
|
1674
|
+
res.pushed = true;
|
|
1675
|
+
ctx.log(` \u2191 push ${hasUpstream ? `${ahead}\uAC1C \uCEE4\uBC0B` : `(upstream \uC124\uC815: origin/${branch2})`}`);
|
|
1676
|
+
} else {
|
|
1677
|
+
ctx.log(" = \uC6D0\uACA9\uACFC \uAC19\uC74C");
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (res.pulled) ctx.log(`
|
|
1681
|
+
\uCC3D\uACE0\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 \uAE30\uAE30\uC5D0 \uC801\uC6A9\uD558\uB824\uBA74: lshed restore`);
|
|
1682
|
+
return res;
|
|
1683
|
+
}
|
|
1684
|
+
function defaultMessage(paths, profile) {
|
|
1685
|
+
const parts = [...new Set(paths.map((p) => p.split("/").slice(0, 2).join("/")))];
|
|
1686
|
+
const head2 = parts.slice(0, 3).join(", ") + (parts.length > 3 ? ` +${parts.length - 3}` : "");
|
|
1687
|
+
return `lshed sync: ${head2}
|
|
1688
|
+
|
|
1689
|
+
${os3.hostname()}${profile ? ` \xB7 profile ${profile}` : ""}`;
|
|
1690
|
+
}
|
|
1691
|
+
var firstLine = (s) => s.split("\n").find((l) => l.trim() && !l.startsWith("Command failed")) ?? s;
|
|
1692
|
+
|
|
1444
1693
|
// src/cli.ts
|
|
1445
1694
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
1446
1695
|
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)");
|
|
1447
1696
|
function adapterFromOpts() {
|
|
1448
1697
|
const { root } = program.opts();
|
|
1449
|
-
return new ClaudeCodeAdapter(root ?
|
|
1698
|
+
return new ClaudeCodeAdapter(root ? path19.resolve(root) : void 0);
|
|
1450
1699
|
}
|
|
1451
1700
|
async function ctxFor(cmd) {
|
|
1452
1701
|
const adapter = adapterFromOpts();
|
|
1453
1702
|
const { shed: flag } = program.opts();
|
|
1454
1703
|
let shed = flag ?? process.env.LSHED_HOME;
|
|
1455
1704
|
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
1456
|
-
if (!shed && cmd === "init") shed =
|
|
1705
|
+
if (!shed && cmd === "init") shed = path19.join(os4.homedir(), "lshed");
|
|
1457
1706
|
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.");
|
|
1458
|
-
return { adapter, shed:
|
|
1707
|
+
return { adapter, shed: path19.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
|
|
1459
1708
|
}
|
|
1460
1709
|
async function run(fn) {
|
|
1461
1710
|
try {
|
|
@@ -1500,7 +1749,7 @@ program.command("status").description("show the applied profile, managed paths a
|
|
|
1500
1749
|
const adapter = adapterFromOpts();
|
|
1501
1750
|
const state = await readState(adapter);
|
|
1502
1751
|
if (!state) {
|
|
1503
|
-
console.log(formatStatus({ state: null, drifted: [], packages: [], missingEnv: [] }, adapter.root));
|
|
1752
|
+
console.log(formatStatus({ state: null, drifted: [], packages: [], missingEnv: [], fresh: [] }, adapter.root));
|
|
1504
1753
|
return;
|
|
1505
1754
|
}
|
|
1506
1755
|
const ctx = await ctxFor("other");
|
|
@@ -1514,6 +1763,14 @@ program.command("save [ids...]").description("copy local edits back into the she
|
|
|
1514
1763
|
const ctx = await ctxFor("other");
|
|
1515
1764
|
await save(ctx, ids);
|
|
1516
1765
|
}));
|
|
1766
|
+
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 () => {
|
|
1767
|
+
const ctx = await ctxFor("other");
|
|
1768
|
+
await add(ctx, keys, { all: o.all });
|
|
1769
|
+
}));
|
|
1770
|
+
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 () => {
|
|
1771
|
+
const ctx = await ctxFor("other");
|
|
1772
|
+
await sync(ctx, { message: o.message, push: o.push, dryRun: o.dryRun });
|
|
1773
|
+
}));
|
|
1517
1774
|
program.command("list").description("everything in the shed and which profiles use it").option("--unused", "only things no profile uses").action((o) => run(async () => {
|
|
1518
1775
|
const ctx = await ctxFor("other");
|
|
1519
1776
|
const m = await loadManifest(ctx);
|