lshed 0.6.0 → 0.7.2
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 +31 -0
- package/README.md +55 -6
- package/dist/cli.js +162 -90
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.2 — 2026-09-03
|
|
4
|
+
|
|
5
|
+
Joining a machine that already has a harness, found while preparing the Windows check.
|
|
6
|
+
|
|
7
|
+
- `restore` warns before removing parts that a **different** shed had claimed. Running `init` against a scratch shed to look around marks that machine's own parts as managed, so a later restore from the real shed would remove them. They were always backed up, but nothing said why. `lshed scan` is the read-only way to look.
|
|
8
|
+
- No longer crashes with an `EPIPE` stack trace when output is piped into something that exits early (`lshed status | head`).
|
|
9
|
+
- The smoke script now models a machine that already runs the agent: a skill whose name collides with the shed, a skill only that machine has, and its own MCP and settings. It checks that a collision is backed up and replaced, that machine-only parts survive, that nothing is removed, and that `add` pushes them into the shed.
|
|
10
|
+
- README: how to join a machine that already has a setup, and why not to `init` there.
|
|
11
|
+
|
|
12
|
+
## 0.7.1 — 2026-09-03
|
|
13
|
+
|
|
14
|
+
Groundwork for running on Windows and macOS. Not yet verified on a real machine.
|
|
15
|
+
|
|
16
|
+
- Package `install:` commands run through the platform shell (`sh` or `cmd.exe`) instead of a hard-coded `sh -c`.
|
|
17
|
+
- On Windows, `claude` and other wrappers installed as `.cmd` are found by spawning through the shell.
|
|
18
|
+
- Home-directory paths with backslashes are also rewritten to `${HOME}`.
|
|
19
|
+
- `sync` explains what to do when git has no user identity.
|
|
20
|
+
- `npm run smoke` drives the built CLI through init, restore, add, diff, save, profile switch, list and sync in a temporary directory without touching the real `~/.claude`. Use it on a new OS before trusting a real restore.
|
|
21
|
+
- GitHub Actions matrix: ubuntu, macOS, Windows × Node 20, 22. Tests use directory junctions on Windows so no elevated privileges are needed.
|
|
22
|
+
|
|
23
|
+
## 0.7.0 — 2026-09-03
|
|
24
|
+
|
|
25
|
+
`settings.json` travels, without merging.
|
|
26
|
+
|
|
27
|
+
- New category `settings` for Claude Code: each top-level key of `~/.claude/settings.json` (`hooks`, `permissions`, `env`, `model`, `theme`, …) is one component, stored as `settings/<key>.json`. `restore` writes only the keys the profile lists; a profile can carry `permissions` and leave `model` to each machine. `enabledPlugins` is skipped because the plugin packages own it.
|
|
28
|
+
- Absolute paths under the home directory are stored as `${HOME}/…` (MCP entries too), so hook commands survive a different home. Claude Code does not expand variables in `settings.json`, so `restore` expands `${HOME}` and other `${VAR}` there from the shell; MCP placeholders are still left for Claude Code.
|
|
29
|
+
- `env` in settings is a secret map: secret-looking keys are masked.
|
|
30
|
+
- The secret heuristic now matches whole words. `CLAUDE_CODE_MAX_OUTPUT_TOKENS` is not a token; `BYPASS_PERMISSIONS` is not a password.
|
|
31
|
+
- `init` and `add` flag an entry whose value points inside a package (a hook a toolkit's installer wrote) and suggest `exclude:`.
|
|
32
|
+
- The MCP adapter became a generic "one JSON key = one entry" store used by both categories.
|
|
33
|
+
|
|
3
34
|
## 0.6.0 — 2026-09-03
|
|
4
35
|
|
|
5
36
|
- `lshed sync [-m <msg>] [--no-push] [--dry-run]`: commits everything in the shed, `git pull --rebase`, `git push` (setting the upstream the first time). Without `origin` it only commits. When commits come in it says to run `lshed restore`. On a conflict it aborts the rebase, leaves your commit in place and hands you the git command. It warns first if `diff` shows edits you have not saved, and never runs `save` for you.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# lshed
|
|
2
2
|
|
|
3
|
-
Keep your coding-agent harness — skills, subagents, commands, instructions, MCP servers — in a **shed**, and restore it on any machine with one command.
|
|
3
|
+
Keep your coding-agent harness — skills, subagents, commands, instructions, MCP servers, settings — in a **shed**, and restore it on any machine with one command.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
lshed init --shed ~/lshed # scan ~/.claude into a shed + write lshed.yaml
|
|
@@ -17,7 +17,7 @@ lshed adds three first-class ideas on top of "a directory in git":
|
|
|
17
17
|
|
|
18
18
|
| Idea | What it gives you |
|
|
19
19
|
|---|---|
|
|
20
|
-
| **Components** | every skill / agent / command / instruction fragment / MCP server is one named part in the shed |
|
|
20
|
+
| **Components** | every skill / agent / command / instruction fragment / MCP server / settings key is one named part in the shed |
|
|
21
21
|
| **Profiles** | named recipes — `research`, `work`, `minimal` — that pick a subset of parts |
|
|
22
22
|
| **Managed set** | lshed remembers what it placed, so switching profiles removes only its own files and never touches yours |
|
|
23
23
|
|
|
@@ -89,6 +89,31 @@ lshed sync # commit the shed, pull, push
|
|
|
89
89
|
|
|
90
90
|
`save` is the only path from `~/.claude` to the shed, and it only works for parts the shed owns (`file:` sources). `sync` warns if you have unsaved edits so you do not push a shed that is behind your machine.
|
|
91
91
|
|
|
92
|
+
### A machine that already has a setup
|
|
93
|
+
|
|
94
|
+
The common case is not an empty machine: it already runs the agent and has skills, settings and MCP servers of its own. `restore` is built for that. With no prior lshed state it **removes nothing** — it places what the profile lists, and anything it overwrites goes to `~/.claude/lshed/backups/<timestamp>/` first. Parts that exist only on that machine are untouched.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
$ lshed restore default --shed ~/lshed --dry-run
|
|
98
|
+
+ skills/mine
|
|
99
|
+
~ skills/shared # same name, different content → backed up, then replaced
|
|
100
|
+
+ mcp:exa (${EXA_API_KEY})
|
|
101
|
+
~ settings:model
|
|
102
|
+
(dry-run) 변경 없음. 배치 5, 제거 0, 백업 예정 3
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Always run `--dry-run` first. `+` is new, `~` replaces with a backup, `-` removes with a backup. If the plan looks right, drop the flag.
|
|
106
|
+
|
|
107
|
+
Then push that machine's own parts up into the shed and both machines have everything:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
lshed add # lists what this machine has that the shed does not
|
|
111
|
+
lshed add windows-only mcp/my-local-server
|
|
112
|
+
lshed sync
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Do not run `init` on such a machine just to look around. `init` claims what it finds as lshed-managed, so a later `restore` from your real shed would treat those parts as removable (backed up, but removed). Use `lshed scan`, which only prints. If you do it anyway, `restore` warns you before removing anything and `lshed add` is the way out.
|
|
116
|
+
|
|
92
117
|
### A new machine
|
|
93
118
|
|
|
94
119
|
```
|
|
@@ -227,6 +252,9 @@ components:
|
|
|
227
252
|
- id: research-style
|
|
228
253
|
mcp:
|
|
229
254
|
- id: exa # file:./mcp/exa.json — secrets replaced by ${VAR}
|
|
255
|
+
settings:
|
|
256
|
+
- id: permissions # file:./settings/permissions.json — one top-level key of settings.json
|
|
257
|
+
- id: hooks
|
|
230
258
|
|
|
231
259
|
packages:
|
|
232
260
|
- id: gstack
|
|
@@ -241,6 +269,7 @@ profiles:
|
|
|
241
269
|
agents: [reviewer]
|
|
242
270
|
instructions: [base, research-style] # order matters
|
|
243
271
|
mcp: [exa]
|
|
272
|
+
settings: [permissions, hooks]
|
|
244
273
|
teaching:
|
|
245
274
|
skills: [grading-helper]
|
|
246
275
|
commands: [summarize]
|
|
@@ -248,7 +277,7 @@ profiles:
|
|
|
248
277
|
```
|
|
249
278
|
|
|
250
279
|
- Component `source` accepts `file:<path relative to the shed>`. Package `source` accepts `github:owner/repo@ref`, `git:<url>#ref`, `claude-marketplace:<owner/repo>`, `claude-plugin:<name>@<marketplace>`.
|
|
251
|
-
- Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`, `mcp`.
|
|
280
|
+
- Category names come from the adapter. For Claude Code: `skills`, `agents`, `commands`, `instructions`, `mcp`, `settings`.
|
|
252
281
|
- `ignore:` adds to the built-in list of things never copied: `node_modules`, `.git`, `__pycache__`, `.venv`, cache directories, `*.log`. Build output like `dist/` is not ignored by default, since some skills ship it.
|
|
253
282
|
- `exclude:` lists parts that exist locally but must not enter the shed. `init --exclude` writes it.
|
|
254
283
|
|
|
@@ -274,7 +303,7 @@ Claude Code plugins are packages with their own scheme. `init` finds user-scope
|
|
|
274
303
|
|
|
275
304
|
User-scope MCP servers live in `~/.claude.json`, next to machine IDs and session state. lshed treats each server as a component of category `mcp`: the shed holds `mcp/<name>.json`, and `restore` edits only the `mcpServers.<name>` key of `~/.claude.json`, leaving everything else in that file alone.
|
|
276
305
|
|
|
277
|
-
**No secret value enters the shed.** `init` and `add` replace values under `env` and `headers` whose key
|
|
306
|
+
**No secret value enters the shed.** `init` and `add` replace values under `env` and `headers` whose key contains a secret-looking word (`key`, `token`, `secret`, `password`, `auth`, `authorization`, `credential`, `cookie`, `session` — whole words, so `MAX_OUTPUT_TOKENS` is left alone) with a `${VAR}` placeholder:
|
|
278
307
|
|
|
279
308
|
```json
|
|
280
309
|
{ "type": "stdio", "command": "npx", "args": ["-y", "exa-mcp-server"],
|
|
@@ -285,6 +314,24 @@ User-scope MCP servers live in `~/.claude.json`, next to machine IDs and session
|
|
|
285
314
|
|
|
286
315
|
`restore` writes the placeholder as is. Claude Code expands `${VAR}` from the environment when it starts the server, so the value only ever lives in your shell (`export EXA_API_KEY=...` in `~/.zshrc`, or however you manage secrets). `restore` and `status` list the variables the profile needs that are not set. The heuristic is a suggestion: edit the JSON in the shed to add or remove placeholders, and `init` warns when something in `args` or `url` looks like a token. `save` keeps existing placeholders and masks new secret-looking keys, so a rotated key never leaks into the shed by accident. `diff` compares with placeholders as wildcards, so a machine holding real values is not drift.
|
|
287
316
|
|
|
317
|
+
## Settings
|
|
318
|
+
|
|
319
|
+
`~/.claude/settings.json` holds hooks, permissions, `env`, the model, the theme, and some state Claude Code writes for itself. lshed does not merge it. Each **top-level key is one component** of category `settings`: the shed holds `settings/permissions.json`, `settings/hooks.json`, and so on, and `restore` writes exactly those keys, leaving the rest of the file alone. A profile can carry `permissions` and `hooks` and leave `model` to each machine.
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
$ lshed add
|
|
323
|
+
창고에 없는 항목 3개:
|
|
324
|
+
settings/hooks ! 패키지 gstack 안을 가리킵니다. 그 설치가 만든 것이면 exclude 하세요: settings/hooks
|
|
325
|
+
settings/model
|
|
326
|
+
settings/theme
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `enabledPlugins` is never taken: the plugin packages own it, and `restore` rebuilds it by installing them.
|
|
330
|
+
- Absolute paths under your home directory become `${HOME}/…` in the shed, so a hook command written on one machine works on another. Claude Code does not expand variables in `settings.json`, so `restore` fills `${HOME}` and any `${VAR}` itself from your shell; unset variables are reported and left as placeholders.
|
|
331
|
+
- `env` is treated as a secret map: keys that look secret are masked, the rest (`CLAUDE_CODE_MAX_OUTPUT_TOKENS`, …) travel as they are.
|
|
332
|
+
- A value pointing inside a package (a hook a toolkit's installer wrote) is flagged. If the installer recreates it, put it in `exclude:` and let `restore --yes` bring it back.
|
|
333
|
+
- Since the shed owns the whole key, extra permissions you grant locally show up in `diff` and go into the shed with `save`, like any other edit.
|
|
334
|
+
|
|
288
335
|
## Commands
|
|
289
336
|
|
|
290
337
|
```
|
|
@@ -309,7 +356,7 @@ Global options: `--shed <dir>` (or `LSHED_HOME`; after the first restore lshed r
|
|
|
309
356
|
|
|
310
357
|
0. Installs any package in the profile that is missing, at the version in `lshed.lock`.
|
|
311
358
|
1. Removes paths that the **previous** profile placed and the new one doesn't need.
|
|
312
|
-
2. Copies every part of the new profile into place; writes
|
|
359
|
+
2. Copies every part of the new profile into place; writes MCP entries into `~/.claude.json` and settings keys into `settings.json`.
|
|
313
360
|
3. Regenerates the instructions file.
|
|
314
361
|
|
|
315
362
|
Anything it overwrites or removes is backed up first under `~/.claude/lshed/backups/<timestamp>/`, unless you pass `--no-backup`. Files lshed never placed are left alone. `--dry-run` prints the plan and writes nothing.
|
|
@@ -335,9 +382,11 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
|
|
|
335
382
|
lshed.lock package versions (generated)
|
|
336
383
|
skills/<id>/ agents/<id>.md commands/<id>.md instructions/<id>.md
|
|
337
384
|
mcp/<id>.json secrets as ${VAR}
|
|
385
|
+
settings/<id>.json one top-level key each; home paths as ${HOME}
|
|
338
386
|
|
|
339
387
|
~/.claude/
|
|
340
388
|
skills/ agents/ commands/ CLAUDE.md ← placed by restore
|
|
389
|
+
settings.json <id> ← one key per settings component; the rest is untouched
|
|
341
390
|
lshed/state.json ← which profile, which paths are managed
|
|
342
391
|
lshed/instructions/<id>.md ← fragments imported by CLAUDE.md
|
|
343
392
|
lshed/backups/<timestamp>/ ← whatever restore replaced
|
|
@@ -350,7 +399,6 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
|
|
|
350
399
|
|
|
351
400
|
- Secrets beyond "name the variable". Encrypted values, `op://` references and OS keychains are possible later; today lshed is deliberately no better than dotfiles here.
|
|
352
401
|
- Project-scope MCP servers (`.mcp.json`, `~/.claude.json` `projects.*`) and project-scope plugins. They belong to the project.
|
|
353
|
-
- `settings.json` merging (hooks, permissions).
|
|
354
402
|
- Windows and macOS have not been tested. The code avoids platform-specific paths, but treat this as Linux/WSL for now.
|
|
355
403
|
|
|
356
404
|
## Troubleshooting
|
|
@@ -361,6 +409,7 @@ The shed is the source of truth for authored parts: `save` copies local edits ba
|
|
|
361
409
|
- **`status` says a package differs from the lock** — something updated the clone or plugin behind lshed's back (Claude Code auto-updates plugins). `lshed update` records the new version.
|
|
362
410
|
- **`status` keeps listing the same new things** — they are installer aliases or scratch. Add them to `exclude:` in `lshed.yaml`.
|
|
363
411
|
- **restore says an MCP variable is missing** — export it in your shell profile and restart Claude Code. The placeholder in `~/.claude.json` is correct; Claude Code fills it at startup.
|
|
412
|
+
- **restore wrote a hook with the wrong path** — the shed stores home paths as `${HOME}/…`. If a command points elsewhere on this machine, edit the JSON in the shed to use `${HOME}` or another variable and `restore` again.
|
|
364
413
|
- **sync stopped on a conflict** — `cd <shed> && git pull --rebase`, resolve, `git rebase --continue`, then `lshed sync` again.
|
|
365
414
|
|
|
366
415
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import
|
|
6
|
+
import os4 from "os";
|
|
7
7
|
import path19 from "path";
|
|
8
8
|
|
|
9
9
|
// src/adapters/claude-code.ts
|
|
@@ -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;
|
|
@@ -413,7 +448,7 @@ var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
|
|
|
413
448
|
var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
|
|
414
449
|
function runShell(cmd, cwd) {
|
|
415
450
|
return new Promise((resolve, reject) => {
|
|
416
|
-
const p = spawn(
|
|
451
|
+
const p = spawn(cmd, { cwd, stdio: "inherit", shell: true });
|
|
417
452
|
p.on("error", reject);
|
|
418
453
|
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
|
|
419
454
|
});
|
|
@@ -609,7 +644,7 @@ async function writeState(adapter, state) {
|
|
|
609
644
|
// src/core/context.ts
|
|
610
645
|
function spawnExec(cmd, args, cwd) {
|
|
611
646
|
return new Promise((resolve, reject) => {
|
|
612
|
-
const p = spawn2(cmd, args, { cwd, stdio: "inherit" });
|
|
647
|
+
const p = spawn2(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
613
648
|
p.on("error", reject);
|
|
614
649
|
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} \uAC00 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4`)));
|
|
615
650
|
});
|
|
@@ -904,7 +939,9 @@ async function discover(ctx, exclude = []) {
|
|
|
904
939
|
ctx.log(` ! ${cat.name}/${id}: \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790\uAC00 \uC788\uC5B4 \uAC74\uB108\uB700`);
|
|
905
940
|
continue;
|
|
906
941
|
}
|
|
907
|
-
|
|
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 });
|
|
908
945
|
}
|
|
909
946
|
}
|
|
910
947
|
return { items, generated, excluded };
|
|
@@ -928,8 +965,12 @@ import { isSeq, isMap } from "yaml";
|
|
|
928
965
|
|
|
929
966
|
// src/core/entries.ts
|
|
930
967
|
import { promises as fs10 } from "fs";
|
|
968
|
+
import os2 from "os";
|
|
931
969
|
import path14 from "path";
|
|
932
|
-
var
|
|
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
|
+
}
|
|
933
974
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
934
975
|
function placeholdersIn(v) {
|
|
935
976
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -946,27 +987,39 @@ function walk(v, onString, keyPath = []) {
|
|
|
946
987
|
return v;
|
|
947
988
|
}
|
|
948
989
|
var envName = (...parts) => parts.join("_").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
949
|
-
function mask(id, entry, cat) {
|
|
990
|
+
function mask(id, entry, cat, home = os2.homedir()) {
|
|
991
|
+
entry = portable(entry, home);
|
|
950
992
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
951
|
-
const
|
|
952
|
-
for (const sk of cat.secretKeys) {
|
|
953
|
-
const sect = out[sk];
|
|
954
|
-
if (!sect || typeof sect !== "object" || Array.isArray(sect)) continue;
|
|
993
|
+
const maskSection = (sect, envLike) => {
|
|
955
994
|
const masked = {};
|
|
956
995
|
for (const [k, v] of Object.entries(sect)) {
|
|
957
|
-
if (typeof v !== "string" || !
|
|
996
|
+
if (typeof v !== "string" || !isSecretKey(k) || PLACEHOLDER_RE.test(v)) {
|
|
958
997
|
masked[k] = v;
|
|
959
998
|
PLACEHOLDER_RE.lastIndex = 0;
|
|
960
999
|
continue;
|
|
961
1000
|
}
|
|
962
|
-
const name =
|
|
1001
|
+
const name = envLike ? envName(k) : envName(id, k);
|
|
963
1002
|
const scheme = /^(\w+) \S+$/.exec(v);
|
|
964
1003
|
masked[k] = scheme ? `${scheme[1]} \${${name}}` : `\${${name}}`;
|
|
965
1004
|
}
|
|
966
|
-
|
|
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");
|
|
967
1013
|
}
|
|
968
1014
|
return out;
|
|
969
1015
|
}
|
|
1016
|
+
function portable(entry, home = os2.homedir()) {
|
|
1017
|
+
if (!home || home === "/") return entry;
|
|
1018
|
+
return walk(entry, (s) => s === home || s.startsWith(home + "/") || s.startsWith(home + "\\") ? "${HOME}" + s.slice(home.length) : s);
|
|
1019
|
+
}
|
|
1020
|
+
function envWithHome(env = process.env) {
|
|
1021
|
+
return { HOME: os2.homedir(), ...env };
|
|
1022
|
+
}
|
|
970
1023
|
function suspiciousStrings(entry) {
|
|
971
1024
|
const out = [];
|
|
972
1025
|
walk(entry, (s, kp) => {
|
|
@@ -1007,7 +1060,7 @@ function matches2(shed, local) {
|
|
|
1007
1060
|
}
|
|
1008
1061
|
return shed === local;
|
|
1009
1062
|
}
|
|
1010
|
-
function remask(id, local, shed, cat) {
|
|
1063
|
+
function remask(id, local, shed, cat, home = os2.homedir()) {
|
|
1011
1064
|
const keep = (l, s) => {
|
|
1012
1065
|
if (typeof l === "string" && typeof s === "string" && stringMatches(s, l)) return s;
|
|
1013
1066
|
if (Array.isArray(l) && Array.isArray(s)) return l.map((x2, i) => keep(x2, s[i]));
|
|
@@ -1016,7 +1069,7 @@ function remask(id, local, shed, cat) {
|
|
|
1016
1069
|
}
|
|
1017
1070
|
return l;
|
|
1018
1071
|
};
|
|
1019
|
-
return mask(id, shed === null ? local : keep(local, shed), cat);
|
|
1072
|
+
return mask(id, shed === null ? local : keep(local, portable(shed, home)), cat, home);
|
|
1020
1073
|
}
|
|
1021
1074
|
function diffEntry(shed, local) {
|
|
1022
1075
|
const out = [];
|
|
@@ -1086,9 +1139,10 @@ async function ingest(ctx, doc, profile, items, lock) {
|
|
|
1086
1139
|
} else {
|
|
1087
1140
|
const masked = mask(f.id, f.value, f.cat);
|
|
1088
1141
|
await writeEntryFile(path15.join(ctx.shed, f.category, `${f.id}.json`), masked);
|
|
1089
|
-
const vars = placeholdersIn(masked);
|
|
1142
|
+
const vars = placeholdersIn(masked).filter((v) => v !== "HOME");
|
|
1090
1143
|
ctx.log(` + ${f.category}/${f.id}${vars.length ? ` (\uC2DC\uD06C\uB9BF \u2192 ${vars.map((v) => "${" + v + "}").join(", ")})` : ""}`);
|
|
1091
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}`);
|
|
1092
1146
|
}
|
|
1093
1147
|
const comps = seqAt(doc, ["components", f.category]);
|
|
1094
1148
|
if (!comps.items.some((it) => isMap(it) && it.get("id") === f.id)) comps.add(doc.createNode({ id: f.id }));
|
|
@@ -1173,6 +1227,10 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1173
1227
|
if (fragments.length) newManaged.add(instrRel);
|
|
1174
1228
|
const oldManaged = new Set(state?.managed ?? []);
|
|
1175
1229
|
const toRemove = [...oldManaged].filter((r) => !newManaged.has(r)).sort();
|
|
1230
|
+
if (state && state.shed !== ctx.shed && toRemove.length) {
|
|
1231
|
+
ctx.log(`! \uB9C8\uC9C0\uB9C9\uC73C\uB85C \uC801\uC6A9\uD55C \uCC3D\uACE0\uAC00 \uB2E4\uB985\uB2C8\uB2E4: ${state.shed}`);
|
|
1232
|
+
ctx.log(` \uC544\uB798 ${toRemove.length}\uAC1C\uB294 \uADF8 \uCC3D\uACE0\uC758 \uAD00\uB9AC \uBAA9\uB85D\uC5D0 \uC788\uC5B4 \uC81C\uAC70 \uB300\uC0C1\uC785\uB2C8\uB2E4 (\uBC31\uC5C5\uB428). \uC774 \uAE30\uAE30\uC758 \uAC83\uC744 \uC9C0\uD0A4\uB824\uBA74 \uBA3C\uC800 'lshed add' \uB85C \uCC3D\uACE0\uC5D0 \uB123\uC73C\uC138\uC694.`);
|
|
1233
|
+
}
|
|
1176
1234
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1177
1235
|
const backupDir = path17.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
1178
1236
|
const backedUp = [];
|
|
@@ -1211,8 +1269,8 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1211
1269
|
if (it.entry) {
|
|
1212
1270
|
const shed = await readEntryFile(it.src);
|
|
1213
1271
|
const local = (await entriesOf(it.entry))[it.id];
|
|
1214
|
-
const vars = placeholdersIn(shed);
|
|
1215
|
-
const ex = expand(shed);
|
|
1272
|
+
const vars = placeholdersIn(shed).filter((v) => v !== "HOME");
|
|
1273
|
+
const ex = expand(shed, envWithHome());
|
|
1216
1274
|
if (ex.missing.length) missingEnv.push({ rel: it.rel, vars: ex.missing });
|
|
1217
1275
|
const value = it.entry.expandsEnv ? shed : ex.value;
|
|
1218
1276
|
const same2 = local !== void 0 && matches2(shed, local);
|
|
@@ -1324,7 +1382,7 @@ async function add(ctx, keys = [], opts = {}) {
|
|
|
1324
1382
|
if (!c.fresh.length) ctx.log("\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uC0C8 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
1325
1383
|
else {
|
|
1326
1384
|
ctx.log(`\uCC3D\uACE0\uC5D0 \uC5C6\uB294 \uD56D\uBAA9 ${c.fresh.length}\uAC1C (\uB123\uC73C\uB824\uBA74 lshed add <key...> \uB610\uB294 --all):`);
|
|
1327
|
-
for (const f of c.fresh) ctx.log(` ${f.kind === "package" ? "\u2261" : " "} ${keyOf(f)}${f.kind === "package" ? ` ${f.pkg.source}` : ""}`);
|
|
1385
|
+
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}` : ""}`);
|
|
1328
1386
|
}
|
|
1329
1387
|
hint(ctx, c, state.profile);
|
|
1330
1388
|
return [];
|
|
@@ -1380,7 +1438,7 @@ async function status(ctx) {
|
|
|
1380
1438
|
const missingEnv = [];
|
|
1381
1439
|
for (const it of planProfile(ctx, m, state.profile).filter((p) => p.entry)) {
|
|
1382
1440
|
const shed = await readEntryFile(it.src);
|
|
1383
|
-
const missing = shed === null ? [] : expand(shed).missing;
|
|
1441
|
+
const missing = shed === null ? [] : expand(shed, envWithHome()).missing;
|
|
1384
1442
|
if (missing.length) missingEnv.push({ rel: it.rel, vars: missing });
|
|
1385
1443
|
}
|
|
1386
1444
|
const fresh = (await candidates(ctx, m, state.profile)).fresh.map(keyOf);
|
|
@@ -1553,7 +1611,7 @@ ${removed.length}\uAC1C \uC81C\uAC70. \uCC3D\uACE0\uB97C \uCEE4\uBC0B\uD558\uC13
|
|
|
1553
1611
|
}
|
|
1554
1612
|
|
|
1555
1613
|
// src/core/sync.ts
|
|
1556
|
-
import
|
|
1614
|
+
import os3 from "os";
|
|
1557
1615
|
async function sync(ctx, opts = {}) {
|
|
1558
1616
|
const shed = ctx.shed;
|
|
1559
1617
|
const push = opts.push ?? true;
|
|
@@ -1576,7 +1634,16 @@ async function sync(ctx, opts = {}) {
|
|
|
1576
1634
|
ctx.log(` ${opts.dryRun ? "(dry-run) " : ""}commit ${dirty.length}\uAC1C: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ` \uC678 ${dirty.length - 5}` : ""}`);
|
|
1577
1635
|
if (!opts.dryRun) {
|
|
1578
1636
|
await git(["add", "-A"], shed);
|
|
1579
|
-
|
|
1637
|
+
try {
|
|
1638
|
+
await git(["commit", "--quiet", "-m", msg], shed);
|
|
1639
|
+
} catch (e) {
|
|
1640
|
+
if (/Author identity unknown|Please tell me who you are/.test(e.message)) {
|
|
1641
|
+
throw new Error(`git \uC0AC\uC6A9\uC790 \uC815\uBCF4\uAC00 \uC5C6\uC5B4 \uCEE4\uBC0B\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD55C \uBC88\uB9CC \uC124\uC815\uD558\uC138\uC694:
|
|
1642
|
+
git config --global user.name "\uC774\uB984"
|
|
1643
|
+
git config --global user.email "\uBA54\uC77C"`);
|
|
1644
|
+
}
|
|
1645
|
+
throw e;
|
|
1646
|
+
}
|
|
1580
1647
|
}
|
|
1581
1648
|
res.committed = dirty;
|
|
1582
1649
|
} else {
|
|
@@ -1632,12 +1699,17 @@ function defaultMessage(paths, profile) {
|
|
|
1632
1699
|
const head2 = parts.slice(0, 3).join(", ") + (parts.length > 3 ? ` +${parts.length - 3}` : "");
|
|
1633
1700
|
return `lshed sync: ${head2}
|
|
1634
1701
|
|
|
1635
|
-
${
|
|
1702
|
+
${os3.hostname()}${profile ? ` \xB7 profile ${profile}` : ""}`;
|
|
1636
1703
|
}
|
|
1637
1704
|
var firstLine = (s) => s.split("\n").find((l) => l.trim() && !l.startsWith("Command failed")) ?? s;
|
|
1638
1705
|
|
|
1639
1706
|
// src/cli.ts
|
|
1640
1707
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
1708
|
+
for (const s of [process.stdout, process.stderr]) {
|
|
1709
|
+
s.on("error", (e) => {
|
|
1710
|
+
if (e.code === "EPIPE") process.exit(0);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1641
1713
|
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)");
|
|
1642
1714
|
function adapterFromOpts() {
|
|
1643
1715
|
const { root } = program.opts();
|
|
@@ -1648,7 +1720,7 @@ async function ctxFor(cmd) {
|
|
|
1648
1720
|
const { shed: flag } = program.opts();
|
|
1649
1721
|
let shed = flag ?? process.env.LSHED_HOME;
|
|
1650
1722
|
if (!shed && cmd === "other") shed = (await readState(adapter))?.shed;
|
|
1651
|
-
if (!shed && cmd === "init") shed = path19.join(
|
|
1723
|
+
if (!shed && cmd === "init") shed = path19.join(os4.homedir(), "lshed");
|
|
1652
1724
|
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.");
|
|
1653
1725
|
return { adapter, shed: path19.resolve(shed), log: (l) => console.log(l), exec: spawnExec };
|
|
1654
1726
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lshed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Portable harness environment manager for coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "leesongheon <leesongheon1209@gmail.com>",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dev": "tsx src/cli.ts",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
+
"smoke": "node scripts/smoke.mjs",
|
|
25
26
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|