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 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("sh", ["-c", cmd], { cwd, stdio: "inherit" });
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
- await git(["commit", "--quiet", "-m", msg], shed);
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.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": [