lshed 0.7.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 +20 -0
- package/README.md +25 -0
- package/dist/cli.js +22 -4
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
3
23
|
## 0.7.0 — 2026-09-03
|
|
4
24
|
|
|
5
25
|
`settings.json` travels, without merging.
|
package/README.md
CHANGED
|
@@ -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
|
```
|
package/dist/cli.js
CHANGED
|
@@ -448,7 +448,7 @@ var resetHard = (dir, sha) => git(["reset", "--hard", "--quiet", sha], dir);
|
|
|
448
448
|
var pullFf = (dir) => git(["pull", "--ff-only", "--quiet"], dir);
|
|
449
449
|
function runShell(cmd, cwd) {
|
|
450
450
|
return new Promise((resolve, reject) => {
|
|
451
|
-
const p = spawn(
|
|
451
|
+
const p = spawn(cmd, { cwd, stdio: "inherit", shell: true });
|
|
452
452
|
p.on("error", reject);
|
|
453
453
|
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`\uBA85\uB839\uC774 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4: ${cmd}`)));
|
|
454
454
|
});
|
|
@@ -644,7 +644,7 @@ async function writeState(adapter, state) {
|
|
|
644
644
|
// src/core/context.ts
|
|
645
645
|
function spawnExec(cmd, args, cwd) {
|
|
646
646
|
return new Promise((resolve, reject) => {
|
|
647
|
-
const p = spawn2(cmd, args, { cwd, stdio: "inherit" });
|
|
647
|
+
const p = spawn2(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
648
648
|
p.on("error", reject);
|
|
649
649
|
p.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} \uAC00 ${code} \uB85C \uB05D\uB0AC\uC2B5\uB2C8\uB2E4`)));
|
|
650
650
|
});
|
|
@@ -1015,7 +1015,7 @@ function mask(id, entry, cat, home = os2.homedir()) {
|
|
|
1015
1015
|
}
|
|
1016
1016
|
function portable(entry, home = os2.homedir()) {
|
|
1017
1017
|
if (!home || home === "/") return entry;
|
|
1018
|
-
return walk(entry, (s) => s === home || s.startsWith(home + "/") ? "${HOME}" + s.slice(home.length) : s);
|
|
1018
|
+
return walk(entry, (s) => s === home || s.startsWith(home + "/") || s.startsWith(home + "\\") ? "${HOME}" + s.slice(home.length) : s);
|
|
1019
1019
|
}
|
|
1020
1020
|
function envWithHome(env = process.env) {
|
|
1021
1021
|
return { HOME: os2.homedir(), ...env };
|
|
@@ -1227,6 +1227,10 @@ async function restore(ctx, profileArg, opts = {}) {
|
|
|
1227
1227
|
if (fragments.length) newManaged.add(instrRel);
|
|
1228
1228
|
const oldManaged = new Set(state?.managed ?? []);
|
|
1229
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
|
+
}
|
|
1230
1234
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1231
1235
|
const backupDir = path17.join(ctx.adapter.root, LSHED_DIR, "backups", stamp);
|
|
1232
1236
|
const backedUp = [];
|
|
@@ -1630,7 +1634,16 @@ async function sync(ctx, opts = {}) {
|
|
|
1630
1634
|
ctx.log(` ${opts.dryRun ? "(dry-run) " : ""}commit ${dirty.length}\uAC1C: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ` \uC678 ${dirty.length - 5}` : ""}`);
|
|
1631
1635
|
if (!opts.dryRun) {
|
|
1632
1636
|
await git(["add", "-A"], shed);
|
|
1633
|
-
|
|
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
|
+
}
|
|
1634
1647
|
}
|
|
1635
1648
|
res.committed = dirty;
|
|
1636
1649
|
} else {
|
|
@@ -1692,6 +1705,11 @@ var firstLine = (s) => s.split("\n").find((l) => l.trim() && !l.startsWith("Comm
|
|
|
1692
1705
|
|
|
1693
1706
|
// src/cli.ts
|
|
1694
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
|
+
}
|
|
1695
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)");
|
|
1696
1714
|
function adapterFromOpts() {
|
|
1697
1715
|
const { root } = program.opts();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lshed",
|
|
3
|
-
"version": "0.7.
|
|
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": [
|