agentwheel 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -2
  2. package/dist/index.js +188 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,9 +33,9 @@ No lock-in. No central gatekeeper. Your packages live in plain git repos, your c
33
33
 
34
34
  ---
35
35
 
36
- > **Status: early (v0.4).** The lifecycle core is real and tested — local/git/skillkit/vercel
36
+ > **Status: early (v0.5).** The lifecycle core is real and tested — local/git/skillkit/vercel
37
37
  > sources, optional registry discovery, plan/sync/update/drift/uninstall, overlays, eject/remember,
38
- > profiles, runtime auto-detection, fleet targeting, rich JSON merge, and pluggable adapters.
38
+ > profiles, runtime auto-detection, fleet targeting, asset-includes, rich JSON merge, and pluggable adapters.
39
39
  > Expect sharp edges.
40
40
 
41
41
  ## What it does
@@ -72,6 +72,9 @@ pnpm link --global
72
72
 
73
73
  `plan`, `sync --dry-run`, and `update --dry-run` show exactly what would change before anything is written. They're the commands to trust.
74
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
+
75
78
  ## Runtime targeting
76
79
 
77
80
  Normal use no longer needs `--target-root`. Run agentwheel inside a runtime folder and it detects
@@ -144,6 +147,28 @@ A package is a git repo (or folder) with a JSON manifest and a canonical layout:
144
147
  }
145
148
  ```
146
149
 
150
+ Packages can compose shared files into each directory artifact at staging time. This keeps one
151
+ canonical copy in the package repo while installing self-contained skills:
152
+
153
+ ```jsonc
154
+ {
155
+ "type": "skills",
156
+ "path": "skills",
157
+ "assets": [
158
+ {
159
+ "from": "packages/tmux-bridge/bin",
160
+ "into": "bin",
161
+ "include": ["*.sh"],
162
+ "mode": "preserve"
163
+ }
164
+ ]
165
+ }
166
+ ```
167
+
168
+ `mode: "preserve"` keeps executable bits on copied scripts. The composed files are included in
169
+ the skill directory hash, so idempotency and drift detection work as if the assets had always
170
+ belonged to the skill.
171
+
147
172
  Publish by pushing to any git host. A registry exists only for short names and discovery — it's
148
173
  optional, and `agentwheel add <url|path>` always works without it.
149
174
 
@@ -218,6 +243,7 @@ clear conversion format.
218
243
  - [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.
219
244
  - [x] **v0.3** — skillkit/vercel source drivers; optional registry & federation; programmatic adapters behind `--allow-adapter-code`; rich JSON merge for mcp/hooks/settings; profiles.
220
245
  - [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`.
246
+ - [x] **v0.5** — asset-includes compose shared files into skills at install time; executable bits preserved; hashes include composed assets.
221
247
 
222
248
  ## Design docs
223
249
 
package/dist/index.js CHANGED
@@ -34,6 +34,12 @@ var artifactTypeSchema = z.enum([
34
34
  "plugins"
35
35
  ]);
36
36
  var fileKindSchema = z.enum(["file", "dir"]);
37
+ var packageAssetSchema = z.object({
38
+ from: z.string().min(1),
39
+ into: z.string().min(1),
40
+ include: z.array(z.string().min(1)).optional(),
41
+ mode: z.enum(["preserve", "copy"]).default("preserve")
42
+ });
37
43
  var artifactSchema = z.object({
38
44
  type: artifactTypeSchema,
39
45
  name: z.string().min(1),
@@ -43,7 +49,8 @@ var artifactSchema = z.object({
43
49
  kind: fileKindSchema,
44
50
  hash: z.string().min(16),
45
51
  packageName: z.string().min(1).optional(),
46
- channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed")
52
+ channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
53
+ assets: z.array(packageAssetSchema).optional()
47
54
  });
48
55
 
49
56
  // src/model/adapter.ts
@@ -461,16 +468,52 @@ async function applyInstallPlan(plan, sourceLock, options = {}) {
461
468
  await writeSourceLock(plan.targetRoot, plan.adapter, sourceLock);
462
469
  return manifest;
463
470
  }
464
- async function uninstall(plan, dryRun) {
471
+ async function uninstall(plan, options = {}) {
472
+ const resolvedOptions = typeof options === "boolean" ? { dryRun: options } : options;
465
473
  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(", ")}`);
474
+ const blockers = plan.operations.filter((operation) => operation.action === "conflict");
475
+ throw new Error(`Refusing to uninstall with blocking changes: ${blockers.map((item) => item.relativeDestPath).join(", ")}`);
468
476
  }
469
- if (dryRun) return;
477
+ const removable = plan.operations.filter((operation) => operation.action === "remove" || resolvedOptions.force && operation.action === "keep");
478
+ const kept = resolvedOptions.force ? [] : plan.operations.filter((operation) => operation.action === "keep");
479
+ const removedDrifted = resolvedOptions.force ? plan.operations.filter((operation) => operation.action === "keep").length : 0;
480
+ if (resolvedOptions.dryRun) return { removed: removable.length, kept: kept.length, removedDrifted };
470
481
  for (const operation of plan.operations) {
471
- await rm2(operation.destPath, { recursive: true, force: true });
482
+ if (operation.action === "remove" || resolvedOptions.force && operation.action === "keep") {
483
+ await rm2(operation.destPath, { recursive: true, force: true });
484
+ }
485
+ }
486
+ if (kept.length > 0) {
487
+ const now = (/* @__PURE__ */ new Date()).toISOString();
488
+ await writeInstallManifest({
489
+ version: 1,
490
+ adapter: plan.adapter,
491
+ targetRoot: plan.targetRoot,
492
+ generatedAt: now,
493
+ adapterCode: plan.adapterCode,
494
+ entries: kept.map((operation) => {
495
+ if (!operation.manifestHash || !operation.desiredHash) {
496
+ throw new Error(`Invalid kept operation missing manifest/source hash: ${operation.relativeDestPath}`);
497
+ }
498
+ return {
499
+ path: operation.relativeDestPath,
500
+ artifactType: operation.artifactType,
501
+ artifactName: operation.artifactName,
502
+ kind: operation.kind,
503
+ hash: operation.manifestHash,
504
+ sourceHash: operation.desiredHash,
505
+ updatedAt: now,
506
+ channel: operation.channel,
507
+ packageName: operation.packageName,
508
+ semanticCommand: operation.semanticCommand,
509
+ mergeStrategy: operation.mergeStrategy
510
+ };
511
+ }).sort((a, b) => a.path.localeCompare(b.path))
512
+ });
513
+ } else {
514
+ await removeStateFiles(plan.targetRoot, plan.adapter);
472
515
  }
473
- await removeStateFiles(plan.targetRoot, plan.adapter);
516
+ return { removed: removable.length, kept: kept.length, removedDrifted };
474
517
  }
475
518
 
476
519
  // src/install/plan.ts
@@ -658,6 +701,7 @@ function summarizePlan(plan) {
658
701
  update: 0,
659
702
  skip: 0,
660
703
  remove: 0,
704
+ keep: 0,
661
705
  drift: 0,
662
706
  conflict: 0,
663
707
  plugin: 0,
@@ -679,17 +723,20 @@ async function createUninstallPlan(manifest) {
679
723
  const currentHash = await hashPath(destPath);
680
724
  if (currentHash !== entry.hash) {
681
725
  operations.push({
682
- action: "drift",
726
+ action: "keep",
683
727
  artifactType: entry.artifactType,
684
728
  artifactName: entry.artifactName,
685
729
  kind: entry.kind,
686
730
  destPath,
687
731
  relativeDestPath: entry.path,
732
+ desiredHash: entry.sourceHash,
688
733
  currentHash,
689
734
  manifestHash: entry.hash,
690
- reason: "managed destination changed outside agentwheel",
735
+ reason: "managed destination changed outside agentwheel; keeping by default",
691
736
  channel: entry.channel,
692
- packageName: entry.packageName
737
+ packageName: entry.packageName,
738
+ semanticCommand: entry.semanticCommand,
739
+ mergeStrategy: entry.mergeStrategy
693
740
  });
694
741
  } else {
695
742
  operations.push({
@@ -712,7 +759,8 @@ async function createUninstallPlan(manifest) {
712
759
  adapter: manifest.adapter,
713
760
  targetRoot: manifest.targetRoot,
714
761
  operations,
715
- hasBlockingChanges: operations.some((operation) => operation.action === "drift" || operation.action === "conflict")
762
+ hasBlockingChanges: operations.some((operation) => operation.action === "conflict"),
763
+ adapterCode: manifest.adapterCode
716
764
  };
717
765
  }
718
766
 
@@ -722,6 +770,7 @@ var labels = {
722
770
  update: "UPDATE",
723
771
  skip: "SKIP",
724
772
  remove: "REMOVE",
773
+ keep: "KEEP",
725
774
  drift: "DRIFT",
726
775
  conflict: "CONFLICT",
727
776
  plugin: "PLUGIN",
@@ -743,7 +792,7 @@ function formatPlan(plan) {
743
792
  }
744
793
  const summary = summarizePlan(plan);
745
794
  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}`
795
+ `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
796
  );
748
797
  return lines.join("\n");
749
798
  }
@@ -762,7 +811,8 @@ import { parse as parse2, printParseErrorCode as printParseErrorCode2 } from "js
762
811
  import { z as z4 } from "zod";
763
812
  var packageProvideSchema = z4.object({
764
813
  type: artifactTypeSchema,
765
- path: z4.string().min(1)
814
+ path: z4.string().min(1),
815
+ assets: z4.array(packageAssetSchema).optional()
766
816
  });
767
817
  var packageManifestSchema = z4.object({
768
818
  schemaVersion: z4.literal(1),
@@ -931,7 +981,7 @@ async function listFromManifest(root, packageName) {
931
981
  const stats = await stat2(full);
932
982
  if (provide.type === "instructions") {
933
983
  if (stats.isFile()) {
934
- artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
984
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName, provide.assets));
935
985
  }
936
986
  continue;
937
987
  }
@@ -939,16 +989,16 @@ async function listFromManifest(root, packageName) {
939
989
  for (const entry of await sortedDirEntries(full)) {
940
990
  const child = join6(full, entry.name);
941
991
  if (provide.type === "skills" && entry.isDirectory()) {
942
- artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
992
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName, provide.assets));
943
993
  } else if (provide.type === "plugins" && entry.isDirectory()) {
944
- artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
994
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName, provide.assets));
945
995
  } else if (entry.isFile()) {
946
996
  const name = provide.type === "rules" && entry.name.endsWith(".md") ? entry.name : entry.name;
947
- artifacts.push(await artifactForFile(provide.type, name, child, join6(provide.path, entry.name), packageName));
997
+ artifacts.push(await artifactForFile(provide.type, name, child, join6(provide.path, entry.name), packageName, provide.assets));
948
998
  }
949
999
  }
950
1000
  } else if (stats.isFile()) {
951
- artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
1001
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName, provide.assets));
952
1002
  }
953
1003
  }
954
1004
  return artifacts;
@@ -965,7 +1015,7 @@ async function listGenericArtifacts(type, dir, relativeRoot, packageName) {
965
1015
  }
966
1016
  return artifacts;
967
1017
  }
968
- async function artifactForFile(type, name, sourcePath, relativePath, packageName) {
1018
+ async function artifactForFile(type, name, sourcePath, relativePath, packageName, assets) {
969
1019
  return {
970
1020
  type,
971
1021
  name,
@@ -974,10 +1024,11 @@ async function artifactForFile(type, name, sourcePath, relativePath, packageName
974
1024
  kind: "file",
975
1025
  hash: await hashPath(sourcePath),
976
1026
  packageName,
977
- channel: "managed"
1027
+ channel: "managed",
1028
+ assets
978
1029
  };
979
1030
  }
980
- async function artifactForDir(type, name, sourcePath, relativePath, packageName) {
1031
+ async function artifactForDir(type, name, sourcePath, relativePath, packageName, assets) {
981
1032
  return {
982
1033
  type,
983
1034
  name,
@@ -986,7 +1037,8 @@ async function artifactForDir(type, name, sourcePath, relativePath, packageName)
986
1037
  kind: "dir",
987
1038
  hash: await hashPath(sourcePath),
988
1039
  packageName,
989
- channel: "managed"
1040
+ channel: "managed",
1041
+ assets
990
1042
  };
991
1043
  }
992
1044
 
@@ -1421,8 +1473,8 @@ function getSourceDriver(name = "local") {
1421
1473
  }
1422
1474
 
1423
1475
  // src/staging/staging.ts
1424
- import { cp as cp3, mkdir as mkdir5, mkdtemp } from "fs/promises";
1425
- import { dirname as dirname5, join as join12 } from "path";
1476
+ import { chmod, cp as cp3, mkdir as mkdir5, mkdtemp, readdir as readdir4, stat as stat6 } from "fs/promises";
1477
+ import { basename as basename7, dirname as dirname5, join as join12, relative as relative2, resolve as resolve8, sep } from "path";
1426
1478
  import { tmpdir as tmpdir2 } from "os";
1427
1479
 
1428
1480
  // src/staging/customize.ts
@@ -1541,6 +1593,7 @@ async function stageSource(driver, source, options = {}) {
1541
1593
  const stagedPath = join12(root, artifact.relativePath);
1542
1594
  await mkdir5(dirname5(stagedPath), { recursive: true });
1543
1595
  await cp3(artifact.sourcePath, stagedPath, { recursive: artifact.kind === "dir", dereference: true });
1596
+ await composeAssets(artifact, resolved.resolvedPath, stagedPath);
1544
1597
  stagedArtifacts.push({
1545
1598
  ...artifact,
1546
1599
  stagedPath,
@@ -1581,11 +1634,93 @@ async function stageSource(driver, source, options = {}) {
1581
1634
  }
1582
1635
  };
1583
1636
  }
1637
+ async function composeAssets(artifact, packageRoot, stagedPath) {
1638
+ if (!artifact.assets?.length) return;
1639
+ if (artifact.kind !== "dir") {
1640
+ throw new Error(`Asset includes require a directory artifact: ${artifact.type}/${artifact.name}`);
1641
+ }
1642
+ for (const asset of artifact.assets) {
1643
+ const source = resolvePackagePath(packageRoot, asset.from);
1644
+ const dest = join12(stagedPath, asset.into);
1645
+ await copyAsset(asset, source, dest);
1646
+ }
1647
+ }
1648
+ async function copyAsset(asset, source, dest) {
1649
+ const sourceStats = await stat6(source);
1650
+ if (sourceStats.isFile()) {
1651
+ if (matchesAny(basename7(source), asset.include)) {
1652
+ await mkdir5(dest, { recursive: true });
1653
+ await copyAssetFile(source, join12(dest, basename7(source)), asset);
1654
+ }
1655
+ return;
1656
+ }
1657
+ if (!sourceStats.isDirectory()) {
1658
+ throw new Error(`Asset include source is not a file or directory: ${source}`);
1659
+ }
1660
+ if (!asset.include?.length) {
1661
+ await mkdir5(dirname5(dest), { recursive: true });
1662
+ await cp3(source, dest, { recursive: true, dereference: true });
1663
+ if (asset.mode === "copy") await normalizeCopiedModes(dest);
1664
+ return;
1665
+ }
1666
+ for (const file of await listFiles(source)) {
1667
+ const rel = relative2(source, file).replaceAll("\\", "/");
1668
+ if (!matchesAny(rel, asset.include) && !matchesAny(basename7(file), asset.include)) continue;
1669
+ await copyAssetFile(file, join12(dest, rel), asset);
1670
+ }
1671
+ }
1672
+ async function copyAssetFile(source, dest, asset) {
1673
+ await mkdir5(dirname5(dest), { recursive: true });
1674
+ await cp3(source, dest, { dereference: true });
1675
+ if (asset.mode === "copy") await chmod(dest, 420);
1676
+ }
1677
+ function resolvePackagePath(packageRoot, path) {
1678
+ const resolved = resolve8(packageRoot, path);
1679
+ const root = resolve8(packageRoot);
1680
+ if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) {
1681
+ throw new Error(`Asset include escapes package root: ${path}`);
1682
+ }
1683
+ return resolved;
1684
+ }
1685
+ async function listFiles(root) {
1686
+ const out = [];
1687
+ async function walk2(dir) {
1688
+ for (const entry of (await readdir4(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
1689
+ const full = join12(dir, entry.name);
1690
+ if (entry.isDirectory()) {
1691
+ await walk2(full);
1692
+ } else if (entry.isFile()) {
1693
+ out.push(full);
1694
+ }
1695
+ }
1696
+ }
1697
+ await walk2(root);
1698
+ return out;
1699
+ }
1700
+ async function normalizeCopiedModes(path) {
1701
+ const stats = await stat6(path);
1702
+ if (stats.isFile()) {
1703
+ await chmod(path, 420);
1704
+ return;
1705
+ }
1706
+ if (!stats.isDirectory()) return;
1707
+ for (const entry of await readdir4(path, { withFileTypes: true })) {
1708
+ await normalizeCopiedModes(join12(path, entry.name));
1709
+ }
1710
+ }
1711
+ function matchesAny(path, patterns) {
1712
+ if (!patterns?.length) return true;
1713
+ return patterns.some((pattern) => matchesGlob(path, pattern));
1714
+ }
1715
+ function matchesGlob(path, pattern) {
1716
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1717
+ return new RegExp(`^${escaped}$`).test(path);
1718
+ }
1584
1719
 
1585
1720
  // src/model/workspace.ts
1586
1721
  import { readFile as readFile8 } from "fs/promises";
1587
1722
  import { homedir as homedir3 } from "os";
1588
- import { dirname as dirname6, join as join13, resolve as resolve8 } from "path";
1723
+ import { dirname as dirname6, join as join13, resolve as resolve9 } from "path";
1589
1724
  import { z as z5 } from "zod";
1590
1725
  var workspacePackageSchema = z5.object({
1591
1726
  name: z5.string().min(1),
@@ -1645,11 +1780,11 @@ function globalWorkspaceConfigPath(globalRoot = homedir3()) {
1645
1780
  return join13(globalRoot, ".agentwheel", "config.json");
1646
1781
  }
1647
1782
  async function findWorkspaceRoot(start = process.cwd()) {
1648
- let current = resolve8(start);
1783
+ let current = resolve9(start);
1649
1784
  while (true) {
1650
1785
  if (await pathExists(workspaceConfigPath(current))) return current;
1651
1786
  const parent = dirname6(current);
1652
- if (parent === current) return resolve8(start);
1787
+ if (parent === current) return resolve9(start);
1653
1788
  current = parent;
1654
1789
  }
1655
1790
  }
@@ -1673,9 +1808,9 @@ function mergeWorkspaceConfig(global, project) {
1673
1808
  });
1674
1809
  }
1675
1810
  function resolveConfigPath(path, baseRoot) {
1676
- if (path.startsWith("~/")) return resolve8(homedir3(), path.slice(2));
1811
+ if (path.startsWith("~/")) return resolve9(homedir3(), path.slice(2));
1677
1812
  if (path === "~") return homedir3();
1678
- return path.startsWith("/") ? resolve8(path) : resolve8(baseRoot, path);
1813
+ return path.startsWith("/") ? resolve9(path) : resolve9(baseRoot, path);
1679
1814
  }
1680
1815
  function emptyWorkspaceConfig() {
1681
1816
  return { schemaVersion: 1, packages: [], registry: {}, profiles: {}, agents: {} };
@@ -1739,9 +1874,9 @@ import { rm as rm7 } from "fs/promises";
1739
1874
  import { join as join16 } from "path";
1740
1875
 
1741
1876
  // src/registry/client.ts
1742
- import { readFile as readFile9, rm as rm6, stat as stat6 } from "fs/promises";
1877
+ import { readFile as readFile9, rm as rm6, stat as stat7 } from "fs/promises";
1743
1878
  import { homedir as homedir4 } from "os";
1744
- import { dirname as dirname8, join as join15, resolve as resolve9 } from "path";
1879
+ import { dirname as dirname8, join as join15, resolve as resolve10 } from "path";
1745
1880
  import { fileURLToPath } from "url";
1746
1881
 
1747
1882
  // src/model/registry.ts
@@ -1844,8 +1979,8 @@ var RegistryClient = class {
1844
1979
  }
1845
1980
  const filePath = source.startsWith("file:") ? fileURLToPath(source) : source;
1846
1981
  if (await pathExists(filePath)) {
1847
- const fullPath = resolve9(filePath);
1848
- const stats = await stat6(fullPath);
1982
+ const fullPath = resolve10(filePath);
1983
+ const stats = await stat7(fullPath);
1849
1984
  return readFile9(stats.isDirectory() ? join15(fullPath, "index.json") : fullPath, "utf8");
1850
1985
  }
1851
1986
  const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join15(dirname8(this.cachePath), "registry-repos") }));
@@ -1979,7 +2114,7 @@ function shouldUpdatePackage(pkg, lock) {
1979
2114
  }
1980
2115
 
1981
2116
  // src/runtime/target.ts
1982
- import { basename as basename7, dirname as dirname9, join as join18, resolve as resolve10 } from "path";
2117
+ import { basename as basename8, dirname as dirname9, join as join18, resolve as resolve11 } from "path";
1983
2118
  var runtimeMarkers = [
1984
2119
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
1985
2120
  { adapter: "claude", dirs: [".claude"] },
@@ -1988,9 +2123,9 @@ var runtimeMarkers = [
1988
2123
  { adapter: "copilot", dirs: [".github"] }
1989
2124
  ];
1990
2125
  async function resolveRuntimeTarget(request = {}) {
1991
- const cwd = resolve10(request.cwd ?? process.cwd());
2126
+ const cwd = resolve11(request.cwd ?? process.cwd());
1992
2127
  if (request.targetRoot) {
1993
- const targetRoot = resolve10(request.targetRoot);
2128
+ const targetRoot = resolve11(request.targetRoot);
1994
2129
  return {
1995
2130
  adapter: request.adapter ?? "openclaw",
1996
2131
  targetRoot,
@@ -2017,7 +2152,7 @@ async function resolveRuntimeTarget(request = {}) {
2017
2152
  async function resolveAllRuntimeTargets(request = {}) {
2018
2153
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
2019
2154
  if (request.agent) return [await resolveRuntimeTarget(request)];
2020
- const cwd = resolve10(request.cwd ?? process.cwd());
2155
+ const cwd = resolve11(request.cwd ?? process.cwd());
2021
2156
  const workspaceRoot = await findWorkspaceRoot(cwd);
2022
2157
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
2023
2158
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot));
@@ -2027,12 +2162,12 @@ async function resolveAllRuntimeTargets(request = {}) {
2027
2162
  return targets;
2028
2163
  }
2029
2164
  async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
2030
- const root = resolve10(cwd);
2165
+ const root = resolve11(cwd);
2031
2166
  const matches = [];
2032
2167
  for (const marker of runtimeMarkers) {
2033
2168
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
2034
2169
  for (const dir of marker.dirs) {
2035
- if (basename7(root) === dir) {
2170
+ if (basename8(root) === dir) {
2036
2171
  matches.push({ adapter: marker.adapter, targetRoot: dirname9(root) });
2037
2172
  } else if (await pathExists(join18(root, dir))) {
2038
2173
  matches.push({ adapter: marker.adapter, targetRoot: root });
@@ -2068,7 +2203,7 @@ function dedupeTargets(matches) {
2068
2203
 
2069
2204
  // src/cli/index.ts
2070
2205
  var program = new Command();
2071
- program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.4.0");
2206
+ program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.5.0");
2072
2207
  program.command("init").argument("[kind]", "workspace or package", "workspace").option("--target-root <path>", "workspace root", process.cwd()).action(async (kind, options) => {
2073
2208
  const root = normalizeTargetRoot(options.targetRoot);
2074
2209
  if (kind === "package") {
@@ -2225,7 +2360,7 @@ program.command("eject").argument("<item>", "package/type/name").option("--targe
2225
2360
  const result = await ejectArtifact(targetRoot, item);
2226
2361
  console.log(`Ejected ${item} to ${result.ejectedPath}.`);
2227
2362
  });
2228
- 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).action(async (options) => {
2363
+ 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) => {
2229
2364
  const targets = await resolveCliTargets(options);
2230
2365
  for (const target of targets) {
2231
2366
  const adapter = await resolveAdapterForTarget(target, options);
@@ -2236,10 +2371,10 @@ program.command("uninstall").option("--adapter <adapter>", "adapter", "openclaw"
2236
2371
  }
2237
2372
  const plan = await createUninstallPlan(manifest);
2238
2373
  console.log(formatPlan(plan));
2239
- await uninstall(plan, options.dryRun);
2374
+ const result = await uninstall(plan, { dryRun: options.dryRun, force: options.force });
2240
2375
  if (!options.dryRun) {
2241
2376
  await adapter.programmatic?.uninstall?.({ targetRoot: target.targetRoot, adapterName: adapter.name });
2242
- console.log(`Uninstalled ${adapter.name} at ${target.targetRoot}.`);
2377
+ console.log(formatUninstallResult(result));
2243
2378
  }
2244
2379
  if (plan.hasBlockingChanges) process.exitCode = 1;
2245
2380
  }
@@ -2329,6 +2464,16 @@ async function initPackage(root) {
2329
2464
  `, "utf8");
2330
2465
  await writeFile4(join19(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
2331
2466
  }
2467
+ function formatUninstallResult(result) {
2468
+ const removedLabel = result.removed === 1 ? "managed file" : "managed files";
2469
+ if (result.removedDrifted > 0) {
2470
+ const driftedLabel = result.removedDrifted === 1 ? "drifted file" : "drifted files";
2471
+ return `Removed ${result.removed} ${removedLabel}, including ${result.removedDrifted} ${driftedLabel}.`;
2472
+ }
2473
+ if (result.kept === 0) return `Removed ${result.removed} ${removedLabel}.`;
2474
+ const keptLabel = result.kept === 1 ? "drifted file" : "drifted files";
2475
+ return `Removed ${result.removed} ${removedLabel}; kept ${result.kept} ${keptLabel} (use --force to remove).`;
2476
+ }
2332
2477
  function printRegistryEntries(entries) {
2333
2478
  for (const entry of entries) {
2334
2479
  const tags = entry.tags?.length ? ` [${entry.tags.join(",")}]` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",