lshed 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +14 -2
- package/dist/cli.js +477 -291
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-09-02
|
|
4
|
+
|
|
5
|
+
Claude Code plugins are packages now. On the machine this was built against, the five installed plugins were the only thing a fresh `restore` still left out, and two of them carry MCP servers.
|
|
6
|
+
|
|
7
|
+
- `init` records each user-scope plugin as `claude-plugin:<name>@<marketplace>` and each GitHub-backed marketplace as `claude-marketplace:<owner/repo>`. Project-scope plugins belong to their project and are skipped.
|
|
8
|
+
- `restore` adds missing marketplaces first, then installs missing plugins through `claude plugin install`. Both are the agent's own package manager, so they run without `--yes`; `--yes` is forwarded as `-y` to accept a marketplace-declared install command.
|
|
9
|
+
- Plugins cannot be pinned. `lshed.lock` records the version that actually got installed and `status` shows when it differs from what another machine had. `update` runs `claude plugin update`.
|
|
10
|
+
- Installers are an interface now. `github:`/`git:` live in core; an adapter contributes its own (`ClaudeCodeAdapter` provides the two above). Install order follows installer priority, then manifest order.
|
|
11
|
+
- Lock entries use `rev` instead of `commit`. Old locks still read.
|
|
12
|
+
|
|
3
13
|
## 0.2.1 — 2026-09-02
|
|
4
14
|
|
|
5
15
|
- `lshed list [--unused]` shows everything in the shed and which profiles use it.
|
package/README.md
CHANGED
|
@@ -108,10 +108,22 @@ profiles:
|
|
|
108
108
|
|
|
109
109
|
`lshed.lock` pins each package to a commit, so a fresh machine gets the same version you had. `lshed update` moves it forward.
|
|
110
110
|
|
|
111
|
+
Claude Code plugins are packages too, with their own scheme. `init` finds them in `~/.claude/plugins`:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
packages:
|
|
115
|
+
- id: claude-plugins-official
|
|
116
|
+
source: claude-marketplace:anthropics/claude-plugins-official
|
|
117
|
+
- id: exa
|
|
118
|
+
source: claude-plugin:exa@claude-plugins-official
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`restore` adds the marketplace, then runs `claude plugin install exa@claude-plugins-official`. Plugins cannot be pinned to a version, so the lock records whatever got installed and `status` tells you when it differs from the machine you came from. Plugins that bundle MCP servers bring them along.
|
|
122
|
+
|
|
111
123
|
Rules that keep this safe:
|
|
112
124
|
|
|
113
125
|
- A package that is already present is never touched by `restore`. Your local checkout is yours.
|
|
114
|
-
- `install:` is a shell command. `restore` and `update` **print it and stop** unless you pass `--yes`.
|
|
126
|
+
- `install:` is a shell command. `restore` and `update` **print it and stop** unless you pass `--yes`. Plugin installs go through Claude Code's own package manager and run without it; `--yes` is forwarded as `-y` for plugins that declare an install command.
|
|
115
127
|
- Packages are not part of the managed set. Switching profiles never deletes a clone.
|
|
116
128
|
- Installers sometimes create aliases without symlinks, which `init` cannot tell from authored skills. Leave those out with `--exclude`:
|
|
117
129
|
|
|
@@ -171,7 +183,7 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
|
|
|
171
183
|
- MCP servers and secrets. Planned: the manifest names the keys, values are injected locally, nothing secret enters the shed.
|
|
172
184
|
- `settings.json` merging (hooks, permissions).
|
|
173
185
|
- `sync` (a git pull/push wrapper). Use git in the shed directly for now.
|
|
174
|
-
-
|
|
186
|
+
- MCP servers configured by hand in `~/.claude.json`. Plugin-bundled ones are covered.
|
|
175
187
|
- Windows and macOS have not been tested. The code avoids platform-specific paths, but treat 0.1 as Linux/WSL.
|
|
176
188
|
|
|
177
189
|
## Troubleshooting
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,169 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import os2 from "os";
|
|
7
|
-
import
|
|
7
|
+
import path15 from "path";
|
|
8
8
|
|
|
9
9
|
// src/adapters/claude-code.ts
|
|
10
|
+
import { promises as fs2 } from "fs";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
|
|
14
|
+
// src/installers/claude-plugin.ts
|
|
10
15
|
import { promises as fs } from "fs";
|
|
11
16
|
import path from "path";
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
// src/source.ts
|
|
19
|
+
var GITHUB_RE = /^([\w.-]+)\/([\w.-]+)(?:@([^#]+))?(?:#(.+))?$/;
|
|
20
|
+
function parseSource(raw) {
|
|
21
|
+
const idx = raw.indexOf(":");
|
|
22
|
+
if (idx <= 0) {
|
|
23
|
+
throw new Error(`source\uC5D0 \uC2A4\uD0B4\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}" (\uC608: file:./skills/x, github:user/repo@v1)`);
|
|
24
|
+
}
|
|
25
|
+
const scheme = raw.slice(0, idx);
|
|
26
|
+
const rest2 = raw.slice(idx + 1);
|
|
27
|
+
switch (scheme) {
|
|
28
|
+
case "file":
|
|
29
|
+
if (!rest2) throw new Error(`file: \uB4A4\uC5D0 \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
|
|
30
|
+
return { scheme, path: rest2 };
|
|
31
|
+
case "github": {
|
|
32
|
+
const m = GITHUB_RE.exec(rest2);
|
|
33
|
+
if (!m) throw new Error(`github: \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: "${raw}" (\uC608: github:user/repo@v1.0#sub/path)`);
|
|
34
|
+
const [, owner, repo, ref, subpath] = m;
|
|
35
|
+
return { scheme, owner, repo, ref, subpath };
|
|
36
|
+
}
|
|
37
|
+
case "git": {
|
|
38
|
+
const hash = rest2.lastIndexOf("#");
|
|
39
|
+
const url = hash >= 0 ? rest2.slice(0, hash) : rest2;
|
|
40
|
+
const ref = hash >= 0 ? rest2.slice(hash + 1) : void 0;
|
|
41
|
+
if (!url) throw new Error(`git: \uB4A4\uC5D0 URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: "${raw}"`);
|
|
42
|
+
return { scheme, url, ref: ref || void 0 };
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
if (!/^[a-z][\w-]*$/.test(scheme)) throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
|
|
46
|
+
return { scheme: "other", name: scheme, rest: rest2 };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isComponentSource(s) {
|
|
50
|
+
return s.scheme === "file" || s.scheme === "github" || s.scheme === "git";
|
|
51
|
+
}
|
|
52
|
+
function formatSource(s) {
|
|
53
|
+
switch (s.scheme) {
|
|
54
|
+
case "file":
|
|
55
|
+
return `file:${s.path}`;
|
|
56
|
+
case "github":
|
|
57
|
+
return `github:${s.owner}/${s.repo}${s.ref ? `@${s.ref}` : ""}${s.subpath ? `#${s.subpath}` : ""}`;
|
|
58
|
+
case "git":
|
|
59
|
+
return `git:${s.url}${s.ref ? `#${s.ref}` : ""}`;
|
|
60
|
+
case "other":
|
|
61
|
+
return `${s.name}:${s.rest}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function cloneTarget(s) {
|
|
65
|
+
if (s.scheme === "github") return { url: `https://github.com/${s.owner}/${s.repo}.git`, ref: s.ref };
|
|
66
|
+
if (s.scheme === "git") return { url: s.url, ref: s.ref };
|
|
67
|
+
throw new Error(`file: \uCD9C\uCC98\uB294 clone \uB300\uC0C1\uC774 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
68
|
+
}
|
|
69
|
+
function sourceFromRemote(url, ref) {
|
|
70
|
+
const m = /^(?:https?:\/\/github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
|
|
71
|
+
if (m) return formatSource({ scheme: "github", owner: m[1], repo: m[2], ref });
|
|
72
|
+
return formatSource({ scheme: "git", url, ref });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/installers/claude-plugin.ts
|
|
76
|
+
async function readJson(p, fallback) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(await fs.readFile(p, "utf8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var installedPath = (ctx) => path.join(ctx.adapter.root, "plugins", "installed_plugins.json");
|
|
84
|
+
var marketplacesPath = (ctx) => path.join(ctx.adapter.root, "plugins", "known_marketplaces.json");
|
|
85
|
+
function rest(pkg) {
|
|
86
|
+
const s = parseSource(pkg.source);
|
|
87
|
+
if (s.scheme !== "other") throw new Error(`package ${pkg.id}: ${pkg.source} \uB294 \uD50C\uB7EC\uADF8\uC778 \uCD9C\uCC98\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
88
|
+
return s.rest;
|
|
89
|
+
}
|
|
90
|
+
async function claude(ctx, args) {
|
|
91
|
+
try {
|
|
92
|
+
await ctx.exec("claude", args);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e.code === "ENOENT") throw new Error("claude CLI \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. Claude Code \uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC5B4\uC57C \uD50C\uB7EC\uADF8\uC778\uC744 \uBCF5\uC6D0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var marketplaceInstaller = {
|
|
99
|
+
name: "claude-marketplace",
|
|
100
|
+
schemes: ["claude-marketplace"],
|
|
101
|
+
priority: 10,
|
|
102
|
+
async detect(ctx) {
|
|
103
|
+
const m = await readJson(marketplacesPath(ctx), {});
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const [name, v] of Object.entries(m)) {
|
|
106
|
+
const src = v.source;
|
|
107
|
+
if (src.source === "github" && src.repo) out.push({ id: name, source: `claude-marketplace:${src.repo}`, rev: src.repo });
|
|
108
|
+
else ctx.log(` ! marketplace ${name}: ${src.source} \uCD9C\uCC98\uB294 \uC544\uC9C1 \uAE30\uB85D\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4 (\uAC74\uB108\uB700)`);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
},
|
|
112
|
+
async status(ctx, pkg) {
|
|
113
|
+
const m = await readJson(marketplacesPath(ctx), {});
|
|
114
|
+
const v = m[pkg.id];
|
|
115
|
+
return { present: !!v, rev: v?.source.repo };
|
|
116
|
+
},
|
|
117
|
+
async install(ctx, pkg, _locked, _opts) {
|
|
118
|
+
const repo = rest(pkg);
|
|
119
|
+
await claude(ctx, ["plugin", "marketplace", "add", repo, "--scope", "user"]);
|
|
120
|
+
return repo;
|
|
121
|
+
},
|
|
122
|
+
async update(ctx, pkg) {
|
|
123
|
+
await claude(ctx, ["plugin", "marketplace", "update", pkg.id]);
|
|
124
|
+
return rest(pkg);
|
|
125
|
+
},
|
|
126
|
+
describe(pkg) {
|
|
127
|
+
return `claude plugin marketplace add ${rest(pkg)}`;
|
|
128
|
+
},
|
|
129
|
+
cwd(ctx) {
|
|
130
|
+
return ctx.adapter.root;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var pluginInstaller = {
|
|
134
|
+
name: "claude-plugin",
|
|
135
|
+
schemes: ["claude-plugin"],
|
|
136
|
+
priority: 20,
|
|
137
|
+
async detect(ctx) {
|
|
138
|
+
const f = await readJson(installedPath(ctx), { version: 2, plugins: {} });
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const [key, entries] of Object.entries(f.plugins)) {
|
|
141
|
+
const e = entries.find((x2) => x2.scope === "user");
|
|
142
|
+
if (!e) continue;
|
|
143
|
+
const [name] = key.split("@");
|
|
144
|
+
out.push({ id: name, source: `claude-plugin:${key}`, rev: e.version });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
},
|
|
148
|
+
async status(ctx, pkg) {
|
|
149
|
+
const f = await readJson(installedPath(ctx), { version: 2, plugins: {} });
|
|
150
|
+
const e = f.plugins[rest(pkg)]?.find((x2) => x2.scope === "user");
|
|
151
|
+
return { present: !!e, rev: e?.version };
|
|
152
|
+
},
|
|
153
|
+
async install(ctx, pkg, _locked, opts) {
|
|
154
|
+
await claude(ctx, ["plugin", "install", rest(pkg), "--scope", "user", ...opts.yes ? ["-y"] : []]);
|
|
155
|
+
return (await this.status(ctx, pkg)).rev ?? "?";
|
|
156
|
+
},
|
|
157
|
+
async update(ctx, pkg) {
|
|
158
|
+
await claude(ctx, ["plugin", "update", rest(pkg)]);
|
|
159
|
+
return (await this.status(ctx, pkg)).rev ?? "?";
|
|
160
|
+
},
|
|
161
|
+
describe(pkg, locked) {
|
|
162
|
+
return `claude plugin install ${rest(pkg)}${locked ? ` (\uC804\uC5D0 ${locked}; \uACE0\uC815\uC740 \uC548 \uB428)` : ""}`;
|
|
163
|
+
},
|
|
164
|
+
cwd(ctx) {
|
|
165
|
+
return ctx.adapter.root;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/adapters/claude-code.ts
|
|
13
170
|
var CATEGORIES = [
|
|
14
171
|
{ name: "skills", root: "skills", kind: "dir" },
|
|
15
172
|
{ name: "agents", root: "agents", kind: "file" },
|
|
@@ -20,7 +177,7 @@ var ClaudeCodeAdapter = class {
|
|
|
20
177
|
name = "claude-code";
|
|
21
178
|
root;
|
|
22
179
|
constructor(root) {
|
|
23
|
-
this.root = root ??
|
|
180
|
+
this.root = root ?? path2.join(os.homedir(), ".claude");
|
|
24
181
|
}
|
|
25
182
|
categories() {
|
|
26
183
|
return CATEGORIES;
|
|
@@ -31,22 +188,25 @@ var ClaudeCodeAdapter = class {
|
|
|
31
188
|
instructionsFileName() {
|
|
32
189
|
return "CLAUDE.md";
|
|
33
190
|
}
|
|
191
|
+
installers() {
|
|
192
|
+
return [marketplaceInstaller, pluginInstaller];
|
|
193
|
+
}
|
|
34
194
|
async scan() {
|
|
35
195
|
const out = [];
|
|
36
196
|
for (const cat of CATEGORIES) {
|
|
37
|
-
const dir =
|
|
197
|
+
const dir = path2.join(this.root, cat.root);
|
|
38
198
|
let entries;
|
|
39
199
|
try {
|
|
40
|
-
entries = await
|
|
200
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
41
201
|
} catch {
|
|
42
202
|
continue;
|
|
43
203
|
}
|
|
44
204
|
for (const e of entries) {
|
|
45
205
|
if (e.name.startsWith(".")) continue;
|
|
46
|
-
const full =
|
|
206
|
+
const full = path2.join(dir, e.name);
|
|
47
207
|
let st;
|
|
48
208
|
try {
|
|
49
|
-
st = await
|
|
209
|
+
st = await fs2.stat(full);
|
|
50
210
|
} catch {
|
|
51
211
|
continue;
|
|
52
212
|
}
|
|
@@ -61,70 +221,208 @@ var ClaudeCodeAdapter = class {
|
|
|
61
221
|
}
|
|
62
222
|
};
|
|
63
223
|
|
|
64
|
-
// src/core/init.ts
|
|
65
|
-
import { promises as fs7 } from "fs";
|
|
66
|
-
import path10 from "path";
|
|
67
|
-
|
|
68
224
|
// src/core/context.ts
|
|
69
|
-
import { promises as
|
|
225
|
+
import { promises as fs6 } from "fs";
|
|
226
|
+
import path8 from "path";
|
|
227
|
+
import { spawn as spawn2 } from "child_process";
|
|
228
|
+
|
|
229
|
+
// src/installers/git.ts
|
|
230
|
+
import { promises as fs4 } from "fs";
|
|
231
|
+
import path5 from "path";
|
|
232
|
+
|
|
233
|
+
// src/git.ts
|
|
234
|
+
import { execFile, spawn } from "child_process";
|
|
235
|
+
import { promisify } from "util";
|
|
70
236
|
import path4 from "path";
|
|
71
237
|
|
|
72
|
-
// src/
|
|
73
|
-
import {
|
|
74
|
-
import
|
|
238
|
+
// src/fsutil.ts
|
|
239
|
+
import { promises as fs3 } from "fs";
|
|
240
|
+
import { createHash } from "crypto";
|
|
241
|
+
import path3 from "path";
|
|
75
242
|
|
|
76
|
-
// src/
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
243
|
+
// src/ignore.ts
|
|
244
|
+
var DEFAULT_IGNORE = [
|
|
245
|
+
"node_modules",
|
|
246
|
+
".git",
|
|
247
|
+
"__pycache__",
|
|
248
|
+
".venv",
|
|
249
|
+
".mypy_cache",
|
|
250
|
+
".pytest_cache",
|
|
251
|
+
".DS_Store",
|
|
252
|
+
"*.log"
|
|
253
|
+
];
|
|
254
|
+
function matches(name, pattern) {
|
|
255
|
+
if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
|
|
256
|
+
return name === pattern;
|
|
257
|
+
}
|
|
258
|
+
function isIgnored(rel, patterns) {
|
|
259
|
+
if (!rel) return false;
|
|
260
|
+
return rel.split("/").some((seg) => patterns.some((p) => matches(seg, p)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/fsutil.ts
|
|
264
|
+
async function exists(p) {
|
|
265
|
+
try {
|
|
266
|
+
await fs3.access(p);
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
82
270
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
271
|
+
}
|
|
272
|
+
async function isDir(p) {
|
|
273
|
+
try {
|
|
274
|
+
return (await fs3.stat(p)).isDirectory();
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
280
|
+
if (!await exists(root)) return [];
|
|
281
|
+
if (!await isDir(root)) return [""];
|
|
282
|
+
const out = [];
|
|
283
|
+
async function walk(dir, rel) {
|
|
284
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
285
|
+
for (const e of entries) {
|
|
286
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
287
|
+
if (isIgnored(r, ignore)) continue;
|
|
288
|
+
const full = path3.join(dir, e.name);
|
|
289
|
+
let st;
|
|
290
|
+
try {
|
|
291
|
+
st = await fs3.stat(full);
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (st.isDirectory()) await walk(full, r);
|
|
296
|
+
else if (st.isFile()) out.push(r);
|
|
101
297
|
}
|
|
102
|
-
default:
|
|
103
|
-
throw new Error(`\uC54C \uC218 \uC5C6\uB294 \uC2A4\uD0B4 "${scheme}": "${raw}"`);
|
|
104
298
|
}
|
|
299
|
+
await walk(root, "");
|
|
300
|
+
return out.sort();
|
|
105
301
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
302
|
+
async function hashFile(p) {
|
|
303
|
+
return createHash("sha256").update(await fs3.readFile(p)).digest("hex");
|
|
304
|
+
}
|
|
305
|
+
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
306
|
+
if (!await exists(root)) return null;
|
|
307
|
+
const h = createHash("sha256");
|
|
308
|
+
for (const rel of await listFiles(root, ignore)) {
|
|
309
|
+
h.update(rel).update("\0").update(await fs3.readFile(rel ? path3.join(root, rel) : root)).update("\0");
|
|
114
310
|
}
|
|
311
|
+
return h.digest("hex");
|
|
115
312
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
313
|
+
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
314
|
+
await fs3.rm(dst, { recursive: true, force: true });
|
|
315
|
+
await fs3.mkdir(path3.dirname(dst), { recursive: true });
|
|
316
|
+
const srcRoot = path3.resolve(src);
|
|
317
|
+
await fs3.cp(src, dst, {
|
|
318
|
+
recursive: true,
|
|
319
|
+
dereference: true,
|
|
320
|
+
filter: async (from) => {
|
|
321
|
+
const rel = path3.relative(srcRoot, path3.resolve(from)).split(path3.sep).join("/");
|
|
322
|
+
if (isIgnored(rel, ignore)) return false;
|
|
323
|
+
try {
|
|
324
|
+
if ((await fs3.lstat(from)).isSymbolicLink()) await fs3.stat(from);
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
120
331
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
332
|
+
async function removeTree(p) {
|
|
333
|
+
await fs3.rm(p, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
336
|
+
const l = new Set(await listFiles(local, ignore));
|
|
337
|
+
const s = new Set(await listFiles(shed, ignore));
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
340
|
+
const lp = f ? path3.join(local, f) : local;
|
|
341
|
+
const sp = f ? path3.join(shed, f) : shed;
|
|
342
|
+
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
343
|
+
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
344
|
+
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/git.ts
|
|
350
|
+
var x = promisify(execFile);
|
|
351
|
+
async function git(args, cwd) {
|
|
352
|
+
const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
|
|
353
|
+
return stdout.trim();
|
|
354
|
+
}
|
|
355
|
+
var isRepo = (dir) => exists(path4.join(dir, ".git"));
|
|
356
|
+
var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
|
|
357
|
+
var head = (dir) => git(["rev-parse", "HEAD"], dir);
|
|
358
|
+
var branch = async (dir) => {
|
|
359
|
+
const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
|
|
360
|
+
return b === "HEAD" ? void 0 : b;
|
|
361
|
+
};
|
|
362
|
+
var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
|
|
363
|
+
var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
|
|
364
|
+
var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
|
|
365
|
+
function runShell(cmd, cwd) {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
|
|
368
|
+
p.on("error", reject);
|
|
369
|
+
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
|
|
370
|
+
});
|
|
125
371
|
}
|
|
126
372
|
|
|
373
|
+
// src/installers/git.ts
|
|
374
|
+
function dirOf(ctx, pkg) {
|
|
375
|
+
if (!pkg.into) throw new Error(`package ${pkg.id}: git \uACC4\uC5F4 \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
|
|
376
|
+
return path5.join(ctx.adapter.root, ...pkg.into.split("/"));
|
|
377
|
+
}
|
|
378
|
+
var gitInstaller = {
|
|
379
|
+
name: "git",
|
|
380
|
+
schemes: ["github", "git"],
|
|
381
|
+
priority: 0,
|
|
382
|
+
async detect(ctx, found) {
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const f of found) {
|
|
385
|
+
if (!await isRepo(f.path)) continue;
|
|
386
|
+
const url = await remoteUrl(f.path);
|
|
387
|
+
if (!url) continue;
|
|
388
|
+
const into = path5.relative(ctx.adapter.root, f.path).split(path5.sep).join("/");
|
|
389
|
+
out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), rev: await head(f.path), path: f.path });
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
},
|
|
393
|
+
async status(ctx, pkg) {
|
|
394
|
+
const dir = dirOf(ctx, pkg);
|
|
395
|
+
const present = await isRepo(dir);
|
|
396
|
+
return { present, rev: present ? await head(dir) : void 0 };
|
|
397
|
+
},
|
|
398
|
+
async install(ctx, pkg, locked, _opts) {
|
|
399
|
+
const dir = dirOf(ctx, pkg);
|
|
400
|
+
if (await exists(dir)) throw new Error(`package ${pkg.id}: ${dir} \uAC00 \uC788\uC9C0\uB9CC git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uCE58\uC6B0\uAC70\uB098 into \uB97C \uBC14\uAFB8\uC138\uC694.`);
|
|
401
|
+
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
402
|
+
await fs4.mkdir(path5.dirname(dir), { recursive: true });
|
|
403
|
+
await clone(url, dir, ref);
|
|
404
|
+
if (locked) {
|
|
405
|
+
await resetHard(dir, locked).catch(() => {
|
|
406
|
+
throw new Error(`package ${pkg.id}: \uB77D\uC758 \uCEE4\uBC0B ${locked.slice(0, 7)} \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 'lshed update ${pkg.id}' \uB85C \uB77D\uC744 \uAC31\uC2E0\uD558\uC138\uC694.`);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return head(dir);
|
|
410
|
+
},
|
|
411
|
+
async update(ctx, pkg) {
|
|
412
|
+
const dir = dirOf(ctx, pkg);
|
|
413
|
+
await pullFf(dir);
|
|
414
|
+
return head(dir);
|
|
415
|
+
},
|
|
416
|
+
describe(pkg, locked) {
|
|
417
|
+
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
418
|
+
return `clone ${url}${ref ? ` @${ref}` : ""}${locked ? ` \u2192 ${locked.slice(0, 7)}` : ""}`;
|
|
419
|
+
},
|
|
420
|
+
cwd: dirOf
|
|
421
|
+
};
|
|
422
|
+
|
|
127
423
|
// src/manifest.ts
|
|
424
|
+
import { z } from "zod";
|
|
425
|
+
import YAML from "yaml";
|
|
128
426
|
var ComponentSchema = z.object({
|
|
129
427
|
id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
|
|
130
428
|
source: z.string().optional(),
|
|
@@ -133,7 +431,9 @@ var ComponentSchema = z.object({
|
|
|
133
431
|
var PackageSchema = z.object({
|
|
134
432
|
id: z.string().regex(/^[\w.-]+$/, "id\uB294 \uC601\uBB38\xB7\uC22B\uC790\xB7._- \uB9CC \uD5C8\uC6A9"),
|
|
135
433
|
source: z.string(),
|
|
136
|
-
|
|
434
|
+
/** git 계열 패키지의 위치 (어댑터 루트 기준). 어댑터 설치기 스킴은 필요 없다 */
|
|
435
|
+
into: z.string().regex(/^[^/\\][^\\]*$/, "into \uB294 \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C (POSIX)").optional(),
|
|
436
|
+
/** 설치 후 실행할 셸 명령. --yes 일 때만 실행 */
|
|
137
437
|
install: z.string().optional()
|
|
138
438
|
});
|
|
139
439
|
var PACKAGES = "packages";
|
|
@@ -150,7 +450,7 @@ var ManifestSchema = z.object({
|
|
|
150
450
|
});
|
|
151
451
|
var ManifestError = class extends Error {
|
|
152
452
|
};
|
|
153
|
-
function parseManifest(text, knownCategories2) {
|
|
453
|
+
function parseManifest(text, knownCategories2, knownSchemes) {
|
|
154
454
|
const raw = YAML.parse(text);
|
|
155
455
|
const result = ManifestSchema.safeParse(raw);
|
|
156
456
|
if (!result.success) {
|
|
@@ -169,7 +469,7 @@ ${lines.join("\n")}`);
|
|
|
169
469
|
if (seen.has(c.id)) problems.push(`${cat}: id "${c.id}" \uC911\uBCF5`);
|
|
170
470
|
seen.add(c.id);
|
|
171
471
|
try {
|
|
172
|
-
parseSource(effectiveSource(cat, c));
|
|
472
|
+
if (!isComponentSource(parseSource(effectiveSource(cat, c)))) problems.push(`${cat}/${c.id}: \uBD80\uD488 \uCD9C\uCC98\uB294 file:/github:/git: \uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
|
|
173
473
|
} catch (e) {
|
|
174
474
|
problems.push(`${cat}/${c.id}: ${e.message}`);
|
|
175
475
|
}
|
|
@@ -180,7 +480,11 @@ ${lines.join("\n")}`);
|
|
|
180
480
|
if (pkgIds.has(p.id)) problems.push(`packages: id "${p.id}" \uC911\uBCF5`);
|
|
181
481
|
pkgIds.add(p.id);
|
|
182
482
|
try {
|
|
183
|
-
|
|
483
|
+
const src = parseSource(p.source);
|
|
484
|
+
const scheme = src.scheme === "other" ? src.name : src.scheme;
|
|
485
|
+
if (scheme === "file") problems.push(`packages/${p.id}: \uD328\uD0A4\uC9C0 \uCD9C\uCC98\uB294 file: \uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
486
|
+
else if (knownSchemes && !knownSchemes.includes(scheme)) problems.push(`packages/${p.id}: \uC2A4\uD0B4 "${scheme}" \uC744 \uB2E4\uB8F0 \uC124\uCE58\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (${knownSchemes.join(", ")})`);
|
|
487
|
+
if ((scheme === "github" || scheme === "git") && !p.into) problems.push(`packages/${p.id}: ${scheme}: \uD328\uD0A4\uC9C0\uB294 into \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`);
|
|
184
488
|
} catch (e) {
|
|
185
489
|
problems.push(`packages/${p.id}: ${e.message}`);
|
|
186
490
|
}
|
|
@@ -216,10 +520,10 @@ function packagesOf(m, profile) {
|
|
|
216
520
|
}
|
|
217
521
|
|
|
218
522
|
// src/resolvers/file.ts
|
|
219
|
-
import
|
|
523
|
+
import path6 from "path";
|
|
220
524
|
function resolveSource(shed, raw) {
|
|
221
525
|
const s = parseSource(raw);
|
|
222
|
-
if (s.scheme === "file") return
|
|
526
|
+
if (s.scheme === "file") return path6.resolve(shed, s.path);
|
|
223
527
|
throw new Error(`"${raw}": ${s.scheme}: \uCD9C\uCC98\uB294 v0.2\uC5D0\uC11C \uC9C0\uC6D0\uB429\uB2C8\uB2E4. \uC9C0\uAE08\uC740 file: \uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
224
528
|
}
|
|
225
529
|
function isSaveable(raw) {
|
|
@@ -227,8 +531,8 @@ function isSaveable(raw) {
|
|
|
227
531
|
}
|
|
228
532
|
|
|
229
533
|
// src/state.ts
|
|
230
|
-
import { promises as
|
|
231
|
-
import
|
|
534
|
+
import { promises as fs5 } from "fs";
|
|
535
|
+
import path7 from "path";
|
|
232
536
|
import { z as z2 } from "zod";
|
|
233
537
|
var StateSchema = z2.object({
|
|
234
538
|
profile: z2.string(),
|
|
@@ -239,11 +543,11 @@ var StateSchema = z2.object({
|
|
|
239
543
|
});
|
|
240
544
|
var LSHED_DIR = "lshed";
|
|
241
545
|
function statePath(adapter) {
|
|
242
|
-
return
|
|
546
|
+
return path7.join(adapter.root, LSHED_DIR, "state.json");
|
|
243
547
|
}
|
|
244
548
|
async function readState(adapter) {
|
|
245
549
|
try {
|
|
246
|
-
const raw = JSON.parse(await
|
|
550
|
+
const raw = JSON.parse(await fs5.readFile(statePath(adapter), "utf8"));
|
|
247
551
|
return StateSchema.parse(raw);
|
|
248
552
|
} catch (e) {
|
|
249
553
|
if (e.code === "ENOENT") return null;
|
|
@@ -252,36 +556,33 @@ async function readState(adapter) {
|
|
|
252
556
|
}
|
|
253
557
|
async function writeState(adapter, state) {
|
|
254
558
|
const p = statePath(adapter);
|
|
255
|
-
await
|
|
256
|
-
await
|
|
559
|
+
await fs5.mkdir(path7.dirname(p), { recursive: true });
|
|
560
|
+
await fs5.writeFile(p, JSON.stringify(state, null, 2) + "\n");
|
|
257
561
|
}
|
|
258
562
|
|
|
259
|
-
// src/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
".pytest_cache",
|
|
267
|
-
".DS_Store",
|
|
268
|
-
"*.log"
|
|
269
|
-
];
|
|
270
|
-
function matches(name, pattern) {
|
|
271
|
-
if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
|
|
272
|
-
return name === pattern;
|
|
563
|
+
// src/core/context.ts
|
|
564
|
+
function spawnExec(cmd, args, cwd) {
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
const p = spawn2(cmd, args, { cwd, stdio: "inherit" });
|
|
567
|
+
p.on("error", reject);
|
|
568
|
+
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} \uAC00 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4`)));
|
|
569
|
+
});
|
|
273
570
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
571
|
+
function installersFor(ctx) {
|
|
572
|
+
return [gitInstaller, ...ctx.adapter.installers()].sort((a, b) => a.priority - b.priority);
|
|
573
|
+
}
|
|
574
|
+
function installerFor(ctx, source) {
|
|
575
|
+
const s = parseSource(source);
|
|
576
|
+
const scheme = s.scheme === "other" ? s.name : s.scheme;
|
|
577
|
+
const inst = installersFor(ctx).find((i) => i.schemes.includes(scheme));
|
|
578
|
+
if (!inst) throw new Error(`"${source}": \uC2A4\uD0B4 ${scheme} \uC744 \uB2E4\uB8F0 \uC124\uCE58\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
579
|
+
return inst;
|
|
277
580
|
}
|
|
278
|
-
|
|
279
|
-
// src/core/context.ts
|
|
280
581
|
var MANIFEST_FILE = "lshed.yaml";
|
|
281
582
|
var INSTRUCTIONS = "instructions";
|
|
282
583
|
var FRAGMENTS_DIR = `${LSHED_DIR}/instructions`;
|
|
283
584
|
function manifestPath(ctx) {
|
|
284
|
-
return
|
|
585
|
+
return path8.join(ctx.shed, MANIFEST_FILE);
|
|
285
586
|
}
|
|
286
587
|
function knownCategories(adapter) {
|
|
287
588
|
return [...adapter.categories().map((c) => c.name), INSTRUCTIONS];
|
|
@@ -289,12 +590,12 @@ function knownCategories(adapter) {
|
|
|
289
590
|
async function loadManifest(ctx) {
|
|
290
591
|
let text;
|
|
291
592
|
try {
|
|
292
|
-
text = await
|
|
593
|
+
text = await fs6.readFile(manifestPath(ctx), "utf8");
|
|
293
594
|
} catch {
|
|
294
595
|
throw new Error(`\uCC3D\uACE0\uC5D0 ${MANIFEST_FILE} \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${ctx.shed}
|
|
295
596
|
\uBA3C\uC800 'lshed init --shed ${ctx.shed}' \uB97C \uC2E4\uD589\uD558\uC138\uC694.`);
|
|
296
597
|
}
|
|
297
|
-
const m = parseManifest(text, knownCategories(ctx.adapter));
|
|
598
|
+
const m = parseManifest(text, knownCategories(ctx.adapter), installersFor(ctx).flatMap((i) => [...i.schemes]));
|
|
298
599
|
ctx.ignore = [...DEFAULT_IGNORE, ...m.ignore ?? []];
|
|
299
600
|
return m;
|
|
300
601
|
}
|
|
@@ -334,103 +635,18 @@ function planProfile(ctx, m, profile) {
|
|
|
334
635
|
return items;
|
|
335
636
|
}
|
|
336
637
|
function abs(ctx, rel) {
|
|
337
|
-
return
|
|
638
|
+
return path8.join(ctx.adapter.root, ...rel.split("/"));
|
|
338
639
|
}
|
|
339
640
|
|
|
340
|
-
// src/
|
|
341
|
-
import { promises as
|
|
342
|
-
import
|
|
343
|
-
import path5 from "path";
|
|
344
|
-
async function exists(p) {
|
|
345
|
-
try {
|
|
346
|
-
await fs4.access(p);
|
|
347
|
-
return true;
|
|
348
|
-
} catch {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async function isDir(p) {
|
|
353
|
-
try {
|
|
354
|
-
return (await fs4.stat(p)).isDirectory();
|
|
355
|
-
} catch {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
async function listFiles(root, ignore = DEFAULT_IGNORE) {
|
|
360
|
-
if (!await exists(root)) return [];
|
|
361
|
-
if (!await isDir(root)) return [""];
|
|
362
|
-
const out = [];
|
|
363
|
-
async function walk(dir, rel) {
|
|
364
|
-
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
365
|
-
for (const e of entries) {
|
|
366
|
-
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
367
|
-
if (isIgnored(r, ignore)) continue;
|
|
368
|
-
const full = path5.join(dir, e.name);
|
|
369
|
-
let st;
|
|
370
|
-
try {
|
|
371
|
-
st = await fs4.stat(full);
|
|
372
|
-
} catch {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (st.isDirectory()) await walk(full, r);
|
|
376
|
-
else if (st.isFile()) out.push(r);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
await walk(root, "");
|
|
380
|
-
return out.sort();
|
|
381
|
-
}
|
|
382
|
-
async function hashFile(p) {
|
|
383
|
-
return createHash("sha256").update(await fs4.readFile(p)).digest("hex");
|
|
384
|
-
}
|
|
385
|
-
async function hashTree(root, ignore = DEFAULT_IGNORE) {
|
|
386
|
-
if (!await exists(root)) return null;
|
|
387
|
-
const h = createHash("sha256");
|
|
388
|
-
for (const rel of await listFiles(root, ignore)) {
|
|
389
|
-
h.update(rel).update("\0").update(await fs4.readFile(rel ? path5.join(root, rel) : root)).update("\0");
|
|
390
|
-
}
|
|
391
|
-
return h.digest("hex");
|
|
392
|
-
}
|
|
393
|
-
async function copyTree(src, dst, ignore = DEFAULT_IGNORE) {
|
|
394
|
-
await fs4.rm(dst, { recursive: true, force: true });
|
|
395
|
-
await fs4.mkdir(path5.dirname(dst), { recursive: true });
|
|
396
|
-
const srcRoot = path5.resolve(src);
|
|
397
|
-
await fs4.cp(src, dst, {
|
|
398
|
-
recursive: true,
|
|
399
|
-
dereference: true,
|
|
400
|
-
filter: async (from) => {
|
|
401
|
-
const rel = path5.relative(srcRoot, path5.resolve(from)).split(path5.sep).join("/");
|
|
402
|
-
if (isIgnored(rel, ignore)) return false;
|
|
403
|
-
try {
|
|
404
|
-
if ((await fs4.lstat(from)).isSymbolicLink()) await fs4.stat(from);
|
|
405
|
-
} catch {
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
async function removeTree(p) {
|
|
413
|
-
await fs4.rm(p, { recursive: true, force: true });
|
|
414
|
-
}
|
|
415
|
-
async function diffTrees(local, shed, ignore = DEFAULT_IGNORE) {
|
|
416
|
-
const l = new Set(await listFiles(local, ignore));
|
|
417
|
-
const s = new Set(await listFiles(shed, ignore));
|
|
418
|
-
const out = [];
|
|
419
|
-
for (const f of [.../* @__PURE__ */ new Set([...l, ...s])].sort()) {
|
|
420
|
-
const lp = f ? path5.join(local, f) : local;
|
|
421
|
-
const sp = f ? path5.join(shed, f) : shed;
|
|
422
|
-
if (l.has(f) && !s.has(f)) out.push({ status: "A", file: f });
|
|
423
|
-
else if (!l.has(f) && s.has(f)) out.push({ status: "D", file: f });
|
|
424
|
-
else if (await hashFile(lp) !== await hashFile(sp)) out.push({ status: "M", file: f });
|
|
425
|
-
}
|
|
426
|
-
return out;
|
|
427
|
-
}
|
|
641
|
+
// src/core/init.ts
|
|
642
|
+
import { promises as fs9 } from "fs";
|
|
643
|
+
import path12 from "path";
|
|
428
644
|
|
|
429
645
|
// src/core/instructions.ts
|
|
430
|
-
import
|
|
646
|
+
import path9 from "path";
|
|
431
647
|
var MARKER = "<!-- generated by lshed";
|
|
432
648
|
function instructionsFile(ctx) {
|
|
433
|
-
return
|
|
649
|
+
return path9.join(ctx.adapter.root, ctx.adapter.instructionsFileName());
|
|
434
650
|
}
|
|
435
651
|
function isGenerated(text) {
|
|
436
652
|
return text.trimStart().startsWith(MARKER);
|
|
@@ -449,49 +665,23 @@ ${f.content.trimEnd()}
|
|
|
449
665
|
}
|
|
450
666
|
|
|
451
667
|
// src/core/packages.ts
|
|
452
|
-
import { promises as
|
|
453
|
-
import
|
|
454
|
-
|
|
455
|
-
// src/git.ts
|
|
456
|
-
import { execFile, spawn } from "child_process";
|
|
457
|
-
import { promisify } from "util";
|
|
458
|
-
import path7 from "path";
|
|
459
|
-
var x = promisify(execFile);
|
|
460
|
-
async function git(args, cwd) {
|
|
461
|
-
const { stdout } = await x("git", args, { cwd, maxBuffer: 1 << 24 });
|
|
462
|
-
return stdout.trim();
|
|
463
|
-
}
|
|
464
|
-
var isRepo = (dir) => exists(path7.join(dir, ".git"));
|
|
465
|
-
var remoteUrl = (dir) => git(["remote", "get-url", "origin"], dir).catch(() => null);
|
|
466
|
-
var head = (dir) => git(["rev-parse", "HEAD"], dir);
|
|
467
|
-
var branch = async (dir) => {
|
|
468
|
-
const b = await git(["rev-parse", "--abbrev-ref", "HEAD"], dir);
|
|
469
|
-
return b === "HEAD" ? void 0 : b;
|
|
470
|
-
};
|
|
471
|
-
var clone = (url, dir, ref) => git(["clone", "--quiet", ...ref ? ["--branch", ref] : [], url, dir]);
|
|
472
|
-
var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
|
|
473
|
-
var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
|
|
474
|
-
function runShell(cmd, cwd) {
|
|
475
|
-
return new Promise((resolve, reject) => {
|
|
476
|
-
const p = spawn("sh", ["-c", cmd], { cwd, stdio: "inherit" });
|
|
477
|
-
p.on("error", reject);
|
|
478
|
-
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
|
|
479
|
-
});
|
|
480
|
-
}
|
|
668
|
+
import { promises as fs8 } from "fs";
|
|
669
|
+
import path11 from "path";
|
|
481
670
|
|
|
482
671
|
// src/lock.ts
|
|
483
|
-
import { promises as
|
|
484
|
-
import
|
|
672
|
+
import { promises as fs7 } from "fs";
|
|
673
|
+
import path10 from "path";
|
|
485
674
|
import YAML2 from "yaml";
|
|
486
675
|
import { z as z3 } from "zod";
|
|
676
|
+
var EntrySchema = z3.object({ source: z3.string(), rev: z3.string().optional(), commit: z3.string().optional() }).transform((e) => ({ source: e.source, rev: e.rev ?? e.commit ?? "" }));
|
|
487
677
|
var LockSchema = z3.object({
|
|
488
678
|
version: z3.literal(1),
|
|
489
|
-
packages: z3.record(z3.string(),
|
|
679
|
+
packages: z3.record(z3.string(), EntrySchema).default({})
|
|
490
680
|
});
|
|
491
681
|
var LOCK_FILE = "lshed.lock";
|
|
492
682
|
async function readLock(shed) {
|
|
493
683
|
try {
|
|
494
|
-
return LockSchema.parse(YAML2.parse(await
|
|
684
|
+
return LockSchema.parse(YAML2.parse(await fs7.readFile(path10.join(shed, LOCK_FILE), "utf8")));
|
|
495
685
|
} catch (e) {
|
|
496
686
|
if (e.code === "ENOENT") return { version: 1, packages: {} };
|
|
497
687
|
throw new Error(`${LOCK_FILE} \uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${e.message}`);
|
|
@@ -499,44 +689,39 @@ async function readLock(shed) {
|
|
|
499
689
|
}
|
|
500
690
|
async function writeLock(shed, lock) {
|
|
501
691
|
const sorted = { version: 1, packages: Object.fromEntries(Object.entries(lock.packages).sort()) };
|
|
502
|
-
await
|
|
692
|
+
await fs7.writeFile(path10.join(shed, LOCK_FILE), "# generated by lshed \u2014 do not edit; 'lshed update' refreshes it\n" + YAML2.stringify(sorted));
|
|
503
693
|
}
|
|
504
694
|
|
|
505
695
|
// src/core/packages.ts
|
|
506
696
|
async function detectPackages(ctx, found) {
|
|
507
697
|
const out = [];
|
|
508
|
-
for (const
|
|
509
|
-
if (!await isRepo(f.path)) continue;
|
|
510
|
-
const url = await remoteUrl(f.path);
|
|
511
|
-
if (!url) continue;
|
|
512
|
-
const into = path9.relative(ctx.adapter.root, f.path).split(path9.sep).join("/");
|
|
513
|
-
out.push({ id: f.id, into, source: sourceFromRemote(url, await branch(f.path)), commit: await head(f.path), path: f.path });
|
|
514
|
-
}
|
|
698
|
+
for (const inst of installersFor(ctx)) out.push(...await inst.detect(ctx, found));
|
|
515
699
|
return out;
|
|
516
700
|
}
|
|
517
701
|
async function detectGenerated(found, pkgs) {
|
|
518
702
|
const out = /* @__PURE__ */ new Map();
|
|
519
|
-
|
|
520
|
-
|
|
703
|
+
const located = pkgs.filter((p) => p.path);
|
|
704
|
+
if (!located.length) return out;
|
|
705
|
+
const roots = await Promise.all(located.map(async (p) => ({ id: p.id, real: await fs8.realpath(p.path) })));
|
|
521
706
|
for (const f of found) {
|
|
522
|
-
if (
|
|
707
|
+
if (located.some((p) => p.path === f.path)) continue;
|
|
523
708
|
let entries;
|
|
524
709
|
try {
|
|
525
|
-
if (!(await
|
|
526
|
-
entries = await
|
|
710
|
+
if (!(await fs8.stat(f.path)).isDirectory()) continue;
|
|
711
|
+
entries = await fs8.readdir(f.path);
|
|
527
712
|
} catch {
|
|
528
713
|
continue;
|
|
529
714
|
}
|
|
530
715
|
for (const name of entries) {
|
|
531
|
-
const p =
|
|
716
|
+
const p = path11.join(f.path, name);
|
|
532
717
|
let target;
|
|
533
718
|
try {
|
|
534
|
-
if (!(await
|
|
535
|
-
target = await
|
|
719
|
+
if (!(await fs8.lstat(p)).isSymbolicLink()) continue;
|
|
720
|
+
target = await fs8.realpath(p).catch(async () => path11.resolve(f.path, await fs8.readlink(p)));
|
|
536
721
|
} catch {
|
|
537
722
|
continue;
|
|
538
723
|
}
|
|
539
|
-
const owner = roots.find((r) => target === r.real || target.startsWith(r.real +
|
|
724
|
+
const owner = roots.find((r) => target === r.real || target.startsWith(r.real + path11.sep));
|
|
540
725
|
if (owner) {
|
|
541
726
|
out.set(`${f.category}/${f.id}`, owner.id);
|
|
542
727
|
break;
|
|
@@ -546,36 +731,34 @@ async function detectGenerated(found, pkgs) {
|
|
|
546
731
|
return out;
|
|
547
732
|
}
|
|
548
733
|
async function packageStatus(ctx, pkg, lock) {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
734
|
+
const st = await installerFor(ctx, pkg.source).status(ctx, pkg);
|
|
735
|
+
return { pkg, ...st, locked: lock.packages[pkg.id]?.rev || void 0 };
|
|
736
|
+
}
|
|
737
|
+
var short = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
|
|
738
|
+
function ordered(ctx, pkgs) {
|
|
739
|
+
return pkgs.map((p, i) => ({ p, i, pr: installerFor(ctx, p.source).priority })).sort((a, b) => a.pr - b.pr || a.i - b.i).map((x2) => x2.p);
|
|
552
740
|
}
|
|
553
741
|
async function ensurePackages(ctx, pkgs, opts = {}) {
|
|
554
742
|
const lock = await readLock(ctx.shed);
|
|
555
|
-
const res = {
|
|
556
|
-
for (const pkg of pkgs) {
|
|
743
|
+
const res = { installed: [], pendingInstalls: [], lockChanged: false };
|
|
744
|
+
for (const pkg of ordered(ctx, pkgs)) {
|
|
745
|
+
const inst = installerFor(ctx, pkg.source);
|
|
557
746
|
const st = await packageStatus(ctx, pkg, lock);
|
|
558
747
|
if (st.present) {
|
|
559
|
-
const note = st.locked && st.
|
|
748
|
+
const note = st.locked && st.rev !== st.locked ? ` (${short(st.rev)} \u2260 lock ${short(st.locked)})` : "";
|
|
560
749
|
ctx.log(` = package ${pkg.id}${note}`);
|
|
561
750
|
continue;
|
|
562
751
|
}
|
|
563
|
-
|
|
564
|
-
const { url, ref } = cloneTarget(parseSource(pkg.source));
|
|
565
|
-
ctx.log(` + package ${pkg.id} (clone ${url}${ref ? ` @${ref}` : ""}${st.locked ? ` \u2192 ${st.locked.slice(0, 7)}` : ""})`);
|
|
752
|
+
ctx.log(` + package ${pkg.id} (${inst.describe(pkg, st.locked)})`);
|
|
566
753
|
if (opts.dryRun) continue;
|
|
567
|
-
await
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
throw new Error(`package ${pkg.id}: \uB77D\uC758 \uCEE4\uBC0B ${st.locked.slice(0, 7)} \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 'lshed update ${pkg.id}' \uB85C \uB77D\uC744 \uAC31\uC2E0\uD558\uC138\uC694.`);
|
|
572
|
-
});
|
|
573
|
-
} else {
|
|
574
|
-
lock.packages[pkg.id] = { source: pkg.source, commit: await head(st.dir) };
|
|
754
|
+
const rev = await inst.install(ctx, pkg, st.locked, opts);
|
|
755
|
+
if (rev !== st.locked) {
|
|
756
|
+
if (st.locked) ctx.log(` \uB77D\uC740 ${short(st.locked)} \uC774\uC9C0\uB9CC ${short(rev)} \uC774 \uC124\uCE58\uB428. \uB77D\uC744 \uB530\uB77C\uAC00\uAC8C \uAC31\uC2E0`);
|
|
757
|
+
lock.packages[pkg.id] = { source: pkg.source, rev };
|
|
575
758
|
res.lockChanged = true;
|
|
576
759
|
}
|
|
577
|
-
res.
|
|
578
|
-
|
|
760
|
+
res.installed.push(pkg.id);
|
|
761
|
+
await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
|
|
579
762
|
}
|
|
580
763
|
if (res.lockChanged) await writeLock(ctx.shed, lock);
|
|
581
764
|
return res;
|
|
@@ -583,7 +766,7 @@ async function ensurePackages(ctx, pkgs, opts = {}) {
|
|
|
583
766
|
async function maybeInstall(ctx, pkg, dir, opts, res) {
|
|
584
767
|
if (!pkg.install) return;
|
|
585
768
|
if (opts.yes) {
|
|
586
|
-
ctx.log(` $ (${
|
|
769
|
+
ctx.log(` $ (${path11.relative(ctx.adapter.root, dir) || "."}) ${pkg.install}`);
|
|
587
770
|
await runShell(pkg.install, dir);
|
|
588
771
|
} else {
|
|
589
772
|
res.pendingInstalls.push({ id: pkg.id, dir, cmd: pkg.install });
|
|
@@ -597,27 +780,27 @@ function reportPending(ctx, res) {
|
|
|
597
780
|
}
|
|
598
781
|
async function updatePackages(ctx, pkgs, opts = {}) {
|
|
599
782
|
const lock = await readLock(ctx.shed);
|
|
600
|
-
const res = {
|
|
601
|
-
for (const pkg of pkgs) {
|
|
783
|
+
const res = { installed: [], pendingInstalls: [], lockChanged: false };
|
|
784
|
+
for (const pkg of ordered(ctx, pkgs)) {
|
|
785
|
+
const inst = installerFor(ctx, pkg.source);
|
|
602
786
|
const st = await packageStatus(ctx, pkg, lock);
|
|
603
787
|
if (!st.present) {
|
|
604
788
|
ctx.log(` ! package ${pkg.id}: \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC74C. \uBA3C\uC800 restore \uD558\uC138\uC694`);
|
|
605
789
|
continue;
|
|
606
790
|
}
|
|
607
791
|
if (opts.dryRun) {
|
|
608
|
-
ctx.log(` ~ package ${pkg.id} (
|
|
792
|
+
ctx.log(` ~ package ${pkg.id} (${inst.name} update)`);
|
|
609
793
|
continue;
|
|
610
794
|
}
|
|
611
|
-
await
|
|
612
|
-
const
|
|
613
|
-
const before = lock.packages[pkg.id]?.commit;
|
|
795
|
+
const now = await inst.update(ctx, pkg, opts);
|
|
796
|
+
const before = lock.packages[pkg.id]?.rev;
|
|
614
797
|
if (now !== before) {
|
|
615
|
-
lock.packages[pkg.id] = { source: pkg.source,
|
|
798
|
+
lock.packages[pkg.id] = { source: pkg.source, rev: now };
|
|
616
799
|
res.lockChanged = true;
|
|
617
|
-
ctx.log(` \u2191 package ${pkg.id} ${before ? before
|
|
618
|
-
await maybeInstall(ctx, pkg,
|
|
800
|
+
ctx.log(` \u2191 package ${pkg.id} ${before ? short(before) : "(\uC5C6\uC74C)"} \u2192 ${short(now)}`);
|
|
801
|
+
await maybeInstall(ctx, pkg, inst.cwd(ctx, pkg), opts, res);
|
|
619
802
|
} else {
|
|
620
|
-
ctx.log(` = package ${pkg.id} ${now
|
|
803
|
+
ctx.log(` = package ${pkg.id} ${short(now)} (\uCD5C\uC2E0)`);
|
|
621
804
|
}
|
|
622
805
|
}
|
|
623
806
|
if (res.lockChanged) await writeLock(ctx.shed, lock);
|
|
@@ -640,8 +823,9 @@ async function init(ctx, opts = {}) {
|
|
|
640
823
|
const generated = await detectGenerated(all, pkgs);
|
|
641
824
|
const found = all.filter((f) => !pkgs.some((p) => p.path === f.path) && !generated.has(`${f.category}/${f.id}`));
|
|
642
825
|
for (const p of pkgs) {
|
|
643
|
-
m.packages.push({ id: p.id, source: p.source, into: p.into });
|
|
644
|
-
|
|
826
|
+
m.packages.push(p.into ? { id: p.id, source: p.source, into: p.into } : { id: p.id, source: p.source });
|
|
827
|
+
const rev = /^[0-9a-f]{40}$/.test(p.rev) ? p.rev.slice(0, 7) : p.rev;
|
|
828
|
+
ctx.log(` \u2261 package ${p.id} ${p.source} @${rev} (\uCC38\uC870\uB9CC \uAE30\uB85D)`);
|
|
645
829
|
}
|
|
646
830
|
if (pkgs.length) m.profiles[profileName][PACKAGES] = pkgs.map((p) => p.id);
|
|
647
831
|
for (const [key, by] of generated) ctx.log(` \xB7 ${key} (${by} \uAC00 \uC0DD\uC131\uD55C \uAC83 \u2192 \uAC74\uB108\uB700)`);
|
|
@@ -657,7 +841,7 @@ async function init(ctx, opts = {}) {
|
|
|
657
841
|
m.components[cat.name] = [];
|
|
658
842
|
m.profiles[profileName][cat.name] = [];
|
|
659
843
|
for (const f of mine) {
|
|
660
|
-
const dst =
|
|
844
|
+
const dst = path12.join(ctx.shed, cat.root, cat.kind === "dir" ? f.id : `${f.id}.md`);
|
|
661
845
|
await copyTree(f.path, dst, ignoreOf(ctx));
|
|
662
846
|
m.components[cat.name].push({ id: f.id });
|
|
663
847
|
m.profiles[profileName][cat.name].push(f.id);
|
|
@@ -668,32 +852,33 @@ async function init(ctx, opts = {}) {
|
|
|
668
852
|
}
|
|
669
853
|
const instr = instructionsFile(ctx);
|
|
670
854
|
if (await exists(instr)) {
|
|
671
|
-
const text = await
|
|
855
|
+
const text = await fs9.readFile(instr, "utf8");
|
|
672
856
|
if (!isGenerated(text)) {
|
|
673
|
-
const dst =
|
|
674
|
-
await
|
|
675
|
-
await
|
|
857
|
+
const dst = path12.join(ctx.shed, INSTRUCTIONS, "main.md");
|
|
858
|
+
await fs9.mkdir(path12.dirname(dst), { recursive: true });
|
|
859
|
+
await fs9.writeFile(dst, text);
|
|
676
860
|
const fragRel = targetRel(INSTRUCTIONS, "main");
|
|
677
861
|
await copyTree(dst, abs(ctx, fragRel), ignoreOf(ctx));
|
|
678
862
|
managed.push(fragRel);
|
|
679
863
|
m.components[INSTRUCTIONS] = [{ id: "main" }];
|
|
680
864
|
m.profiles[profileName][INSTRUCTIONS] = ["main"];
|
|
681
865
|
copied++;
|
|
682
|
-
ctx.log(` + ${INSTRUCTIONS}/main (${
|
|
866
|
+
ctx.log(` + ${INSTRUCTIONS}/main (${path12.basename(instr)})`);
|
|
683
867
|
}
|
|
684
868
|
}
|
|
685
|
-
await
|
|
869
|
+
await fs9.mkdir(ctx.shed, { recursive: true });
|
|
686
870
|
let yamlText = stringifyManifest(m);
|
|
687
871
|
for (const p of pkgs) {
|
|
872
|
+
if (!p.into) continue;
|
|
688
873
|
yamlText = yamlText.replace(` into: ${p.into}
|
|
689
874
|
`, ` into: ${p.into}
|
|
690
875
|
# install: ./setup # \u2190 \uBCF5\uC6D0 \uD6C4 \uC2E4\uD589\uD560 \uBA85\uB839\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC138\uC694 (--yes \uB85C \uC2E4\uD589)
|
|
691
876
|
`);
|
|
692
877
|
}
|
|
693
|
-
await
|
|
878
|
+
await fs9.writeFile(manifestPath(ctx), `# lshed manifest \u2014 edit freely. Reference: https://github.com/LeeSongHeon-LSH/lshed
|
|
694
879
|
` + yamlText);
|
|
695
880
|
if (pkgs.length) {
|
|
696
|
-
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source,
|
|
881
|
+
await writeLock(ctx.shed, { version: 1, packages: Object.fromEntries(pkgs.map((p) => [p.id, { source: p.source, rev: p.rev }])) });
|
|
697
882
|
}
|
|
698
883
|
await writeState(ctx.adapter, { profile: profileName, shed: ctx.shed, managed, appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
699
884
|
const parts = [`\uBD80\uD488 ${copied}\uAC1C`];
|
|
@@ -702,13 +887,13 @@ async function init(ctx, opts = {}) {
|
|
|
702
887
|
if (skipped.length) parts.push(`\uC81C\uC678 ${skipped.length}\uAC1C`);
|
|
703
888
|
ctx.log(`
|
|
704
889
|
${MANIFEST_FILE} \uC0DD\uC131: ${manifestPath(ctx)} (${parts.join(", ")}, \uD504\uB85C\uD544 "${profileName}")`);
|
|
705
|
-
if (pkgs.
|
|
890
|
+
if (pkgs.some((p) => p.into)) ctx.log(`git \uD328\uD0A4\uC9C0\uC758 \uC124\uCE58 \uBA85\uB839(install:)\uC740 lshed.yaml \uC5D0\uC11C \uC9C1\uC811 \uCC44\uC6B0\uC138\uC694.`);
|
|
706
891
|
return { manifest: m, copied, skipped, packages: pkgs.map((p) => p.id), generated: [...generated.keys()] };
|
|
707
892
|
}
|
|
708
893
|
|
|
709
894
|
// src/core/restore.ts
|
|
710
|
-
import { promises as
|
|
711
|
-
import
|
|
895
|
+
import { promises as fs10 } from "fs";
|
|
896
|
+
import path13 from "path";
|
|
712
897
|
async function restore(ctx, profileArg, opts = {}) {
|
|
713
898
|
const backup = opts.backup ?? true;
|
|
714
899
|
const state = await readState(ctx.adapter);
|
|
@@ -727,7 +912,7 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
727
912
|
const oldManaged = new Set(state?.managed ?? []);
|
|
728
913
|
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
729
914
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
730
|
-
const backupDir =
|
|
915
|
+
const backupDir = path13.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
731
916
|
const backedUp = [];
|
|
732
917
|
const placed = [];
|
|
733
918
|
async function backUp(rel) {
|
|
@@ -735,7 +920,7 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
735
920
|
if (!await exists(from)) return;
|
|
736
921
|
backedUp.push(rel);
|
|
737
922
|
if (opts.dryRun || !backup) return;
|
|
738
|
-
await copyTree(from,
|
|
923
|
+
await copyTree(from, path13.join(backupDir, ...rel.split("/")), ignoreOf(ctx));
|
|
739
924
|
}
|
|
740
925
|
for (const rel of toRemove) {
|
|
741
926
|
ctx.log(` - ${rel}`);
|
|
@@ -758,14 +943,14 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
758
943
|
const instrPath = instructionsFile(ctx);
|
|
759
944
|
if (fragments.length) {
|
|
760
945
|
const contents = [];
|
|
761
|
-
for (const f of fragments) contents.push({ id: f.id, content: await
|
|
946
|
+
for (const f of fragments) contents.push({ id: f.id, content: await fs10.readFile(f.src, "utf8") });
|
|
762
947
|
const rendered = renderInstructions(ctx, profile, contents);
|
|
763
|
-
const existing = await exists(instrPath) ? await
|
|
948
|
+
const existing = await exists(instrPath) ? await fs10.readFile(instrPath, "utf8") : null;
|
|
764
949
|
if (existing !== rendered) {
|
|
765
950
|
const mark = existing === null ? "+" : "~";
|
|
766
951
|
ctx.log(` ${mark} ${instrRel}${existing !== null && !isGenerated(existing) ? " (\uAE30\uC874 \uD30C\uC77C\uC740 lshed \uC0DD\uC131\uBB3C\uC774 \uC544\uB2D8 \u2192 \uBC31\uC5C5)" : ""}`);
|
|
767
952
|
if (existing !== null) await backUp(instrRel);
|
|
768
|
-
if (!opts.dryRun) await
|
|
953
|
+
if (!opts.dryRun) await fs10.writeFile(instrPath, rendered);
|
|
769
954
|
} else {
|
|
770
955
|
ctx.log(` = ${instrRel}`);
|
|
771
956
|
}
|
|
@@ -780,7 +965,7 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
780
965
|
await writeState(ctx.adapter, { profile, shed: ctx.shed, managed: [...newManaged].sort(), appliedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
781
966
|
const bdir = backup && backedUp.length ? backupDir : null;
|
|
782
967
|
ctx.log(`
|
|
783
|
-
\uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.
|
|
968
|
+
\uD504\uB85C\uD544 "${profile}" \uC801\uC6A9: \uBC30\uCE58 ${placed.length}, \uC81C\uAC70 ${toRemove.length}${pkgRes.installed.length ? `, \uD328\uD0A4\uC9C0 \uC124\uCE58 ${pkgRes.installed.length}` : ""}${bdir ? `, \uBC31\uC5C5 ${backedUp.length} \u2192 ${bdir}` : ""}`);
|
|
784
969
|
reportPending(ctx, pkgRes);
|
|
785
970
|
return { profile, placed, removed: toRemove, backedUp, backupDir: bdir };
|
|
786
971
|
}
|
|
@@ -828,8 +1013,9 @@ function formatStatus(s, adapterRoot) {
|
|
|
828
1013
|
`\uAD00\uB9AC \uC911 ${s.state.managed.length}\uAC1C \uACBD\uB85C (${adapterRoot})`
|
|
829
1014
|
];
|
|
830
1015
|
lines.push(s.drifted.length ? `\uB4DC\uB9AC\uD504\uD2B8 ${s.drifted.length}\uAC1C: ${s.drifted.join(", ")} \u2192 lshed diff` : "\uB4DC\uB9AC\uD504\uD2B8 \uC5C6\uC74C");
|
|
1016
|
+
const short2 = (r) => r && /^[0-9a-f]{40}$/.test(r) ? r.slice(0, 7) : r;
|
|
831
1017
|
for (const p of s.packages) {
|
|
832
|
-
const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${p.
|
|
1018
|
+
const where = !p.present ? "\uC124\uCE58 \uC548 \uB428 \u2192 lshed restore" : !p.locked ? `${short2(p.rev)} (\uB77D \uC5C6\uC74C)` : p.rev === p.locked ? `${short2(p.rev)} = lock` : `${short2(p.rev)} \u2260 lock ${short2(p.locked)} \u2192 lshed update`;
|
|
833
1019
|
lines.push(`\uD328\uD0A4\uC9C0 ${p.pkg.id} ${where}`);
|
|
834
1020
|
}
|
|
835
1021
|
return lines.join("\n");
|
|
@@ -897,8 +1083,8 @@ function formatRows(rows, m) {
|
|
|
897
1083
|
}
|
|
898
1084
|
|
|
899
1085
|
// src/core/remove.ts
|
|
900
|
-
import { promises as
|
|
901
|
-
import
|
|
1086
|
+
import { promises as fs11 } from "fs";
|
|
1087
|
+
import path14 from "path";
|
|
902
1088
|
import YAML3, { isSeq, isMap } from "yaml";
|
|
903
1089
|
function resolveKey(m, raw) {
|
|
904
1090
|
const rows = listRows(m);
|
|
@@ -913,7 +1099,7 @@ async function remove(ctx, raw) {
|
|
|
913
1099
|
const { category, id } = resolveKey(m, raw);
|
|
914
1100
|
const users = listRows(m).find((r) => r.category === category && r.id === id).usedBy;
|
|
915
1101
|
if (users.length) throw new Error(`${category}/${id} \uB294 \uD504\uB85C\uD544 ${users.join(", ")} \uC774 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uD504\uB85C\uD544\uC5D0\uC11C \uBE7C\uC138\uC694.`);
|
|
916
|
-
const text = await
|
|
1102
|
+
const text = await fs11.readFile(manifestPath(ctx), "utf8");
|
|
917
1103
|
const doc = YAML3.parseDocument(text);
|
|
918
1104
|
let deleted;
|
|
919
1105
|
if (category === PACKAGES) {
|
|
@@ -935,14 +1121,14 @@ async function remove(ctx, raw) {
|
|
|
935
1121
|
seq.delete(idx);
|
|
936
1122
|
if (!seq.items.length) doc.deleteIn(["components", category]);
|
|
937
1123
|
const src = sourcePath(ctx, category, findComponent(m, category, id));
|
|
938
|
-
const inside = !
|
|
1124
|
+
const inside = !path14.relative(ctx.shed, src).startsWith("..");
|
|
939
1125
|
if (inside && await exists(src)) {
|
|
940
1126
|
await removeTree(src);
|
|
941
1127
|
deleted = src;
|
|
942
1128
|
}
|
|
943
1129
|
ctx.log(` - ${category}/${id}${deleted ? "" : " (\uCC3D\uACE0 \uBC16 \uACBD\uB85C\uB77C \uD30C\uC77C\uC740 \uB450\uC5C8\uC74C)"}`);
|
|
944
1130
|
}
|
|
945
|
-
await
|
|
1131
|
+
await fs11.writeFile(manifestPath(ctx), doc.toString());
|
|
946
1132
|
return { category, id, deleted };
|
|
947
1133
|
}
|
|
948
1134
|
async function prune(ctx, opts = {}) {
|
|
@@ -972,16 +1158,16 @@ var { version } = createRequire(import.meta.url)("../package.json");
|
|
|
972
1158
|
var program = new Command().name("lshed").description("Keep your coding-agent harness (skills, agents, commands, instructions) in a shed and restore it anywhere by profile.").version(version).option("--shed <dir>", "shed directory (default: $LSHED_HOME, then the shed recorded by the last restore)").option("--root <dir>", "agent config root (default: ~/.claude)");
|
|
973
1159
|
function adapterFromOpts() {
|
|
974
1160
|
const { root } = program.opts();
|
|
975
|
-
return new ClaudeCodeAdapter(root ?
|
|
1161
|
+
return new ClaudeCodeAdapter(root ? path15.resolve(root) : void 0);
|
|
976
1162
|
}
|
|
977
1163
|
async function ctxFor(cmd) {
|
|
978
1164
|
const adapter = adapterFromOpts();
|
|
979
1165
|
const { shed: flag } = program.opts();
|
|
980
1166
|
let shed = flag ?? process.env.LSHED_HOME;
|
|
981
1167
|
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
982
|
-
if (!shed && cmd === "init") shed =
|
|
1168
|
+
if (!shed && cmd === "init") shed = path15.join(os2.homedir(), "lshed");
|
|
983
1169
|
if (!shed) throw new Error("\uCC3D\uACE0 \uC704\uCE58\uB97C \uBAA8\uB985\uB2C8\uB2E4. --shed <dir> \uB610\uB294 LSHED_HOME \uC744 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
984
|
-
return { adapter, shed:
|
|
1170
|
+
return { adapter, shed: path15.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
|
|
985
1171
|
}
|
|
986
1172
|
async function run(fn) {
|
|
987
1173
|
try {
|