agentwheel 0.3.1 → 0.4.1

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.
Files changed (3) hide show
  1. package/README.md +55 -6
  2. package/dist/index.js +345 -127
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,17 +24,19 @@ So you copy-paste. You forget which agent has the latest version. You tweak a ru
24
24
  ```bash
25
25
  npm i -g agentwheel
26
26
  agentwheel add github:your-org/agent-pack
27
- agentwheel update --dry-run # show me what would change
28
- agentwheel update # install into configured agents
27
+ cd ~/.openclaw
28
+ agentwheel sync --dry-run # show me what would change
29
+ agentwheel sync # install into the detected runtime
29
30
  ```
30
31
 
31
32
  No lock-in. No central gatekeeper. Your packages live in plain git repos, your customizations live in your own repo, and anything reachable by a URL just works.
32
33
 
33
34
  ---
34
35
 
35
- > **Status: early (v0.3).** The lifecycle core is real and tested — local/git/skillkit/vercel
36
+ > **Status: early (v0.4).** The lifecycle core is real and tested — local/git/skillkit/vercel
36
37
  > sources, optional registry discovery, plan/sync/update/drift/uninstall, overlays, eject/remember,
37
- > profiles, rich JSON merge, and pluggable adapters. Expect sharp edges.
38
+ > profiles, runtime auto-detection, fleet targeting, rich JSON merge, and pluggable adapters.
39
+ > Expect sharp edges.
38
40
 
39
41
  ## What it does
40
42
 
@@ -51,8 +53,9 @@ npm i -g agentwheel
51
53
 
52
54
  agentwheel init
53
55
  agentwheel add github:your-org/agent-pack --adapter openclaw --mode tracking
54
- agentwheel update --dry-run
55
- agentwheel update
56
+ cd ~/.openclaw
57
+ agentwheel sync --dry-run
58
+ agentwheel sync
56
59
  ```
57
60
 
58
61
  Prefer pnpm? `pnpm add -g agentwheel` works too.
@@ -69,6 +72,51 @@ pnpm link --global
69
72
 
70
73
  `plan`, `sync --dry-run`, and `update --dry-run` show exactly what would change before anything is written. They're the commands to trust.
71
74
 
75
+ `uninstall` removes clean managed files by default and keeps drifted files in place with a warning.
76
+ Use `agentwheel uninstall --force` only when you also want to remove drifted managed files.
77
+
78
+ ## Runtime targeting
79
+
80
+ Normal use no longer needs `--target-root`. Run agentwheel inside a runtime folder and it detects
81
+ the target:
82
+
83
+ ```bash
84
+ cd ~/.openclaw
85
+ agentwheel sync github:your-org/agent-pack
86
+ ```
87
+
88
+ If the current directory is already the runtime directory (`~/.openclaw`), agentwheel uses its
89
+ parent as the root so output lands in `~/.openclaw/skills`, not `~/.openclaw/.openclaw/skills`.
90
+ If the current directory contains a runtime directory (`./.openclaw`), that directory is used as
91
+ the target under the current project.
92
+
93
+ For a control-plane setup, define named agents in config. Global config lives at
94
+ `~/.agentwheel/config.json`; project config lives at `.agentwheel/config.json`; project values win.
95
+
96
+ ```jsonc
97
+ {
98
+ "agents": {
99
+ "lab-openclaw": { "adapter": "openclaw", "root": "/Users/me/.openclaw-home" },
100
+ "docs-copilot": { "adapter": "copilot", "root": "/Users/me/projects/docs" }
101
+ },
102
+ "profiles": {
103
+ "daily": [
104
+ { "agent": "lab-openclaw" },
105
+ { "agent": "docs-copilot" }
106
+ ]
107
+ }
108
+ }
109
+ ```
110
+
111
+ ```bash
112
+ agentwheel sync --agent lab-openclaw
113
+ agentwheel sync --all
114
+ agentwheel sync --profile daily
115
+ ```
116
+
117
+ Target resolution order is exact: `--target-root` wins, then `--agent`, then auto-detect from the
118
+ current directory, then fallback to the current directory.
119
+
72
120
  ## Core ideas
73
121
 
74
122
  **Three places, one direction:**
@@ -172,6 +220,7 @@ clear conversion format.
172
220
  - [x] **v0.1** — install spine: local sources; openclaw/claude/codex adapters; skills/rules/instructions; `plan` · `sync` · `--dry-run` · `uninstall`; manifest + drift + idempotency.
173
221
  - [x] **v0.2** — git source driver; `update` (pinned & tracking); overlays/additive/override/eject; `init`; hermes + copilot adapters; commands/mcp/hooks artifacts; OpenClaw semantic plugin planning.
174
222
  - [x] **v0.3** — skillkit/vercel source drivers; optional registry & federation; programmatic adapters behind `--allow-adapter-code`; rich JSON merge for mcp/hooks/settings; profiles.
223
+ - [x] **v0.4** — runtime auto-detection; no `--target-root` needed for normal use; fleet config with named agents; global + project config merge; `--agent` and `--all`.
175
224
 
176
225
  ## Design docs
177
226
 
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/cli/index.ts
11
11
  import { mkdir as mkdir7, rm as rm8, writeFile as writeFile4 } from "fs/promises";
12
- import { join as join18 } from "path";
12
+ import { join as join19 } from "path";
13
13
  import { Command } from "commander";
14
14
 
15
15
  // src/adapters/resolve.ts
@@ -461,16 +461,52 @@ async function applyInstallPlan(plan, sourceLock, options = {}) {
461
461
  await writeSourceLock(plan.targetRoot, plan.adapter, sourceLock);
462
462
  return manifest;
463
463
  }
464
- async function uninstall(plan, dryRun) {
464
+ async function uninstall(plan, options = {}) {
465
+ const resolvedOptions = typeof options === "boolean" ? { dryRun: options } : options;
465
466
  if (plan.hasBlockingChanges) {
466
- const blockers = plan.operations.filter((operation) => operation.action === "drift" || operation.action === "conflict");
467
- throw new Error(`Refusing to uninstall with drift: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
467
+ const blockers = plan.operations.filter((operation) => operation.action === "conflict");
468
+ throw new Error(`Refusing to uninstall with blocking changes: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
468
469
  }
469
- if (dryRun) return;
470
+ const removable = plan.operations.filter((operation) => operation.action === "remove" || resolvedOptions.force && operation.action === "keep");
471
+ const kept = resolvedOptions.force ? [] : plan.operations.filter((operation) => operation.action === "keep");
472
+ const removedDrifted = resolvedOptions.force ? plan.operations.filter((operation) => operation.action === "keep").length : 0;
473
+ if (resolvedOptions.dryRun) return { removed: removable.length, kept: kept.length, removedDrifted };
470
474
  for (const operation of plan.operations) {
471
- await rm2(operation.destPath, { recursive: true, force: true });
475
+ if (operation.action === "remove" || resolvedOptions.force && operation.action === "keep") {
476
+ await rm2(operation.destPath, { recursive: true, force: true });
477
+ }
472
478
  }
473
- await removeStateFiles(plan.targetRoot, plan.adapter);
479
+ if (kept.length > 0) {
480
+ const now = (/* @__PURE__ */ new Date()).toISOString();
481
+ await writeInstallManifest({
482
+ version: 1,
483
+ adapter: plan.adapter,
484
+ targetRoot: plan.targetRoot,
485
+ generatedAt: now,
486
+ adapterCode: plan.adapterCode,
487
+ entries: kept.map((operation) => {
488
+ if (!operation.manifestHash || !operation.desiredHash) {
489
+ throw new Error(`Invalid kept operation missing manifest/source hash: ${operation.relativeDestPath}`);
490
+ }
491
+ return {
492
+ path: operation.relativeDestPath,
493
+ artifactType: operation.artifactType,
494
+ artifactName: operation.artifactName,
495
+ kind: operation.kind,
496
+ hash: operation.manifestHash,
497
+ sourceHash: operation.desiredHash,
498
+ updatedAt: now,
499
+ channel: operation.channel,
500
+ packageName: operation.packageName,
501
+ semanticCommand: operation.semanticCommand,
502
+ mergeStrategy: operation.mergeStrategy
503
+ };
504
+ }).sort((a, b) => a.path.localeCompare(b.path))
505
+ });
506
+ } else {
507
+ await removeStateFiles(plan.targetRoot, plan.adapter);
508
+ }
509
+ return { removed: removable.length, kept: kept.length, removedDrifted };
474
510
  }
475
511
 
476
512
  // src/install/plan.ts
@@ -658,6 +694,7 @@ function summarizePlan(plan) {
658
694
  update: 0,
659
695
  skip: 0,
660
696
  remove: 0,
697
+ keep: 0,
661
698
  drift: 0,
662
699
  conflict: 0,
663
700
  plugin: 0,
@@ -679,17 +716,20 @@ async function createUninstallPlan(manifest) {
679
716
  const currentHash = await hashPath(destPath);
680
717
  if (currentHash !== entry.hash) {
681
718
  operations.push({
682
- action: "drift",
719
+ action: "keep",
683
720
  artifactType: entry.artifactType,
684
721
  artifactName: entry.artifactName,
685
722
  kind: entry.kind,
686
723
  destPath,
687
724
  relativeDestPath: entry.path,
725
+ desiredHash: entry.sourceHash,
688
726
  currentHash,
689
727
  manifestHash: entry.hash,
690
- reason: "managed destination changed outside agentwheel",
728
+ reason: "managed destination changed outside agentwheel; keeping by default",
691
729
  channel: entry.channel,
692
- packageName: entry.packageName
730
+ packageName: entry.packageName,
731
+ semanticCommand: entry.semanticCommand,
732
+ mergeStrategy: entry.mergeStrategy
693
733
  });
694
734
  } else {
695
735
  operations.push({
@@ -712,7 +752,8 @@ async function createUninstallPlan(manifest) {
712
752
  adapter: manifest.adapter,
713
753
  targetRoot: manifest.targetRoot,
714
754
  operations,
715
- hasBlockingChanges: operations.some((operation) => operation.action === "drift" || operation.action === "conflict")
755
+ hasBlockingChanges: operations.some((operation) => operation.action === "conflict"),
756
+ adapterCode: manifest.adapterCode
716
757
  };
717
758
  }
718
759
 
@@ -722,6 +763,7 @@ var labels = {
722
763
  update: "UPDATE",
723
764
  skip: "SKIP",
724
765
  remove: "REMOVE",
766
+ keep: "KEEP",
725
767
  drift: "DRIFT",
726
768
  conflict: "CONFLICT",
727
769
  plugin: "PLUGIN",
@@ -743,7 +785,7 @@ function formatPlan(plan) {
743
785
  }
744
786
  const summary = summarizePlan(plan);
745
787
  lines.push(
746
- `Summary: create ${summary.create}, update ${summary.update}, skip ${summary.skip}, remove ${summary.remove}, drift ${summary.drift}, conflict ${summary.conflict}, plugin ${summary.plugin}`
788
+ `Summary: create ${summary.create}, update ${summary.update}, skip ${summary.skip}, remove ${summary.remove}, keep ${summary.keep}, drift ${summary.drift}, conflict ${summary.conflict}, plugin ${summary.plugin}`
747
789
  );
748
790
  return lines.join("\n");
749
791
  }
@@ -1584,7 +1626,8 @@ async function stageSource(driver, source, options = {}) {
1584
1626
 
1585
1627
  // src/model/workspace.ts
1586
1628
  import { readFile as readFile8 } from "fs/promises";
1587
- import { join as join13 } from "path";
1629
+ import { homedir as homedir3 } from "os";
1630
+ import { dirname as dirname6, join as join13, resolve as resolve8 } from "path";
1588
1631
  import { z as z5 } from "zod";
1589
1632
  var workspacePackageSchema = z5.object({
1590
1633
  name: z5.string().min(1),
@@ -1598,6 +1641,7 @@ var workspacePackageSchema = z5.object({
1598
1641
  requestedRef: z5.string().min(1).optional()
1599
1642
  });
1600
1643
  var workspaceProfileRuntimeSchema = z5.object({
1644
+ agent: z5.string().min(1).optional(),
1601
1645
  adapter: z5.string().min(1).default("openclaw"),
1602
1646
  adapterConfig: z5.string().min(1).optional(),
1603
1647
  adapterModule: z5.string().min(1).optional(),
@@ -1611,18 +1655,23 @@ var workspaceRegistrySchema = z5.object({
1611
1655
  sources: z5.array(z5.string().min(1)).optional(),
1612
1656
  ttlSeconds: z5.number().int().positive().optional()
1613
1657
  }).default({});
1658
+ var workspaceAgentSchema = z5.object({
1659
+ adapter: z5.string().min(1),
1660
+ root: z5.string().min(1)
1661
+ });
1614
1662
  var workspaceConfigSchema = z5.object({
1615
1663
  schemaVersion: z5.literal(1),
1616
1664
  packages: z5.array(workspacePackageSchema).default([]),
1617
1665
  registry: workspaceRegistrySchema,
1618
- profiles: z5.record(z5.string(), workspaceProfileSchema).default({})
1666
+ profiles: z5.record(z5.string(), workspaceProfileSchema).default({}),
1667
+ agents: z5.record(z5.string(), workspaceAgentSchema).default({})
1619
1668
  });
1620
1669
  function workspaceConfigPath(workspaceRoot) {
1621
1670
  return join13(workspaceRoot, ".agentwheel", "config.json");
1622
1671
  }
1623
1672
  async function readWorkspaceConfig(workspaceRoot) {
1624
1673
  const path = workspaceConfigPath(workspaceRoot);
1625
- if (!await pathExists(path)) return { schemaVersion: 1, packages: [], registry: {}, profiles: {} };
1674
+ if (!await pathExists(path)) return emptyWorkspaceConfig();
1626
1675
  return workspaceConfigSchema.parse(JSON.parse(await readFile8(path, "utf8")));
1627
1676
  }
1628
1677
  async function writeWorkspaceConfig(workspaceRoot, config) {
@@ -1632,15 +1681,58 @@ function upsertPackage(config, entry) {
1632
1681
  const packages = config.packages.filter((candidate) => candidate.name !== entry.name);
1633
1682
  packages.push(entry);
1634
1683
  packages.sort((a, b) => a.name.localeCompare(b.name));
1635
- return { schemaVersion: 1, packages, registry: config.registry ?? {}, profiles: config.profiles ?? {} };
1684
+ return { schemaVersion: 1, packages, registry: config.registry ?? {}, profiles: config.profiles ?? {}, agents: config.agents ?? {} };
1685
+ }
1686
+ function globalWorkspaceConfigPath(globalRoot = homedir3()) {
1687
+ return join13(globalRoot, ".agentwheel", "config.json");
1688
+ }
1689
+ async function findWorkspaceRoot(start = process.cwd()) {
1690
+ let current = resolve8(start);
1691
+ while (true) {
1692
+ if (await pathExists(workspaceConfigPath(current))) return current;
1693
+ const parent = dirname6(current);
1694
+ if (parent === current) return resolve8(start);
1695
+ current = parent;
1696
+ }
1697
+ }
1698
+ async function readMergedWorkspaceConfig(projectRoot, options = {}) {
1699
+ const global = await readConfigPath(globalWorkspaceConfigPath(options.globalRoot));
1700
+ const project = await readWorkspaceConfig(projectRoot);
1701
+ return mergeWorkspaceConfig(global, project);
1702
+ }
1703
+ function mergeWorkspaceConfig(global, project) {
1704
+ return workspaceConfigSchema.parse({
1705
+ schemaVersion: 1,
1706
+ packages: project.packages.length > 0 ? project.packages : global.packages,
1707
+ registry: {
1708
+ ...global.registry,
1709
+ ...project.registry,
1710
+ sources: project.registry.sources ?? global.registry.sources,
1711
+ ttlSeconds: project.registry.ttlSeconds ?? global.registry.ttlSeconds
1712
+ },
1713
+ profiles: { ...global.profiles, ...project.profiles },
1714
+ agents: { ...global.agents, ...project.agents }
1715
+ });
1716
+ }
1717
+ function resolveConfigPath(path, baseRoot) {
1718
+ if (path.startsWith("~/")) return resolve8(homedir3(), path.slice(2));
1719
+ if (path === "~") return homedir3();
1720
+ return path.startsWith("/") ? resolve8(path) : resolve8(baseRoot, path);
1721
+ }
1722
+ function emptyWorkspaceConfig() {
1723
+ return { schemaVersion: 1, packages: [], registry: {}, profiles: {}, agents: {} };
1724
+ }
1725
+ async function readConfigPath(path) {
1726
+ if (!await pathExists(path)) return emptyWorkspaceConfig();
1727
+ return workspaceConfigSchema.parse(JSON.parse(await readFile8(path, "utf8")));
1636
1728
  }
1637
1729
 
1638
1730
  // src/lifecycle/customization.ts
1639
1731
  import { appendFile, cp as cp4, mkdir as mkdir6, rm as rm5 } from "fs/promises";
1640
- import { dirname as dirname6, join as join14 } from "path";
1732
+ import { dirname as dirname7, join as join14 } from "path";
1641
1733
  async function remember(workspaceRoot, runtime, text) {
1642
1734
  const overlayPath = join14(workspaceRoot, ".agentwheel", "overlays", runtime, "instructions.local.md");
1643
- await mkdir6(dirname6(overlayPath), { recursive: true });
1735
+ await mkdir6(dirname7(overlayPath), { recursive: true });
1644
1736
  await appendFile(overlayPath, `${text.trim()}
1645
1737
  `, "utf8");
1646
1738
  return { overlayPath };
@@ -1665,7 +1757,7 @@ async function ejectArtifact(workspaceRoot, item) {
1665
1757
  throw new Error(`Artifact not found: ${item}`);
1666
1758
  }
1667
1759
  const ejectedPath = join14(workspaceRoot, ".agentwheel", "ejected", ...parsed.packageName.split("/"), parsed.type, parsed.name);
1668
- await mkdir6(dirname6(ejectedPath), { recursive: true });
1760
+ await mkdir6(dirname7(ejectedPath), { recursive: true });
1669
1761
  await rm5(ejectedPath, { recursive: true, force: true });
1670
1762
  await cp4(artifact.stagedPath ?? artifact.sourcePath, ejectedPath, { recursive: artifact.kind === "dir", dereference: true });
1671
1763
  return { ...parsed, ejectedPath };
@@ -1690,8 +1782,8 @@ import { join as join16 } from "path";
1690
1782
 
1691
1783
  // src/registry/client.ts
1692
1784
  import { readFile as readFile9, rm as rm6, stat as stat6 } from "fs/promises";
1693
- import { homedir as homedir3 } from "os";
1694
- import { dirname as dirname7, join as join15, resolve as resolve8 } from "path";
1785
+ import { homedir as homedir4 } from "os";
1786
+ import { dirname as dirname8, join as join15, resolve as resolve9 } from "path";
1695
1787
  import { fileURLToPath } from "url";
1696
1788
 
1697
1789
  // src/model/registry.ts
@@ -1762,7 +1854,7 @@ var RegistryClient = class {
1762
1854
  return process.env.AGENTWHEEL_REGISTRY.split(",").map((source) => source.trim()).filter(Boolean);
1763
1855
  }
1764
1856
  if (this.options.workspaceRoot) {
1765
- const config = await readWorkspaceConfig(this.options.workspaceRoot);
1857
+ const config = await readMergedWorkspaceConfig(this.options.workspaceRoot);
1766
1858
  if (config.registry.sources?.length) return config.registry.sources;
1767
1859
  }
1768
1860
  return [DEFAULT_REGISTRY_SOURCE];
@@ -1770,7 +1862,7 @@ var RegistryClient = class {
1770
1862
  async getTtlMs() {
1771
1863
  if (this.options.ttlMs !== void 0) return this.options.ttlMs;
1772
1864
  if (this.options.workspaceRoot) {
1773
- const config = await readWorkspaceConfig(this.options.workspaceRoot);
1865
+ const config = await readMergedWorkspaceConfig(this.options.workspaceRoot);
1774
1866
  if (config.registry.ttlSeconds !== void 0) return config.registry.ttlSeconds * 1e3;
1775
1867
  }
1776
1868
  return DEFAULT_REGISTRY_TTL_MS;
@@ -1794,11 +1886,11 @@ var RegistryClient = class {
1794
1886
  }
1795
1887
  const filePath = source.startsWith("file:") ? fileURLToPath(source) : source;
1796
1888
  if (await pathExists(filePath)) {
1797
- const fullPath = resolve8(filePath);
1889
+ const fullPath = resolve9(filePath);
1798
1890
  const stats = await stat6(fullPath);
1799
1891
  return readFile9(stats.isDirectory() ? join15(fullPath, "index.json") : fullPath, "utf8");
1800
1892
  }
1801
- const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join15(dirname7(this.cachePath), "registry-repos") }));
1893
+ const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join15(dirname8(this.cachePath), "registry-repos") }));
1802
1894
  return readFile9(join15(resolved.resolvedPath, "index.json"), "utf8");
1803
1895
  }
1804
1896
  };
@@ -1821,7 +1913,7 @@ function mergeIndexes(indexes) {
1821
1913
  return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
1822
1914
  }
1823
1915
  function defaultRegistryCachePath() {
1824
- return join15(homedir3(), ".agentwheel", "registry-cache.json");
1916
+ return join15(homedir4(), ".agentwheel", "registry-cache.json");
1825
1917
  }
1826
1918
  function sameSources(a, b) {
1827
1919
  return a.length === b.length && a.every((source, index) => source === b[index]);
@@ -1829,7 +1921,7 @@ function sameSources(a, b) {
1829
1921
 
1830
1922
  // src/lifecycle/profile.ts
1831
1923
  async function syncProfile(options) {
1832
- const config = await readWorkspaceConfig(options.workspaceRoot);
1924
+ const config = await readMergedWorkspaceConfig(options.workspaceRoot);
1833
1925
  const profile = config.profiles[options.profile];
1834
1926
  if (!profile) {
1835
1927
  throw new Error(`Unknown profile: ${options.profile}`);
@@ -1841,9 +1933,9 @@ async function syncProfile(options) {
1841
1933
  const results = [];
1842
1934
  for (const pkg of packages) {
1843
1935
  for (const runtime of profile.runtimes) {
1844
- const targetRoot = runtime.targetRoot ?? options.workspaceRoot;
1936
+ const target = resolveProfileRuntime(runtime, config, options.workspaceRoot);
1845
1937
  const adapter = await resolveAdapter({
1846
- adapter: runtime.adapter,
1938
+ adapter: target.adapter,
1847
1939
  adapterConfig: runtime.adapterConfig,
1848
1940
  adapterModule: runtime.adapterModule,
1849
1941
  allowAdapterCode: options.allowAdapterCode,
@@ -1858,8 +1950,8 @@ async function syncProfile(options) {
1858
1950
  mode: options.mode ?? pkg.mode
1859
1951
  });
1860
1952
  try {
1861
- const plan = await createInstallPlan(bundle, adapter, targetRoot, await readInstallManifest(targetRoot, adapter.name));
1862
- results.push({ runtime: adapter.name, packageName: pkg.name, plan });
1953
+ const plan = await createInstallPlan(bundle, adapter, target.targetRoot, await readInstallManifest(target.targetRoot, adapter.name));
1954
+ results.push({ runtime: adapter.name, targetRoot: target.targetRoot, packageName: pkg.name, plan });
1863
1955
  if (!options.dryRun) {
1864
1956
  await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: runtime.executePlugins ?? options.executePlugins });
1865
1957
  }
@@ -1870,6 +1962,17 @@ async function syncProfile(options) {
1870
1962
  }
1871
1963
  return results;
1872
1964
  }
1965
+ function resolveProfileRuntime(runtime, config, workspaceRoot) {
1966
+ if (runtime.agent) {
1967
+ const agent = config.agents[runtime.agent];
1968
+ if (!agent) throw new Error(`Unknown agent in profile: ${runtime.agent}`);
1969
+ return { adapter: agent.adapter, targetRoot: resolveConfigPath(agent.root, workspaceRoot) };
1970
+ }
1971
+ return {
1972
+ adapter: runtime.adapter,
1973
+ targetRoot: runtime.targetRoot ? resolveConfigPath(runtime.targetRoot, workspaceRoot) : workspaceRoot
1974
+ };
1975
+ }
1873
1976
  async function packageFromSource(source, options) {
1874
1977
  const resolved = await resolvePackageSource(source, options.workspaceRoot);
1875
1978
  const driver = options.driver ?? inferSourceDriverName(resolved.source);
@@ -1885,13 +1988,14 @@ async function packageFromSource(source, options) {
1885
1988
  // src/lifecycle/source-plan.ts
1886
1989
  import { join as join17 } from "path";
1887
1990
  async function createSourcePlan(options) {
1888
- const resolvedInput = await resolvePackageSource(options.source, options.targetRoot);
1991
+ const workspaceRoot = options.workspaceRoot ?? options.targetRoot;
1992
+ const resolvedInput = await resolvePackageSource(options.source, workspaceRoot);
1889
1993
  const resolvedSource = resolvedInput.source;
1890
1994
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedSource));
1891
1995
  const bundle = await stageSource(driver, resolvedSource, {
1892
- workspaceRoot: options.targetRoot,
1996
+ workspaceRoot,
1893
1997
  adapter: options.adapter,
1894
- cacheRoot: join17(options.targetRoot, ".agentwheel", "cache"),
1998
+ cacheRoot: join17(workspaceRoot, ".agentwheel", "cache"),
1895
1999
  mode: options.mode
1896
2000
  });
1897
2001
  const manifest = await readInstallManifest(options.targetRoot, options.adapter.name);
@@ -1916,9 +2020,97 @@ function shouldUpdatePackage(pkg, lock) {
1916
2020
  return { shouldUpdate: false, reason: "pinned source unchanged" };
1917
2021
  }
1918
2022
 
2023
+ // src/runtime/target.ts
2024
+ import { basename as basename7, dirname as dirname9, join as join18, resolve as resolve10 } from "path";
2025
+ var runtimeMarkers = [
2026
+ { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
2027
+ { adapter: "claude", dirs: [".claude"] },
2028
+ { adapter: "codex", dirs: [".codex"] },
2029
+ { adapter: "hermes", dirs: [".hermes"] },
2030
+ { adapter: "copilot", dirs: [".github"] }
2031
+ ];
2032
+ async function resolveRuntimeTarget(request = {}) {
2033
+ const cwd = resolve10(request.cwd ?? process.cwd());
2034
+ if (request.targetRoot) {
2035
+ const targetRoot = resolve10(request.targetRoot);
2036
+ return {
2037
+ adapter: request.adapter ?? "openclaw",
2038
+ targetRoot,
2039
+ workspaceRoot: targetRoot,
2040
+ source: "target-root"
2041
+ };
2042
+ }
2043
+ const workspaceRoot = await findWorkspaceRoot(cwd);
2044
+ const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
2045
+ if (request.agent) {
2046
+ return targetFromAgent(request.agent, config, workspaceRoot);
2047
+ }
2048
+ const detected = await detectRuntimeTarget(cwd, request.adapter);
2049
+ if (detected) {
2050
+ return { ...detected, workspaceRoot: await findWorkspaceRoot(detected.targetRoot), source: "auto-detect" };
2051
+ }
2052
+ return {
2053
+ adapter: request.adapter ?? "openclaw",
2054
+ targetRoot: cwd,
2055
+ workspaceRoot,
2056
+ source: "cwd"
2057
+ };
2058
+ }
2059
+ async function resolveAllRuntimeTargets(request = {}) {
2060
+ if (request.targetRoot) return [await resolveRuntimeTarget(request)];
2061
+ if (request.agent) return [await resolveRuntimeTarget(request)];
2062
+ const cwd = resolve10(request.cwd ?? process.cwd());
2063
+ const workspaceRoot = await findWorkspaceRoot(cwd);
2064
+ const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
2065
+ const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot));
2066
+ if (targets.length === 0) {
2067
+ throw new Error("No agents configured. Add agents to .agentwheel/config.json or pass --target-root.");
2068
+ }
2069
+ return targets;
2070
+ }
2071
+ async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
2072
+ const root = resolve10(cwd);
2073
+ const matches = [];
2074
+ for (const marker of runtimeMarkers) {
2075
+ if (adapterFilter && marker.adapter !== adapterFilter) continue;
2076
+ for (const dir of marker.dirs) {
2077
+ if (basename7(root) === dir) {
2078
+ matches.push({ adapter: marker.adapter, targetRoot: dirname9(root) });
2079
+ } else if (await pathExists(join18(root, dir))) {
2080
+ matches.push({ adapter: marker.adapter, targetRoot: root });
2081
+ }
2082
+ }
2083
+ }
2084
+ const unique = dedupeTargets(matches);
2085
+ if (unique.length > 1) {
2086
+ throw new Error(`Multiple runtime directories detected: ${unique.map((item) => `${item.adapter} at ${item.targetRoot}`).join(", ")}. Pass --adapter or --agent.`);
2087
+ }
2088
+ return unique[0];
2089
+ }
2090
+ function targetFromAgent(name, config, workspaceRoot) {
2091
+ const agent = config.agents[name];
2092
+ if (!agent) {
2093
+ throw new Error(`Unknown agent: ${name}`);
2094
+ }
2095
+ return {
2096
+ agentName: name,
2097
+ adapter: agent.adapter,
2098
+ targetRoot: resolveConfigPath(agent.root, workspaceRoot),
2099
+ workspaceRoot,
2100
+ source: "agent"
2101
+ };
2102
+ }
2103
+ function dedupeTargets(matches) {
2104
+ const byKey = /* @__PURE__ */ new Map();
2105
+ for (const match of matches) {
2106
+ byKey.set(`${match.adapter}:${match.targetRoot}`, match);
2107
+ }
2108
+ return [...byKey.values()];
2109
+ }
2110
+
1919
2111
  // src/cli/index.ts
1920
2112
  var program = new Command();
1921
- program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.3.1");
2113
+ program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.4.1");
1922
2114
  program.command("init").argument("[kind]", "workspace or package", "workspace").option("--target-root <path>", "workspace root", process.cwd()).action(async (kind, options) => {
1923
2115
  const root = normalizeTargetRoot(options.targetRoot);
1924
2116
  if (kind === "package") {
@@ -1949,7 +2141,7 @@ program.command("add").argument("<source>", "package source").option("--driver <
1949
2141
  const bundle = await stageSource(driver, resolvedSource, {
1950
2142
  workspaceRoot: targetRoot,
1951
2143
  adapter,
1952
- cacheRoot: join18(targetRoot, ".agentwheel", "cache"),
2144
+ cacheRoot: join19(targetRoot, ".agentwheel", "cache"),
1953
2145
  mode: options.mode
1954
2146
  });
1955
2147
  const name = options.name ?? resolvedInput.registryEntry?.name ?? bundle.source.packageName ?? source;
@@ -1972,7 +2164,7 @@ program.command("list").argument("<source>", "package source").option("--driver
1972
2164
  const targetRoot = normalizeTargetRoot(options.targetRoot);
1973
2165
  const resolvedInput = await resolvePackageSource(source, targetRoot);
1974
2166
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
1975
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join18(targetRoot, ".agentwheel", "cache") }))));
2167
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join19(targetRoot, ".agentwheel", "cache") }))));
1976
2168
  const artifacts = await driver.list(resolved);
1977
2169
  for (const artifact of artifacts) {
1978
2170
  console.log(`${artifact.type} ${artifact.name} ${artifact.relativePath}`);
@@ -1982,7 +2174,7 @@ program.command("scan").argument("<source>", "package source").option("--driver
1982
2174
  const targetRoot = normalizeTargetRoot(options.targetRoot);
1983
2175
  const resolvedInput = await resolvePackageSource(source, targetRoot);
1984
2176
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
1985
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join18(targetRoot, ".agentwheel", "cache") }))));
2177
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join19(targetRoot, ".agentwheel", "cache") }))));
1986
2178
  const result = await driver.scan(resolved);
1987
2179
  if (result.findings.length === 0) {
1988
2180
  console.log("Scan ok: no findings");
@@ -1993,17 +2185,20 @@ program.command("scan").argument("<source>", "package source").option("--driver
1993
2185
  }
1994
2186
  if (!result.ok) process.exitCode = 1;
1995
2187
  });
1996
- program.command("plan").argument("<source>", "source directory").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root", process.cwd()).option("--mode <mode>", "pinned or tracking").option("--dry-run", "accepted for symmetry; plan never writes", false).action(async (source, options) => {
1997
- const { plan, bundle } = await buildPlan(source, options);
1998
- console.log(formatPlan(plan));
1999
- await rm8(bundle.root, { recursive: true, force: true });
2000
- if (plan.hasBlockingChanges) process.exitCode = 1;
2188
+ program.command("plan").argument("<source>", "source directory").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--mode <mode>", "pinned or tracking").option("--dry-run", "accepted for symmetry; plan never writes", false).action(async (source, options) => {
2189
+ const targets = await resolveCliTargets(options);
2190
+ for (const target of targets) {
2191
+ const { plan, bundle } = await buildPlan(source, target, options);
2192
+ console.log(formatPlan(plan));
2193
+ await rm8(bundle.root, { recursive: true, force: true });
2194
+ if (plan.hasBlockingChanges) process.exitCode = 1;
2195
+ }
2001
2196
  });
2002
- program.command("sync").argument("[source]", "source directory").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root", process.cwd()).option("--mode <mode>", "pinned or tracking").option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).action(async (source, options) => {
2197
+ program.command("sync").argument("[source]", "source directory").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter", "openclaw").option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--mode <mode>", "pinned or tracking").option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).action(async (source, options) => {
2003
2198
  if (options.profile) {
2004
- const targetRoot = normalizeTargetRoot(options.targetRoot);
2199
+ const target = await resolveRuntimeTarget({ targetRoot: options.targetRoot, adapter: options.adapter, agent: options.agent });
2005
2200
  const results = await syncProfile({
2006
- workspaceRoot: targetRoot,
2201
+ workspaceRoot: target.workspaceRoot,
2007
2202
  profile: options.profile,
2008
2203
  source,
2009
2204
  driver: options.driver,
@@ -2014,66 +2209,37 @@ program.command("sync").argument("[source]", "source directory").option("--drive
2014
2209
  warn: (message) => console.warn(message)
2015
2210
  });
2016
2211
  for (const result of results) {
2017
- console.log(`Profile ${options.profile} / ${result.runtime} / ${result.packageName}:`);
2212
+ console.log(`Profile ${options.profile} / ${result.runtime} / ${result.packageName} at ${result.targetRoot}:`);
2018
2213
  console.log(formatPlan(result.plan));
2019
2214
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
2020
2215
  }
2021
2216
  if (!options.dryRun) console.log("Applied.");
2022
2217
  return;
2023
2218
  }
2219
+ const targets = await resolveCliTargets(options);
2024
2220
  if (!source) {
2025
- throw new Error("sync requires a source unless --profile is used");
2026
- }
2027
- const { plan, bundle } = await buildPlan(source, options);
2028
- console.log(formatPlan(plan));
2029
- if (!options.dryRun) {
2030
- await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: options.executePlugins });
2031
- console.log("Applied.");
2032
- }
2033
- await rm8(bundle.root, { recursive: true, force: true });
2034
- if (plan.hasBlockingChanges) process.exitCode = 1;
2035
- });
2036
- program.command("update").option("--target-root <path>", "workspace root", process.cwd()).option("--dry-run", "show plans without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--allow-adapter-code", "allow loading local adapter code from configured packages", false).action(async (options) => {
2037
- const targetRoot = normalizeTargetRoot(options.targetRoot);
2038
- const config = await readWorkspaceConfig(targetRoot);
2039
- if (config.packages.length === 0) {
2040
- console.log("No packages configured.");
2221
+ for (const target of targets) {
2222
+ await runConfiguredPackages(target, options, { useUpdateDecision: false });
2223
+ }
2041
2224
  return;
2042
2225
  }
2043
- for (const pkg of config.packages) {
2044
- const adapter = await resolveAdapter({
2045
- adapter: pkg.adapter,
2046
- adapterConfig: pkg.adapterConfig,
2047
- adapterModule: pkg.adapterModule,
2048
- allowAdapterCode: options.allowAdapterCode,
2049
- baseDir: targetRoot,
2050
- warn: (message) => console.warn(message)
2051
- });
2052
- const lock = await readSourceLock(targetRoot, adapter.name);
2053
- const decision = shouldUpdatePackage(pkg, lock);
2054
- if (!decision.shouldUpdate) {
2055
- console.log(`Skipping ${pkg.name}: ${decision.reason}.`);
2056
- continue;
2057
- }
2058
- const { plan, bundle } = await buildPlan(pkg.source, {
2059
- driver: pkg.driver,
2060
- adapter: pkg.adapter,
2061
- adapterConfig: pkg.adapterConfig,
2062
- adapterModule: pkg.adapterModule,
2063
- allowAdapterCode: options.allowAdapterCode,
2064
- targetRoot,
2065
- mode: pkg.mode
2066
- });
2067
- console.log(`Update ${pkg.name}:`);
2226
+ for (const target of targets) {
2227
+ const { plan, bundle } = await buildPlan(source, target, options);
2068
2228
  console.log(formatPlan(plan));
2069
2229
  if (!options.dryRun) {
2070
2230
  await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: options.executePlugins });
2071
- console.log(`Applied ${pkg.name}.`);
2231
+ console.log(`Applied ${target.adapter} at ${target.targetRoot}.`);
2072
2232
  }
2073
2233
  await rm8(bundle.root, { recursive: true, force: true });
2074
2234
  if (plan.hasBlockingChanges) process.exitCode = 1;
2075
2235
  }
2076
2236
  });
2237
+ program.command("update").option("--adapter <adapter>", "built-in adapter").option("--target-root <path>", "workspace root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--dry-run", "show plans without writing", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--allow-adapter-code", "allow loading local adapter code from configured packages", false).action(async (options) => {
2238
+ const targets = await resolveCliTargets(options);
2239
+ for (const target of targets) {
2240
+ await runConfiguredPackages(target, options, { useUpdateDecision: true });
2241
+ }
2242
+ });
2077
2243
  program.command("registry").description("manage optional registry indexes").addCommand(
2078
2244
  new Command("update").description("refresh the local registry cache").option("--target-root <path>", "workspace root", process.cwd()).action(async (options) => {
2079
2245
  const client = new RegistryClient({ workspaceRoot: normalizeTargetRoot(options.targetRoot) });
@@ -2101,54 +2267,96 @@ program.command("eject").argument("<item>", "package/type/name").option("--targe
2101
2267
  const result = await ejectArtifact(targetRoot, item);
2102
2268
  console.log(`Ejected ${item} to ${result.ejectedPath}.`);
2103
2269
  });
2104
- program.command("uninstall").option("--adapter <adapter>", "adapter", "openclaw").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root", process.cwd()).option("--dry-run", "show removals without writing", false).action(async (options) => {
2105
- const targetRoot = normalizeTargetRoot(options.targetRoot);
2106
- const programmaticAdapter = options.adapterModule ? await resolveAdapter({
2107
- adapter: options.adapter,
2108
- adapterModule: options.adapterModule,
2109
- allowAdapterCode: options.allowAdapterCode,
2110
- baseDir: targetRoot,
2111
- warn: (message) => console.warn(message)
2112
- }) : void 0;
2113
- const adapterName = programmaticAdapter?.name ?? options.adapter;
2114
- const manifest = await readInstallManifest(targetRoot, adapterName);
2115
- if (!manifest) {
2116
- console.log(`No install manifest for ${adapterName} at ${targetRoot}`);
2117
- return;
2118
- }
2119
- const plan = await createUninstallPlan(manifest);
2120
- console.log(formatPlan(plan));
2121
- await uninstall(plan, options.dryRun);
2122
- if (!options.dryRun && programmaticAdapter) {
2123
- await programmaticAdapter.programmatic?.uninstall?.({ targetRoot, adapterName: programmaticAdapter.name });
2270
+ program.command("uninstall").option("--adapter <adapter>", "adapter", "openclaw").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("--target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--dry-run", "show removals without writing", false).option("--force", "remove drifted managed files too", false).action(async (options) => {
2271
+ const targets = await resolveCliTargets(options);
2272
+ for (const target of targets) {
2273
+ const adapter = await resolveAdapterForTarget(target, options);
2274
+ const manifest = await readInstallManifest(target.targetRoot, adapter.name);
2275
+ if (!manifest) {
2276
+ console.log(`No install manifest for ${adapter.name} at ${target.targetRoot}`);
2277
+ continue;
2278
+ }
2279
+ const plan = await createUninstallPlan(manifest);
2280
+ console.log(formatPlan(plan));
2281
+ const result = await uninstall(plan, { dryRun: options.dryRun, force: options.force });
2282
+ if (!options.dryRun) {
2283
+ await adapter.programmatic?.uninstall?.({ targetRoot: target.targetRoot, adapterName: adapter.name });
2284
+ console.log(formatUninstallResult(result));
2285
+ }
2286
+ if (plan.hasBlockingChanges) process.exitCode = 1;
2124
2287
  }
2125
- if (!options.dryRun) console.log("Uninstalled.");
2126
- if (plan.hasBlockingChanges) process.exitCode = 1;
2127
2288
  });
2128
- async function buildPlan(source, options) {
2129
- const targetRoot = normalizeTargetRoot(options.targetRoot);
2130
- const adapter = await resolveAdapter({
2131
- adapter: options.adapter,
2132
- adapterConfig: options.adapterConfig,
2133
- adapterModule: options.adapterModule,
2134
- allowAdapterCode: options.allowAdapterCode,
2135
- baseDir: targetRoot,
2136
- warn: (message) => console.warn(message)
2137
- });
2289
+ async function buildPlan(source, target, options) {
2290
+ const adapter = await resolveAdapterForTarget(target, options);
2138
2291
  const result = await createSourcePlan({
2139
2292
  source,
2140
- targetRoot,
2293
+ targetRoot: target.targetRoot,
2294
+ workspaceRoot: target.workspaceRoot,
2141
2295
  adapter,
2142
2296
  driver: options.driver,
2143
2297
  mode: options.mode
2144
2298
  });
2145
2299
  return { plan: result.plan, bundle: result.bundle };
2146
2300
  }
2301
+ async function resolveCliTargets(options) {
2302
+ if (options.all) {
2303
+ return resolveAllRuntimeTargets({ targetRoot: options.targetRoot, adapter: options.adapter, agent: options.agent, all: options.all });
2304
+ }
2305
+ return [await resolveRuntimeTarget({ targetRoot: options.targetRoot, adapter: options.adapter, agent: options.agent })];
2306
+ }
2307
+ async function resolveAdapterForTarget(target, options) {
2308
+ return resolveAdapter({
2309
+ adapter: target.adapter,
2310
+ adapterConfig: options.adapterConfig,
2311
+ adapterModule: options.adapterModule,
2312
+ allowAdapterCode: options.allowAdapterCode,
2313
+ baseDir: target.workspaceRoot,
2314
+ warn: (message) => console.warn(message)
2315
+ });
2316
+ }
2317
+ async function runConfiguredPackages(target, options, behavior) {
2318
+ const config = await readMergedWorkspaceConfig(target.workspaceRoot);
2319
+ if (config.packages.length === 0) {
2320
+ console.log(`No packages configured at ${target.workspaceRoot}.`);
2321
+ return;
2322
+ }
2323
+ for (const pkg of config.packages) {
2324
+ const targetForPackage = options.adapter || target.source !== "cwd" ? target : { ...target, adapter: pkg.adapter };
2325
+ const adapter = await resolveAdapterForTarget(targetForPackage, {
2326
+ adapterConfig: options.adapterConfig ?? pkg.adapterConfig,
2327
+ adapterModule: options.adapterModule ?? pkg.adapterModule,
2328
+ allowAdapterCode: options.allowAdapterCode
2329
+ });
2330
+ if (behavior.useUpdateDecision) {
2331
+ const lock = await readSourceLock(targetForPackage.targetRoot, adapter.name);
2332
+ const decision = shouldUpdatePackage(pkg, lock);
2333
+ if (!decision.shouldUpdate) {
2334
+ console.log(`Skipping ${pkg.name}: ${decision.reason}.`);
2335
+ continue;
2336
+ }
2337
+ }
2338
+ const { plan, bundle } = await buildPlan(pkg.source, targetForPackage, {
2339
+ driver: pkg.driver,
2340
+ adapterConfig: options.adapterConfig ?? pkg.adapterConfig,
2341
+ adapterModule: options.adapterModule ?? pkg.adapterModule,
2342
+ allowAdapterCode: options.allowAdapterCode,
2343
+ mode: pkg.mode
2344
+ });
2345
+ console.log(`${behavior.useUpdateDecision ? "Update" : "Sync"} ${pkg.name} (${adapter.name} at ${targetForPackage.targetRoot}):`);
2346
+ console.log(formatPlan(plan));
2347
+ if (!options.dryRun) {
2348
+ await applyInstallPlan(plan, bundle.sourceLock, { executePlugins: options.executePlugins });
2349
+ console.log(`Applied ${pkg.name}.`);
2350
+ }
2351
+ await rm8(bundle.root, { recursive: true, force: true });
2352
+ if (plan.hasBlockingChanges) process.exitCode = 1;
2353
+ }
2354
+ }
2147
2355
  async function initPackage(root) {
2148
- await mkdir7(join18(root, "instructions"), { recursive: true });
2149
- await mkdir7(join18(root, "rules"), { recursive: true });
2150
- await mkdir7(join18(root, "skills"), { recursive: true });
2151
- const manifestPath = join18(root, "agentwheel.json");
2356
+ await mkdir7(join19(root, "instructions"), { recursive: true });
2357
+ await mkdir7(join19(root, "rules"), { recursive: true });
2358
+ await mkdir7(join19(root, "skills"), { recursive: true });
2359
+ const manifestPath = join19(root, "agentwheel.json");
2152
2360
  const manifest = {
2153
2361
  schemaVersion: 1,
2154
2362
  name: "example/agentwheel-package",
@@ -2161,7 +2369,17 @@ async function initPackage(root) {
2161
2369
  };
2162
2370
  await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
2163
2371
  `, "utf8");
2164
- await writeFile4(join18(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
2372
+ await writeFile4(join19(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
2373
+ }
2374
+ function formatUninstallResult(result) {
2375
+ const removedLabel = result.removed === 1 ? "managed file" : "managed files";
2376
+ if (result.removedDrifted > 0) {
2377
+ const driftedLabel = result.removedDrifted === 1 ? "drifted file" : "drifted files";
2378
+ return `Removed ${result.removed} ${removedLabel}, including ${result.removedDrifted} ${driftedLabel}.`;
2379
+ }
2380
+ if (result.kept === 0) return `Removed ${result.removed} ${removedLabel}.`;
2381
+ const keptLabel = result.kept === 1 ? "drifted file" : "drifted files";
2382
+ return `Removed ${result.removed} ${removedLabel}; kept ${result.kept} ${keptLabel} (use --force to remove).`;
2165
2383
  }
2166
2384
  function printRegistryEntries(entries) {
2167
2385
  for (const entry of entries) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",