asdm-cli 0.1.2 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -10,33 +10,19 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/utils/fs.ts
13
- var fs_exports = {};
14
- __export(fs_exports, {
15
- copyFile: () => copyFile,
16
- ensureDir: () => ensureDir,
17
- exists: () => exists,
18
- getAsdmCacheDir: () => getAsdmCacheDir,
19
- getAsdmConfigDir: () => getAsdmConfigDir,
20
- listFiles: () => listFiles,
21
- normalizePath: () => normalizePath,
22
- readFile: () => readFile,
23
- readJson: () => readJson,
24
- removeFile: () => removeFile,
25
- writeFile: () => writeFile,
26
- writeJson: () => writeJson
27
- });
28
13
  import { promises as fs } from "fs";
29
14
  import path from "path";
30
15
  import os from "os";
31
- function normalizePath(p) {
32
- return p.replace(/\\/g, "/");
33
- }
34
16
  async function ensureDir(dirPath) {
35
17
  await fs.mkdir(dirPath, { recursive: true });
36
18
  }
37
19
  async function writeFile(filePath, content) {
38
20
  await ensureDir(path.dirname(filePath));
39
- await fs.writeFile(filePath, content, "utf-8");
21
+ if (typeof content === "string") {
22
+ await fs.writeFile(filePath, content, "utf-8");
23
+ } else {
24
+ await fs.writeFile(filePath, content);
25
+ }
40
26
  }
41
27
  async function readFile(filePath) {
42
28
  try {
@@ -80,6 +66,23 @@ async function listFiles(dirPath) {
80
66
  function getAsdmConfigDir() {
81
67
  return path.join(os.homedir(), ".config", "asdm");
82
68
  }
69
+ function resolveGlobalEmitPath(relativePath, provider) {
70
+ const prefix = PROVIDER_PATH_PREFIXES[provider];
71
+ const globalDir = PROVIDER_GLOBAL_DIRS[provider];
72
+ if (!prefix || !globalDir) return null;
73
+ if (!relativePath.startsWith(prefix)) return null;
74
+ const stripped = relativePath.slice(prefix.length);
75
+ const resolved = path.resolve(globalDir, stripped);
76
+ const safeBase = path.resolve(globalDir) + path.sep;
77
+ if (!resolved.startsWith(safeBase)) return null;
78
+ return resolved;
79
+ }
80
+ function getGlobalLockfilePath() {
81
+ return path.join(getAsdmConfigDir(), "global-lock.json");
82
+ }
83
+ function getGlobalConfigPath() {
84
+ return path.join(getAsdmConfigDir(), "config.json");
85
+ }
83
86
  function getAsdmCacheDir() {
84
87
  const xdgCache = process.env["XDG_CACHE_HOME"];
85
88
  if (xdgCache) return path.join(xdgCache, "asdm");
@@ -93,13 +96,23 @@ async function readJson(filePath) {
93
96
  async function writeJson(filePath, data) {
94
97
  await writeFile(filePath, JSON.stringify(data, null, 2));
95
98
  }
96
- async function copyFile(src, dest) {
97
- await ensureDir(path.dirname(dest));
98
- await fs.copyFile(src, dest);
99
- }
99
+ var PROVIDER_GLOBAL_DIRS, PROVIDER_PATH_PREFIXES;
100
100
  var init_fs = __esm({
101
101
  "src/utils/fs.ts"() {
102
102
  "use strict";
103
+ PROVIDER_GLOBAL_DIRS = {
104
+ opencode: process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "opencode") : path.join(os.homedir(), ".config", "opencode"),
105
+ "claude-code": process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "Claude") : path.join(os.homedir(), ".claude"),
106
+ copilot: process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "GitHub Copilot") : path.join(os.homedir(), ".config", "github-copilot"),
107
+ // agents-dir uses %USERPROFILE%\.agents on all platforms (no AppData variant)
108
+ "agents-dir": path.join(os.homedir(), ".agents")
109
+ };
110
+ PROVIDER_PATH_PREFIXES = {
111
+ opencode: ".opencode/",
112
+ "claude-code": ".claude/",
113
+ copilot: ".github/",
114
+ "agents-dir": ".agents/"
115
+ };
103
116
  }
104
117
  });
105
118
 
@@ -531,8 +544,79 @@ var init_copilot = __esm({
531
544
  }
532
545
  });
533
546
 
547
+ // src/adapters/agents-dir.ts
548
+ var agents_dir_exports = {};
549
+ __export(agents_dir_exports, {
550
+ AgentsDirAdapter: () => AgentsDirAdapter,
551
+ createAgentsDirAdapter: () => createAgentsDirAdapter
552
+ });
553
+ import path11 from "path";
554
+ function formatAgentContent4(parsed) {
555
+ return [managedFileHeader(ADAPTER_NAME4), "", parsed.body].join("\n");
556
+ }
557
+ function formatSkillContent4(parsed) {
558
+ return [managedFileHeader(ADAPTER_NAME4), "", parsed.body].join("\n");
559
+ }
560
+ function formatCommandContent3(parsed) {
561
+ return [managedFileHeader(ADAPTER_NAME4), "", parsed.body].join("\n");
562
+ }
563
+ function createAgentsDirAdapter() {
564
+ return new AgentsDirAdapter();
565
+ }
566
+ var ADAPTER_NAME4, AgentsDirAdapter;
567
+ var init_agents_dir = __esm({
568
+ "src/adapters/agents-dir.ts"() {
569
+ "use strict";
570
+ init_fs();
571
+ init_base();
572
+ ADAPTER_NAME4 = "agents-dir";
573
+ AgentsDirAdapter = class {
574
+ name = ADAPTER_NAME4;
575
+ emitAgent(parsed, _targetDir) {
576
+ const relativePath = `.agents/agents/${parsed.name}.md`;
577
+ const content = formatAgentContent4(parsed);
578
+ return [createEmittedFile(relativePath, content, ADAPTER_NAME4, parsed.sourcePath)];
579
+ }
580
+ emitSkill(parsed, _targetDir) {
581
+ const relativePath = `.agents/skills/${parsed.name}/SKILL.md`;
582
+ const content = formatSkillContent4(parsed);
583
+ return [createEmittedFile(relativePath, content, ADAPTER_NAME4, parsed.sourcePath)];
584
+ }
585
+ emitCommand(parsed, _targetDir) {
586
+ const relativePath = `.agents/commands/${parsed.name}.md`;
587
+ const content = formatCommandContent3(parsed);
588
+ return [createEmittedFile(relativePath, content, ADAPTER_NAME4, parsed.sourcePath)];
589
+ }
590
+ emitRootInstructions(_profile, _targetDir) {
591
+ return [];
592
+ }
593
+ emitConfig(_profile, _targetDir) {
594
+ return [];
595
+ }
596
+ /**
597
+ * Remove all ASDM-managed files from .agents/ in the given project root.
598
+ * Returns the list of absolute paths that were removed.
599
+ */
600
+ async clean(projectRoot) {
601
+ const agentsDir = path11.join(projectRoot, ".agents");
602
+ if (!await exists(agentsDir)) return [];
603
+ const allFiles = await listFiles(agentsDir);
604
+ const removedPaths = [];
605
+ for (const filePath of allFiles) {
606
+ const content = await readFile(filePath);
607
+ if (content && content.includes("ASDM MANAGED FILE")) {
608
+ await removeFile(filePath);
609
+ removedPaths.push(filePath);
610
+ }
611
+ }
612
+ return removedPaths;
613
+ }
614
+ };
615
+ }
616
+ });
617
+
534
618
  // src/cli/index.ts
535
- import { defineCommand as defineCommand16, runMain } from "citty";
619
+ import { defineCommand as defineCommand17, runMain } from "citty";
536
620
 
537
621
  // src/cli/commands/init.ts
538
622
  init_fs();
@@ -679,6 +763,26 @@ function parseRegistryUrl(url) {
679
763
  }
680
764
  return { org: match[1], repo: match[2] };
681
765
  }
766
+ function validateProjectConfig(config, label) {
767
+ if (!config.registry) {
768
+ throw new ConfigError(`Missing required field 'registry' in ${label}`);
769
+ }
770
+ if (!config.profile) {
771
+ throw new ConfigError(`Missing required field 'profile' in ${label}`);
772
+ }
773
+ parseRegistryUrl(config.registry);
774
+ }
775
+ async function readProjectConfigFromPath(filePath) {
776
+ const config = await readJson(filePath);
777
+ if (!config) {
778
+ throw new ConfigError(
779
+ `No config found at ${filePath}`,
780
+ "Run `asdm init` to initialize"
781
+ );
782
+ }
783
+ validateProjectConfig(config, path3.basename(filePath));
784
+ return config;
785
+ }
682
786
  async function readProjectConfig(cwd) {
683
787
  const filePath = path3.join(cwd, PROJECT_CONFIG_FILE);
684
788
  const config = await readJson(filePath);
@@ -688,13 +792,7 @@ async function readProjectConfig(cwd) {
688
792
  "Run `asdm init --profile <name>` to initialize"
689
793
  );
690
794
  }
691
- if (!config.registry) {
692
- throw new ConfigError(`Missing required field 'registry' in ${PROJECT_CONFIG_FILE}`);
693
- }
694
- if (!config.profile) {
695
- throw new ConfigError(`Missing required field 'profile' in ${PROJECT_CONFIG_FILE}`);
696
- }
697
- parseRegistryUrl(config.registry);
795
+ validateProjectConfig(config, PROJECT_CONFIG_FILE);
698
796
  return config;
699
797
  }
700
798
  async function readUserConfig(cwd) {
@@ -729,8 +827,7 @@ Run \`asdm profiles\` to see available profiles`
729
827
  policy
730
828
  };
731
829
  }
732
- async function createProjectConfig(cwd, registry, profile, providers = ["opencode"]) {
733
- const filePath = path3.join(cwd, PROJECT_CONFIG_FILE);
830
+ async function createProjectConfigAtPath(filePath, registry, profile, providers = ["opencode"]) {
734
831
  const config = {
735
832
  $schema: "https://asdm.dev/schemas/config.schema.json",
736
833
  registry,
@@ -739,6 +836,10 @@ async function createProjectConfig(cwd, registry, profile, providers = ["opencod
739
836
  };
740
837
  await writeJson(filePath, config);
741
838
  }
839
+ async function createProjectConfig(cwd, registry, profile, providers = ["opencode"]) {
840
+ const filePath = path3.join(cwd, PROJECT_CONFIG_FILE);
841
+ await createProjectConfigAtPath(filePath, registry, profile, providers);
842
+ }
742
843
 
743
844
  // src/core/telemetry.ts
744
845
  init_hash();
@@ -755,7 +856,7 @@ var TelemetryWriter = class {
755
856
  const fullEvent = {
756
857
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
757
858
  machineId: machineId(),
758
- version: "0.1.2",
859
+ version: "0.3.0",
759
860
  ...event
760
861
  };
761
862
  const line = JSON.stringify(fullEvent) + "\n";
@@ -924,7 +1025,12 @@ var init_default = defineCommand({
924
1025
  },
925
1026
  force: {
926
1027
  type: "boolean",
927
- description: "Overwrite existing .asdm.json",
1028
+ description: "Overwrite existing config",
1029
+ default: false
1030
+ },
1031
+ global: {
1032
+ type: "boolean",
1033
+ description: "Write config to ~/.config/asdm/config.json instead of .asdm.json",
928
1034
  default: false
929
1035
  },
930
1036
  gitignore: {
@@ -935,15 +1041,36 @@ var init_default = defineCommand({
935
1041
  },
936
1042
  async run(ctx) {
937
1043
  const cwd = process.cwd();
1044
+ const profile = ctx.args.profile || "base";
1045
+ const registry = ctx.args.registry || DEFAULT_REGISTRY;
1046
+ const providers = ["opencode"];
1047
+ if (ctx.args.global) {
1048
+ const targetPath = getGlobalConfigPath();
1049
+ const alreadyExists2 = await exists(targetPath);
1050
+ if (alreadyExists2 && !ctx.args.force) {
1051
+ logger.warn(`Global config already exists at ${targetPath}. Use --force to overwrite.`);
1052
+ return;
1053
+ }
1054
+ try {
1055
+ await ensureDir(path5.dirname(targetPath));
1056
+ await createProjectConfigAtPath(targetPath, registry, profile, providers);
1057
+ logger.success(`Global config written to ${targetPath}`);
1058
+ logger.info(`Registry: ${registry}`);
1059
+ logger.info("Next step: run `asdm sync --global` to install agents, skills, and commands");
1060
+ } catch (err) {
1061
+ const message = err instanceof Error ? err.message : String(err);
1062
+ logger.error(message);
1063
+ process.exitCode = 1;
1064
+ return;
1065
+ }
1066
+ return;
1067
+ }
938
1068
  const configPath = path5.join(cwd, ".asdm.json");
939
1069
  const alreadyExists = await exists(configPath);
940
1070
  if (alreadyExists && !ctx.args.force) {
941
1071
  logger.warn(".asdm.json already exists. Use --force to overwrite.");
942
1072
  return;
943
1073
  }
944
- const profile = ctx.args.profile || "base";
945
- const registry = ctx.args.registry || DEFAULT_REGISTRY;
946
- const providers = ["opencode"];
947
1074
  try {
948
1075
  await createProjectConfig(cwd, registry, profile, providers);
949
1076
  logger.success(`Initialized .asdm.json with profile "${profile}"`);
@@ -973,9 +1100,10 @@ var init_default = defineCommand({
973
1100
 
974
1101
  // src/cli/commands/sync.ts
975
1102
  import { defineCommand as defineCommand2 } from "citty";
1103
+ import path13 from "path";
976
1104
 
977
1105
  // src/core/syncer.ts
978
- import path11 from "path";
1106
+ import path12 from "path";
979
1107
 
980
1108
  // src/core/registry-client.ts
981
1109
  var GITHUB_API_BASE = "https://api.github.com";
@@ -1244,16 +1372,16 @@ function diffManifest(manifest, localShas, assetPaths) {
1244
1372
  function getProfileAssetPaths(manifest, agentNames, skillNames, commandNames) {
1245
1373
  const paths = [];
1246
1374
  for (const name of agentNames) {
1247
- const path18 = `agents/${name}.asdm.md`;
1248
- if (manifest.assets[path18]) paths.push(path18);
1375
+ const path22 = `agents/${name}.asdm.md`;
1376
+ if (manifest.assets[path22]) paths.push(path22);
1249
1377
  }
1250
1378
  for (const name of skillNames) {
1251
- const path18 = `skills/${name}/SKILL.asdm.md`;
1252
- if (manifest.assets[path18]) paths.push(path18);
1379
+ const path22 = `skills/${name}/SKILL.asdm.md`;
1380
+ if (manifest.assets[path22]) paths.push(path22);
1253
1381
  }
1254
1382
  for (const name of commandNames) {
1255
- const path18 = `commands/${name}.asdm.md`;
1256
- if (manifest.assets[path18]) paths.push(path18);
1383
+ const path22 = `commands/${name}.asdm.md`;
1384
+ if (manifest.assets[path22]) paths.push(path22);
1257
1385
  }
1258
1386
  return paths;
1259
1387
  }
@@ -1264,13 +1392,13 @@ async function loadCachedManifest(cacheDir) {
1264
1392
  // src/core/lockfile.ts
1265
1393
  init_fs();
1266
1394
  import path7 from "path";
1267
- var LOCKFILE_NAME = ".asdm-lock.json";
1268
- async function readLockfile(cwd) {
1269
- const filePath = path7.join(cwd, LOCKFILE_NAME);
1395
+ var LOCKFILE_FILENAME = ".asdm-lock.json";
1396
+ async function readLockfile(cwd, lockfilePath) {
1397
+ const filePath = lockfilePath ?? path7.join(cwd, LOCKFILE_FILENAME);
1270
1398
  return readJson(filePath);
1271
1399
  }
1272
- async function writeLockfile(cwd, lockfile) {
1273
- const filePath = path7.join(cwd, LOCKFILE_NAME);
1400
+ async function writeLockfile(cwd, lockfile, lockfilePath) {
1401
+ const filePath = lockfilePath ?? path7.join(cwd, LOCKFILE_FILENAME);
1274
1402
  await writeJson(filePath, {
1275
1403
  $schema: "https://asdm.dev/schemas/lock.schema.json",
1276
1404
  ...lockfile
@@ -1373,12 +1501,17 @@ async function loadAdapters(providers) {
1373
1501
  adapters.push(createCopilotAdapter2());
1374
1502
  break;
1375
1503
  }
1504
+ case "agents-dir": {
1505
+ const { createAgentsDirAdapter: createAgentsDirAdapter2 } = await Promise.resolve().then(() => (init_agents_dir(), agents_dir_exports));
1506
+ adapters.push(createAgentsDirAdapter2());
1507
+ break;
1508
+ }
1376
1509
  }
1377
1510
  }
1378
1511
  return adapters;
1379
1512
  }
1380
1513
  async function getCliVersion() {
1381
- return "0.1.2";
1514
+ return "0.3.0";
1382
1515
  }
1383
1516
  async function sync(options) {
1384
1517
  const startTime = Date.now();
@@ -1386,7 +1519,8 @@ async function sync(options) {
1386
1519
  options.telemetry?.write({ event: "sync.started" }).catch(() => {
1387
1520
  });
1388
1521
  try {
1389
- const projectConfig = await readProjectConfig(cwd);
1522
+ const configFilePath = options.configPath ?? path12.join(cwd, ".asdm.json");
1523
+ const projectConfig = await readProjectConfigFromPath(configFilePath);
1390
1524
  const userConfig = await readUserConfig(cwd);
1391
1525
  const client = new RegistryClient(projectConfig.registry);
1392
1526
  const manifest = await client.getLatestManifest();
@@ -1399,7 +1533,8 @@ async function sync(options) {
1399
1533
  resolvedProfile.skills,
1400
1534
  resolvedProfile.commands
1401
1535
  );
1402
- const existingLockfile = options.force ? null : await readLockfile(cwd);
1536
+ const lockfilePath = options.global ? getGlobalLockfilePath() : void 0;
1537
+ const existingLockfile = options.force ? null : await readLockfile(cwd, lockfilePath);
1403
1538
  const localSourceShas = {};
1404
1539
  if (existingLockfile) {
1405
1540
  for (const [, entry] of Object.entries(existingLockfile.files)) {
@@ -1410,7 +1545,7 @@ async function sync(options) {
1410
1545
  }
1411
1546
  const diff = diffManifest(manifest, localSourceShas, assetPaths);
1412
1547
  const toDownload = options.force ? assetPaths : [...diff.added, ...diff.updated];
1413
- const cacheDir = path11.join(getAsdmCacheDir(), manifest.version);
1548
+ const cacheDir = path12.join(getAsdmCacheDir(), manifest.version);
1414
1549
  await ensureDir(cacheDir);
1415
1550
  const downloadedAssets = /* @__PURE__ */ new Map();
1416
1551
  for (const assetPath of toDownload) {
@@ -1424,17 +1559,16 @@ async function sync(options) {
1424
1559
  "The asset may have been tampered with or the manifest is stale. Run `asdm sync --force`."
1425
1560
  );
1426
1561
  }
1427
- const cachedPath = path11.join(cacheDir, assetPath);
1562
+ const cachedPath = path12.join(cacheDir, assetPath);
1428
1563
  if (!options.dryRun) {
1429
1564
  await writeFile(cachedPath, content);
1430
1565
  }
1431
1566
  downloadedAssets.set(assetPath, content);
1432
1567
  }
1433
1568
  for (const assetPath of diff.unchanged) {
1434
- const cachedPath = path11.join(cacheDir, assetPath);
1569
+ const cachedPath = path12.join(cacheDir, assetPath);
1435
1570
  try {
1436
- const { readFile: readFile2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1437
- const cached = await readFile2(cachedPath);
1571
+ const cached = await readFile(cachedPath);
1438
1572
  if (cached) downloadedAssets.set(assetPath, cached);
1439
1573
  } catch {
1440
1574
  }
@@ -1493,12 +1627,21 @@ async function sync(options) {
1493
1627
  const configFiles = adapter.emitConfig(resolvedProfile, cwd);
1494
1628
  allEmittedFiles.push(...configFiles);
1495
1629
  }
1630
+ const resolvedPaths = /* @__PURE__ */ new Map();
1496
1631
  for (const emittedFile of allEmittedFiles) {
1497
- const absolutePath = path11.join(cwd, emittedFile.relativePath);
1632
+ const absolutePath = options.global ? resolveGlobalEmitPath(emittedFile.relativePath, emittedFile.adapter) : path12.join(cwd, emittedFile.relativePath);
1633
+ if (absolutePath !== null) {
1634
+ resolvedPaths.set(emittedFile.relativePath, absolutePath);
1635
+ }
1636
+ }
1637
+ for (const emittedFile of allEmittedFiles) {
1638
+ const absolutePath = resolvedPaths.get(emittedFile.relativePath);
1639
+ if (absolutePath === void 0) continue;
1498
1640
  await writeFile(absolutePath, emittedFile.content);
1499
1641
  }
1500
1642
  const lockfileFiles = {};
1501
1643
  for (const emittedFile of allEmittedFiles) {
1644
+ if (!resolvedPaths.has(emittedFile.relativePath)) continue;
1502
1645
  lockfileFiles[emittedFile.relativePath] = createLockEntry(
1503
1646
  emittedFile.sha256,
1504
1647
  emittedFile.sourcePath,
@@ -1515,7 +1658,7 @@ async function sync(options) {
1515
1658
  resolvedProfiles: resolvedProfile.resolvedFrom,
1516
1659
  files: lockfileFiles
1517
1660
  });
1518
- await writeLockfile(cwd, lockfile);
1661
+ await writeLockfile(cwd, lockfile, lockfilePath);
1519
1662
  const stats = {
1520
1663
  filesAdded: diff.added.length,
1521
1664
  filesUpdated: diff.updated.length,
@@ -1547,6 +1690,19 @@ async function sync(options) {
1547
1690
  }
1548
1691
 
1549
1692
  // src/cli/commands/sync.ts
1693
+ init_fs();
1694
+ async function resolveConfigPath(cwd, isGlobal) {
1695
+ const localPath = path13.join(cwd, ".asdm.json");
1696
+ if (await exists(localPath)) return localPath;
1697
+ if (isGlobal) {
1698
+ const globalPath = getGlobalConfigPath();
1699
+ if (await exists(globalPath)) return globalPath;
1700
+ }
1701
+ throw new ConfigError(
1702
+ "No config found.",
1703
+ isGlobal ? "Run `asdm init` (project) or `asdm init --global` (machine-wide setup)." : "Run `asdm init` to initialize this project."
1704
+ );
1705
+ }
1550
1706
  var sync_default = defineCommand2({
1551
1707
  meta: {
1552
1708
  name: "sync",
@@ -1571,6 +1727,11 @@ var sync_default = defineCommand2({
1571
1727
  provider: {
1572
1728
  type: "string",
1573
1729
  description: "Sync only for a specific provider"
1730
+ },
1731
+ global: {
1732
+ type: "boolean",
1733
+ description: "Install to global provider config directories instead of project-local folders",
1734
+ default: false
1574
1735
  }
1575
1736
  },
1576
1737
  async run(ctx) {
@@ -1584,12 +1745,15 @@ var sync_default = defineCommand2({
1584
1745
  logger.asdm("Starting sync\u2026");
1585
1746
  const telemetry = new TelemetryWriter(cwd);
1586
1747
  try {
1748
+ const configPath = await resolveConfigPath(cwd, ctx.args.global ?? false);
1587
1749
  const result = await sync({
1588
1750
  cwd,
1751
+ configPath,
1589
1752
  force: ctx.args.force,
1590
1753
  dryRun,
1591
1754
  verbose,
1592
1755
  provider: ctx.args.provider,
1756
+ global: ctx.args.global ?? false,
1593
1757
  telemetry
1594
1758
  });
1595
1759
  const { stats } = result;
@@ -1626,7 +1790,7 @@ var sync_default = defineCommand2({
1626
1790
  import { defineCommand as defineCommand3 } from "citty";
1627
1791
 
1628
1792
  // src/core/verifier.ts
1629
- import path12 from "path";
1793
+ import path14 from "path";
1630
1794
  init_hash();
1631
1795
  init_fs();
1632
1796
  var VERIFY_EXIT_CODES = {
@@ -1635,8 +1799,9 @@ var VERIFY_EXIT_CODES = {
1635
1799
  NO_LOCK: 2,
1636
1800
  OUTDATED: 3
1637
1801
  };
1638
- async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry) {
1639
- const lockfile = await readLockfile(cwd);
1802
+ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry, lockfilePath) {
1803
+ const lockfile = await readLockfile(cwd, lockfilePath);
1804
+ const isGlobal = lockfilePath !== void 0;
1640
1805
  if (!lockfile) {
1641
1806
  telemetry?.write({ event: "verify.failed" }).catch(() => {
1642
1807
  });
@@ -1650,7 +1815,7 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry)
1650
1815
  let checkedFiles = 0;
1651
1816
  const filesToCheck = onlyManaged ? Object.entries(lockfile.files).filter(([, entry]) => entry.managed) : Object.entries(lockfile.files);
1652
1817
  for (const [relativePath, entry] of filesToCheck) {
1653
- const absolutePath = path12.join(cwd, relativePath);
1818
+ const absolutePath = isGlobal ? resolveGlobalEmitPath(relativePath, entry.adapter) ?? path14.join(cwd, relativePath) : path14.join(cwd, relativePath);
1654
1819
  checkedFiles++;
1655
1820
  const fileExists = await exists(absolutePath);
1656
1821
  if (!fileExists) {
@@ -1694,6 +1859,17 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry)
1694
1859
  }
1695
1860
 
1696
1861
  // src/cli/commands/verify.ts
1862
+ init_fs();
1863
+ async function fetchLatestManifestVersion(cwd) {
1864
+ try {
1865
+ const config = await readProjectConfig(cwd);
1866
+ const client = new RegistryClient(config.registry);
1867
+ const manifest = await client.getLatestManifest();
1868
+ return manifest.version;
1869
+ } catch {
1870
+ return void 0;
1871
+ }
1872
+ }
1697
1873
  var verify_default = defineCommand3({
1698
1874
  meta: {
1699
1875
  name: "verify",
@@ -1714,6 +1890,16 @@ var verify_default = defineCommand3({
1714
1890
  type: "boolean",
1715
1891
  description: "Suppress non-error output",
1716
1892
  default: false
1893
+ },
1894
+ global: {
1895
+ type: "boolean",
1896
+ description: "Verify files installed to global provider config directories",
1897
+ default: false
1898
+ },
1899
+ offline: {
1900
+ type: "boolean",
1901
+ description: "Skip remote manifest version check (exit code 3 will never trigger)",
1902
+ default: false
1717
1903
  }
1718
1904
  },
1719
1905
  async run(ctx) {
@@ -1722,8 +1908,9 @@ var verify_default = defineCommand3({
1722
1908
  const quiet = ctx.args.quiet;
1723
1909
  if (quiet) logger.setQuiet(true);
1724
1910
  const telemetry = new TelemetryWriter(cwd);
1911
+ const lockfilePath = ctx.args.global ? getGlobalLockfilePath() : void 0;
1725
1912
  if (ctx.args.strict) {
1726
- const result = await verify(cwd, void 0, true, telemetry);
1913
+ const result = await verify(cwd, void 0, true, telemetry, lockfilePath);
1727
1914
  if (useJson) {
1728
1915
  console.log(JSON.stringify({
1729
1916
  status: result.exitCode === VERIFY_EXIT_CODES.OK ? "ok" : "error",
@@ -1740,16 +1927,20 @@ var verify_default = defineCommand3({
1740
1927
  process.exitCode = result.exitCode;
1741
1928
  return;
1742
1929
  }
1930
+ let latestManifestVersion;
1931
+ if (!ctx.args.offline && !ctx.args.global) {
1932
+ latestManifestVersion = await fetchLatestManifestVersion(cwd);
1933
+ }
1743
1934
  try {
1744
- const result = await verify(cwd, void 0, true, telemetry);
1935
+ const result = await verify(cwd, latestManifestVersion, true, telemetry, lockfilePath);
1745
1936
  if (useJson) {
1746
1937
  console.log(JSON.stringify(result, null, 2));
1747
1938
  process.exitCode = result.exitCode;
1748
1939
  return;
1749
1940
  }
1750
1941
  if (result.exitCode === VERIFY_EXIT_CODES.NO_LOCK) {
1751
- logger.warn("No lockfile found (.asdm-lock.json)");
1752
- logger.info("Run `asdm sync` to initialize");
1942
+ logger.warn(lockfilePath ? "No global lockfile found" : "No lockfile found (.asdm-lock.json)");
1943
+ logger.info(lockfilePath ? "Run `asdm sync --global` to initialize" : "Run `asdm sync` to initialize");
1753
1944
  process.exitCode = VERIFY_EXIT_CODES.NO_LOCK;
1754
1945
  return;
1755
1946
  }
@@ -2252,7 +2443,7 @@ var version_default = defineCommand10({
2252
2443
  description: "Print CLI version and environment info"
2253
2444
  },
2254
2445
  run(_ctx) {
2255
- console.log(`asdm v${"0.1.2"}`);
2446
+ console.log(`asdm v${"0.3.0"}`);
2256
2447
  console.log(`node ${process.version}`);
2257
2448
  console.log(`os ${os3.type()} ${os3.release()} (${process.platform})`);
2258
2449
  }
@@ -2260,11 +2451,11 @@ var version_default = defineCommand10({
2260
2451
 
2261
2452
  // src/cli/commands/doctor.ts
2262
2453
  import { defineCommand as defineCommand11 } from "citty";
2263
- import path14 from "path";
2454
+ import path16 from "path";
2264
2455
 
2265
2456
  // src/core/overlay.ts
2266
2457
  init_fs();
2267
- import path13 from "path";
2458
+ import path15 from "path";
2268
2459
  import { promises as fs4 } from "fs";
2269
2460
  var OVERLAY_SEPARATOR = [
2270
2461
  "",
@@ -2275,7 +2466,7 @@ var OVERLAY_SEPARATOR = [
2275
2466
  ""
2276
2467
  ].join("\n");
2277
2468
  async function readOverlays(projectRoot) {
2278
- const overlaysDir = path13.join(projectRoot, "overlays");
2469
+ const overlaysDir = path15.join(projectRoot, "overlays");
2279
2470
  const dirExists = await exists(overlaysDir);
2280
2471
  if (!dirExists) return /* @__PURE__ */ new Map();
2281
2472
  const entries = await fs4.readdir(overlaysDir, { withFileTypes: true });
@@ -2283,7 +2474,7 @@ async function readOverlays(projectRoot) {
2283
2474
  for (const entry of entries) {
2284
2475
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2285
2476
  const agentId = entry.name.slice(0, -".md".length);
2286
- const overlayPath = path13.join(overlaysDir, entry.name);
2477
+ const overlayPath = path15.join(overlaysDir, entry.name);
2287
2478
  const content = await readFile(overlayPath);
2288
2479
  if (content === null) continue;
2289
2480
  overlayMap.set(agentId, { agentId, content, path: overlayPath });
@@ -2314,7 +2505,7 @@ var doctor_default = defineCommand11({
2314
2505
  let anyFailed = false;
2315
2506
  let projectConfig = null;
2316
2507
  let registryClient = null;
2317
- const configPath = path14.join(cwd, ".asdm.json");
2508
+ const configPath = path16.join(cwd, ".asdm.json");
2318
2509
  const hasConfig = await exists(configPath);
2319
2510
  checks.push({
2320
2511
  label: ".asdm.json present",
@@ -2379,10 +2570,10 @@ var doctor_default = defineCommand11({
2379
2570
  }
2380
2571
  const managedEntries = Object.entries(lockfile.files).filter(([, e]) => e.managed);
2381
2572
  const missingFiles = [];
2382
- const resolvedCwd = path14.resolve(cwd);
2573
+ const resolvedCwd = path16.resolve(cwd);
2383
2574
  for (const [filePath] of managedEntries) {
2384
- const absPath = path14.resolve(cwd, filePath);
2385
- if (!absPath.startsWith(resolvedCwd + path14.sep) && absPath !== resolvedCwd) {
2575
+ const absPath = path16.resolve(cwd, filePath);
2576
+ if (!absPath.startsWith(resolvedCwd + path16.sep) && absPath !== resolvedCwd) {
2386
2577
  continue;
2387
2578
  }
2388
2579
  const fileExists = await exists(absPath);
@@ -2437,7 +2628,7 @@ var doctor_default = defineCommand11({
2437
2628
  detail: "Skipped \u2014 no lockfile"
2438
2629
  });
2439
2630
  }
2440
- const gitignoreContent = await readFile(path14.join(cwd, ".gitignore"));
2631
+ const gitignoreContent = await readFile(path16.join(cwd, ".gitignore"));
2441
2632
  const hasAsdmBlock = gitignoreContent?.includes(ASDM_MARKER_START) ?? false;
2442
2633
  checks.push({
2443
2634
  label: ".gitignore has ASDM block",
@@ -2491,11 +2682,11 @@ var doctor_default = defineCommand11({
2491
2682
 
2492
2683
  // src/cli/commands/clean.ts
2493
2684
  import { defineCommand as defineCommand12 } from "citty";
2494
- import path15 from "path";
2685
+ import path17 from "path";
2495
2686
  import { promises as fs5 } from "fs";
2496
2687
  import readline from "readline";
2497
2688
  init_fs();
2498
- var LOCKFILE_NAME2 = ".asdm-lock.json";
2689
+ var LOCKFILE_NAME = ".asdm-lock.json";
2499
2690
  async function getFileSizeBytes(filePath) {
2500
2691
  try {
2501
2692
  const stat = await fs5.stat(filePath);
@@ -2533,9 +2724,14 @@ var clean_default = defineCommand12({
2533
2724
  description: "Preview what would be removed without deleting",
2534
2725
  default: false
2535
2726
  },
2727
+ global: {
2728
+ type: "boolean",
2729
+ description: "Clean files installed to global provider config directories",
2730
+ default: false
2731
+ },
2536
2732
  target: {
2537
2733
  type: "string",
2538
- description: "Only clean files for a specific provider (opencode | claude-code | copilot)",
2734
+ description: "Only clean files for a specific provider (opencode | claude-code | copilot | agents-dir)",
2539
2735
  alias: "t"
2540
2736
  }
2541
2737
  },
@@ -2543,119 +2739,226 @@ var clean_default = defineCommand12({
2543
2739
  const cwd = process.cwd();
2544
2740
  const dryRun = ctx.args["dry-run"];
2545
2741
  const target = ctx.args.target;
2742
+ const isGlobal = ctx.args.global ?? false;
2546
2743
  if (dryRun) {
2547
2744
  logger.info("Dry run \u2014 no files will be removed");
2548
2745
  }
2549
- const lockfile = await readLockfile(cwd);
2550
- if (!lockfile) {
2551
- logger.warn("No lockfile found \u2014 nothing to clean");
2746
+ if (isGlobal) {
2747
+ await runGlobalClean(dryRun, target);
2552
2748
  return;
2553
2749
  }
2554
- const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
2555
- if (!entry.managed) return false;
2556
- if (target) return entry.adapter === target;
2557
- return true;
2558
- });
2559
- if (managedEntries.length === 0) {
2560
- if (target) {
2561
- logger.warn(`No managed files found for provider "${target}"`);
2562
- } else {
2563
- logger.warn("No managed files found \u2014 nothing to clean");
2564
- }
2750
+ await runLocalClean(cwd, dryRun, target);
2751
+ }
2752
+ });
2753
+ async function runGlobalClean(dryRun, target) {
2754
+ const globalLockfilePath = getGlobalLockfilePath();
2755
+ const lockfile = await readLockfile(process.cwd(), globalLockfilePath);
2756
+ if (!lockfile) {
2757
+ logger.warn("No global lockfile found \u2014 nothing to clean");
2758
+ return;
2759
+ }
2760
+ const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
2761
+ if (!entry.managed) return false;
2762
+ if (target) return entry.adapter === target;
2763
+ return true;
2764
+ });
2765
+ if (managedEntries.length === 0) {
2766
+ if (target) {
2767
+ logger.warn(`No globally managed files found for provider "${target}"`);
2768
+ } else {
2769
+ logger.warn("No globally managed files found \u2014 nothing to clean");
2770
+ }
2771
+ return;
2772
+ }
2773
+ if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
2774
+ const suffix = target ? ` for provider "${target}"` : "";
2775
+ const confirmed = await confirmPrompt(
2776
+ `About to delete ${managedEntries.length} globally managed file(s)${suffix}. Continue? [y/N] `
2777
+ );
2778
+ if (!confirmed) {
2779
+ logger.info("Aborted \u2014 no files were removed");
2565
2780
  return;
2566
2781
  }
2567
- const managedPaths = managedEntries.map(([filePath]) => filePath);
2568
- const resolvedCwd = path15.resolve(cwd);
2569
- const safePaths = managedPaths.filter((relativePath) => {
2570
- const absPath = path15.resolve(cwd, relativePath);
2571
- return absPath.startsWith(resolvedCwd + path15.sep) || absPath === resolvedCwd;
2572
- });
2573
- const skippedSuspicious = managedPaths.length - safePaths.length;
2574
- if (skippedSuspicious > 0) {
2575
- logger.warn(`Skipping ${skippedSuspicious} path(s) outside project root`);
2576
- }
2577
- if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
2578
- const suffix = target ? ` for provider "${target}"` : "";
2579
- const confirmed = await confirmPrompt(
2580
- `About to delete ${safePaths.length} file(s)${suffix}. Continue? [y/N] `
2782
+ }
2783
+ logger.asdm(`Cleaning ${managedEntries.length} globally managed file(s)\u2026`);
2784
+ logger.divider();
2785
+ let removed = 0;
2786
+ let skippedMissing = 0;
2787
+ let totalBytesFreed = 0;
2788
+ for (const [relativePath, entry] of managedEntries) {
2789
+ const absolutePath = resolveGlobalEmitPath(relativePath, entry.adapter);
2790
+ if (absolutePath === null) {
2791
+ logger.dim(` skip ${relativePath} (project-root file, not applicable in global mode)`);
2792
+ skippedMissing++;
2793
+ continue;
2794
+ }
2795
+ const filePresent = await exists(absolutePath);
2796
+ if (!filePresent) {
2797
+ logger.dim(` skip ${absolutePath} (not found)`);
2798
+ skippedMissing++;
2799
+ continue;
2800
+ }
2801
+ if (dryRun) {
2802
+ logger.bullet(`would remove: ${absolutePath}`);
2803
+ removed++;
2804
+ continue;
2805
+ }
2806
+ const fileSize = await getFileSizeBytes(absolutePath);
2807
+ await removeFile(absolutePath);
2808
+ totalBytesFreed += fileSize;
2809
+ logger.bullet(`removed: ${absolutePath}`);
2810
+ removed++;
2811
+ }
2812
+ const lockfilePresent = await exists(globalLockfilePath);
2813
+ if (target) {
2814
+ if (!dryRun && lockfilePresent) {
2815
+ const updatedFiles = Object.fromEntries(
2816
+ Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
2581
2817
  );
2582
- if (!confirmed) {
2583
- logger.info("Aborted \u2014 no files were removed");
2584
- return;
2818
+ const hasRemainingEntries = Object.keys(updatedFiles).length > 0;
2819
+ if (hasRemainingEntries) {
2820
+ await writeLockfile(process.cwd(), { ...lockfile, files: updatedFiles }, globalLockfilePath);
2821
+ logger.bullet(`updated: global lockfile (removed ${target} entries)`);
2822
+ } else {
2823
+ await removeFile(globalLockfilePath);
2824
+ logger.bullet(`removed: global lockfile (no entries remaining)`);
2585
2825
  }
2826
+ } else if (dryRun) {
2827
+ logger.bullet(`would update: global lockfile (remove ${target} entries)`);
2586
2828
  }
2587
- logger.asdm(`Cleaning ${safePaths.length} managed file(s)\u2026`);
2588
- logger.divider();
2589
- let removed = 0;
2590
- let skippedMissing = 0;
2591
- let totalBytesFreed = 0;
2592
- for (const relativePath of safePaths) {
2593
- const absolutePath = path15.resolve(cwd, relativePath);
2594
- const fileExists = await exists(absolutePath);
2595
- if (!fileExists) {
2596
- logger.dim(` skip ${relativePath} (not found)`);
2597
- skippedMissing++;
2598
- continue;
2599
- }
2829
+ } else {
2830
+ if (lockfilePresent) {
2600
2831
  if (dryRun) {
2601
- logger.bullet(`would remove: ${relativePath}`);
2602
- removed++;
2603
- continue;
2604
- }
2605
- const fileSize = await getFileSizeBytes(absolutePath);
2606
- await removeFile(absolutePath);
2607
- totalBytesFreed += fileSize;
2608
- logger.bullet(`removed: ${relativePath}`);
2609
- removed++;
2832
+ logger.bullet(`would remove: global lockfile`);
2833
+ } else {
2834
+ const lockfileSize = await getFileSizeBytes(globalLockfilePath);
2835
+ await removeFile(globalLockfilePath);
2836
+ totalBytesFreed += lockfileSize;
2837
+ logger.bullet(`removed: global lockfile`);
2838
+ }
2610
2839
  }
2611
- const lockfilePath = path15.join(cwd, LOCKFILE_NAME2);
2612
- const lockfileOnDisk = await exists(lockfilePath);
2840
+ }
2841
+ logger.divider();
2842
+ if (dryRun) {
2843
+ const suffix = target ? ` for provider "${target}"` : "";
2844
+ logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} skipped`);
2845
+ logger.info("Run without --dry-run to actually remove them");
2846
+ } else {
2847
+ const suffix = target ? ` (${target})` : "";
2848
+ logger.success(`Cleaned ${removed} globally managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
2849
+ if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
2850
+ logger.info("Run `asdm sync --global` to reinstall");
2851
+ }
2852
+ }
2853
+ async function runLocalClean(cwd, dryRun, target) {
2854
+ const lockfile = await readLockfile(cwd);
2855
+ if (!lockfile) {
2856
+ logger.warn("No lockfile found \u2014 nothing to clean");
2857
+ return;
2858
+ }
2859
+ const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
2860
+ if (!entry.managed) return false;
2861
+ if (target) return entry.adapter === target;
2862
+ return true;
2863
+ });
2864
+ if (managedEntries.length === 0) {
2613
2865
  if (target) {
2614
- if (!dryRun && lockfileOnDisk) {
2615
- const updatedFiles = Object.fromEntries(
2616
- Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
2617
- );
2618
- await writeLockfile(cwd, { ...lockfile, files: updatedFiles });
2619
- logger.bullet(`updated: ${LOCKFILE_NAME2} (removed ${target} entries)`);
2620
- } else if (dryRun) {
2621
- logger.bullet(`would update: ${LOCKFILE_NAME2} (remove ${target} entries)`);
2622
- }
2866
+ logger.warn(`No managed files found for provider "${target}"`);
2623
2867
  } else {
2624
- if (lockfileOnDisk) {
2625
- if (dryRun) {
2626
- logger.bullet(`would remove: ${LOCKFILE_NAME2}`);
2627
- } else {
2628
- const lockfileSize = await getFileSizeBytes(lockfilePath);
2629
- await removeFile(lockfilePath);
2630
- totalBytesFreed += lockfileSize;
2631
- logger.bullet(`removed: ${LOCKFILE_NAME2}`);
2632
- }
2633
- }
2868
+ logger.warn("No managed files found \u2014 nothing to clean");
2869
+ }
2870
+ return;
2871
+ }
2872
+ const managedPaths = managedEntries.map(([filePath]) => filePath);
2873
+ const resolvedCwd = path17.resolve(cwd);
2874
+ const safePaths = managedPaths.filter((relativePath) => {
2875
+ const absPath = path17.resolve(cwd, relativePath);
2876
+ return absPath.startsWith(resolvedCwd + path17.sep) || absPath === resolvedCwd;
2877
+ });
2878
+ const skippedSuspicious = managedPaths.length - safePaths.length;
2879
+ if (skippedSuspicious > 0) {
2880
+ logger.warn(`Skipping ${skippedSuspicious} path(s) outside project root`);
2881
+ }
2882
+ if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
2883
+ const suffix = target ? ` for provider "${target}"` : "";
2884
+ const confirmed = await confirmPrompt(
2885
+ `About to delete ${safePaths.length} file(s)${suffix}. Continue? [y/N] `
2886
+ );
2887
+ if (!confirmed) {
2888
+ logger.info("Aborted \u2014 no files were removed");
2889
+ return;
2890
+ }
2891
+ }
2892
+ logger.asdm(`Cleaning ${safePaths.length} managed file(s)\u2026`);
2893
+ logger.divider();
2894
+ let removed = 0;
2895
+ let skippedMissing = 0;
2896
+ let totalBytesFreed = 0;
2897
+ for (const relativePath of safePaths) {
2898
+ const absolutePath = path17.resolve(cwd, relativePath);
2899
+ const filePresent = await exists(absolutePath);
2900
+ if (!filePresent) {
2901
+ logger.dim(` skip ${relativePath} (not found)`);
2902
+ skippedMissing++;
2903
+ continue;
2634
2904
  }
2635
- logger.divider();
2636
2905
  if (dryRun) {
2637
- const suffix = target ? ` for provider "${target}"` : "";
2638
- logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} not found`);
2639
- logger.info("Run without --dry-run to actually remove them");
2640
- } else {
2641
- const suffix = target ? ` (${target})` : "";
2642
- logger.success(`Cleaned ${removed} managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
2643
- if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
2644
- logger.info("Run `asdm sync` to reinstall");
2906
+ logger.bullet(`would remove: ${relativePath}`);
2907
+ removed++;
2908
+ continue;
2909
+ }
2910
+ const fileSize = await getFileSizeBytes(absolutePath);
2911
+ await removeFile(absolutePath);
2912
+ totalBytesFreed += fileSize;
2913
+ logger.bullet(`removed: ${relativePath}`);
2914
+ removed++;
2915
+ }
2916
+ const lockfilePath = path17.join(cwd, LOCKFILE_NAME);
2917
+ const lockfileOnDisk = await exists(lockfilePath);
2918
+ if (target) {
2919
+ if (!dryRun && lockfileOnDisk) {
2920
+ const updatedFiles = Object.fromEntries(
2921
+ Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
2922
+ );
2923
+ await writeLockfile(cwd, { ...lockfile, files: updatedFiles });
2924
+ logger.bullet(`updated: ${LOCKFILE_NAME} (removed ${target} entries)`);
2925
+ } else if (dryRun) {
2926
+ logger.bullet(`would update: ${LOCKFILE_NAME} (remove ${target} entries)`);
2927
+ }
2928
+ } else {
2929
+ if (lockfileOnDisk) {
2930
+ if (dryRun) {
2931
+ logger.bullet(`would remove: ${LOCKFILE_NAME}`);
2932
+ } else {
2933
+ const lockfileSize = await getFileSizeBytes(lockfilePath);
2934
+ await removeFile(lockfilePath);
2935
+ totalBytesFreed += lockfileSize;
2936
+ logger.bullet(`removed: ${LOCKFILE_NAME}`);
2937
+ }
2645
2938
  }
2646
2939
  }
2647
- });
2940
+ logger.divider();
2941
+ if (dryRun) {
2942
+ const suffix = target ? ` for provider "${target}"` : "";
2943
+ logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} not found`);
2944
+ logger.info("Run without --dry-run to actually remove them");
2945
+ } else {
2946
+ const suffix = target ? ` (${target})` : "";
2947
+ logger.success(`Cleaned ${removed} managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
2948
+ if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
2949
+ logger.info("Run `asdm sync` to reinstall");
2950
+ }
2951
+ }
2648
2952
 
2649
2953
  // src/cli/commands/hooks.ts
2650
2954
  init_fs();
2651
2955
  import { defineCommand as defineCommand13 } from "citty";
2652
- import path16 from "path";
2956
+ import path19 from "path";
2653
2957
  import { promises as fs6 } from "fs";
2654
2958
 
2655
2959
  // src/utils/post-merge-hook.ts
2656
- function generatePostMergeHook() {
2657
- return `#!/usr/bin/env sh
2658
- # ASDM MANAGED \u2014 post-merge hook
2960
+ function generatePostMergeHookBody() {
2961
+ return `# ASDM MANAGED \u2014 post-merge hook
2659
2962
  if [ -f ".asdm.yaml" ] || [ -f ".asdm.json" ]; then
2660
2963
  echo "\u{1F504} ASDM: syncing after merge..."
2661
2964
  npx asdm sync
@@ -2663,43 +2966,108 @@ fi
2663
2966
  `;
2664
2967
  }
2665
2968
 
2969
+ // src/utils/husky-detect.ts
2970
+ init_fs();
2971
+ import path18 from "path";
2972
+ function parseMajorVersion(versionString) {
2973
+ const stripped = versionString.replace(/^[^0-9]*/, "");
2974
+ const majorPart = stripped.split(".")[0] ?? "";
2975
+ const major = parseInt(majorPart, 10);
2976
+ return isNaN(major) ? null : major;
2977
+ }
2978
+ function majorToHuskyVersion(major) {
2979
+ return major >= 9 ? "v9+" : "v8";
2980
+ }
2981
+ async function detectHusky(cwd) {
2982
+ const huskyDirPath = path18.join(cwd, ".husky");
2983
+ const huskyDirExists = await exists(huskyDirPath);
2984
+ const pkg = await readJson(path18.join(cwd, "package.json"));
2985
+ const huskyVersionString = pkg?.devDependencies?.["husky"] ?? pkg?.dependencies?.["husky"];
2986
+ if (huskyVersionString !== void 0) {
2987
+ const major = parseMajorVersion(huskyVersionString);
2988
+ const version2 = major !== null ? majorToHuskyVersion(major) : "v9+";
2989
+ return {
2990
+ detected: true,
2991
+ version: version2,
2992
+ huskyDir: huskyDirExists ? huskyDirPath : null
2993
+ };
2994
+ }
2995
+ if (!huskyDirExists) {
2996
+ return { detected: false, version: null, huskyDir: null };
2997
+ }
2998
+ const huskyShExists = await exists(path18.join(huskyDirPath, "_", "husky.sh"));
2999
+ const version = huskyShExists ? "v8" : "v9+";
3000
+ return {
3001
+ detected: true,
3002
+ version,
3003
+ huskyDir: huskyDirPath
3004
+ };
3005
+ }
3006
+
2666
3007
  // src/cli/commands/hooks.ts
2667
- var HOOK_DEFINITIONS = {
2668
- "pre-commit": {
2669
- relativePath: ".git/hooks/pre-commit",
2670
- content: `#!/usr/bin/env sh
2671
- # ASDM \u2014 managed pre-commit hook
3008
+ var HOOK_BODIES = {
3009
+ "pre-commit": `# ASDM \u2014 managed pre-commit hook
2672
3010
  # Verifies integrity of managed files before allowing commits.
2673
3011
  npx asdm verify --strict --quiet
2674
3012
  `,
2675
- marker: "ASDM \u2014 managed pre-commit hook",
2676
- description: "runs `asdm verify --strict --quiet` before every commit"
2677
- },
2678
- "post-merge": {
2679
- relativePath: ".git/hooks/post-merge",
2680
- content: generatePostMergeHook(),
2681
- marker: "ASDM MANAGED \u2014 post-merge hook",
2682
- description: "runs `asdm sync` after git pull/merge"
2683
- }
3013
+ "post-merge": generatePostMergeHookBody()
3014
+ };
3015
+ var HOOK_MARKERS = {
3016
+ "pre-commit": "ASDM \u2014 managed pre-commit hook",
3017
+ "post-merge": "ASDM MANAGED \u2014 post-merge hook"
3018
+ };
3019
+ var HOOK_DESCRIPTIONS = {
3020
+ "pre-commit": "runs `asdm verify --strict --quiet` before every commit",
3021
+ "post-merge": "runs `asdm sync` after git pull/merge"
2684
3022
  };
2685
3023
  function resolveHookTypes(hookFlag) {
2686
- if (hookFlag === "pre-commit" || hookFlag === "post-merge") {
2687
- return [hookFlag];
2688
- }
3024
+ if (hookFlag === "pre-commit" || hookFlag === "post-merge") return [hookFlag];
2689
3025
  return ["pre-commit", "post-merge"];
2690
3026
  }
2691
- async function installHook(cwd, hookType) {
2692
- const def = HOOK_DEFINITIONS[hookType];
2693
- const hookPath = path16.join(cwd, def.relativePath);
2694
- const gitDir = path16.join(cwd, ".git");
2695
- const hasGit = await exists(gitDir);
2696
- if (!hasGit) {
2697
- logger.error("No .git directory found", "Run `git init` first");
2698
- process.exit(1);
3027
+ function determineHookMode(huskyInfo, noHusky) {
3028
+ if (noHusky || !huskyInfo.detected) return "git";
3029
+ if (huskyInfo.version === "v8") return "husky-v8";
3030
+ return "husky-v9+";
3031
+ }
3032
+ function buildHookContent(body, mode) {
3033
+ if (mode === "git") return `#!/usr/bin/env sh
3034
+ ${body}`;
3035
+ if (mode === "husky-v8") return `#!/usr/bin/env sh
3036
+ . "$(dirname -- "$0")/_/husky.sh"
3037
+
3038
+ ${body}`;
3039
+ return body;
3040
+ }
3041
+ function resolveHookDefinition(cwd, hookType, huskyInfo, noHusky) {
3042
+ const mode = determineHookMode(huskyInfo, noHusky);
3043
+ const body = HOOK_BODIES[hookType];
3044
+ const marker = HOOK_MARKERS[hookType];
3045
+ const description = HOOK_DESCRIPTIONS[hookType];
3046
+ const relativePath = mode === "git" ? `.git/hooks/${hookType}` : `.husky/${hookType}`;
3047
+ const content = buildHookContent(body, mode);
3048
+ return {
3049
+ absolutePath: path19.join(cwd, relativePath),
3050
+ relativePath,
3051
+ content,
3052
+ marker,
3053
+ description
3054
+ };
3055
+ }
3056
+ async function installHook(cwd, hookType, huskyInfo, noHusky) {
3057
+ const mode = determineHookMode(huskyInfo, noHusky);
3058
+ const def = resolveHookDefinition(cwd, hookType, huskyInfo, noHusky);
3059
+ if (mode === "git") {
3060
+ const hasGit = await exists(path19.join(cwd, ".git"));
3061
+ if (!hasGit) {
3062
+ logger.error("No .git directory found", "Run `git init` first");
3063
+ process.exit(1);
3064
+ }
3065
+ } else {
3066
+ await ensureDir(path19.join(cwd, ".husky"));
2699
3067
  }
2700
- const hookExists = await exists(hookPath);
3068
+ const hookExists = await exists(def.absolutePath);
2701
3069
  if (hookExists) {
2702
- const existing = await fs6.readFile(hookPath, "utf-8");
3070
+ const existing = await fs6.readFile(def.absolutePath, "utf-8");
2703
3071
  if (existing.includes(def.marker)) {
2704
3072
  logger.info(`ASDM ${hookType} hook is already installed`);
2705
3073
  return;
@@ -2708,30 +3076,42 @@ async function installHook(cwd, hookType) {
2708
3076
  logger.warn(`Manual action required: add the ASDM logic to ${def.relativePath}`);
2709
3077
  process.exit(1);
2710
3078
  }
2711
- await writeFile(hookPath, def.content);
3079
+ await writeFile(def.absolutePath, def.content);
2712
3080
  try {
2713
- await fs6.chmod(hookPath, 493);
3081
+ await fs6.chmod(def.absolutePath, 493);
2714
3082
  } catch {
2715
3083
  }
2716
3084
  logger.success(`Installed ${hookType} hook at ${def.relativePath}`);
2717
3085
  logger.info(`The hook ${def.description}`);
2718
3086
  }
2719
3087
  async function uninstallHook(cwd, hookType) {
2720
- const def = HOOK_DEFINITIONS[hookType];
2721
- const hookPath = path16.join(cwd, def.relativePath);
2722
- const hookExists = await exists(hookPath);
2723
- if (!hookExists) {
2724
- logger.info(`No ${hookType} hook found \u2014 nothing to remove`);
2725
- return;
3088
+ const marker = HOOK_MARKERS[hookType];
3089
+ const candidates = [
3090
+ {
3091
+ relativePath: `.git/hooks/${hookType}`,
3092
+ absolutePath: path19.join(cwd, ".git", "hooks", hookType)
3093
+ },
3094
+ {
3095
+ relativePath: `.husky/${hookType}`,
3096
+ absolutePath: path19.join(cwd, ".husky", hookType)
3097
+ }
3098
+ ];
3099
+ let removed = false;
3100
+ for (const { relativePath, absolutePath } of candidates) {
3101
+ const hookExists = await exists(absolutePath);
3102
+ if (!hookExists) continue;
3103
+ const content = await fs6.readFile(absolutePath, "utf-8");
3104
+ if (!content.includes(marker)) {
3105
+ logger.warn(`A ${hookType} hook at ${relativePath} was not installed by ASDM \u2014 skipping`);
3106
+ continue;
3107
+ }
3108
+ await removeFile(absolutePath);
3109
+ logger.success(`Removed ASDM ${hookType} hook from ${relativePath}`);
3110
+ removed = true;
2726
3111
  }
2727
- const content = await fs6.readFile(hookPath, "utf-8");
2728
- if (!content.includes(def.marker)) {
2729
- logger.warn(`The ${hookType} hook was not installed by ASDM \u2014 not removing it`);
2730
- logger.info(`If you want to remove it manually: rm ${def.relativePath}`);
2731
- process.exit(1);
3112
+ if (!removed) {
3113
+ logger.info(`No ASDM-managed ${hookType} hook found \u2014 nothing to remove`);
2732
3114
  }
2733
- await removeFile(hookPath);
2734
- logger.success(`Removed ASDM ${hookType} hook from ${def.relativePath}`);
2735
3115
  }
2736
3116
  var installCommand = defineCommand13({
2737
3117
  meta: {
@@ -2743,13 +3123,25 @@ var installCommand = defineCommand13({
2743
3123
  type: "string",
2744
3124
  description: "Which hook to install: pre-commit | post-merge | all",
2745
3125
  default: "all"
3126
+ },
3127
+ "no-husky": {
3128
+ type: "boolean",
3129
+ description: "Force .git/hooks/ mode even when Husky is detected",
3130
+ default: false
2746
3131
  }
2747
3132
  },
2748
3133
  async run(ctx) {
2749
3134
  const cwd = process.cwd();
2750
3135
  const hookTypes = resolveHookTypes(ctx.args.hook);
3136
+ const noHusky = ctx.args["no-husky"] ?? false;
3137
+ const huskyInfo = noHusky ? { detected: false, version: null, huskyDir: null } : await detectHusky(cwd);
3138
+ if (huskyInfo.detected) {
3139
+ logger.info(`Using Husky hooks in .husky/ (${huskyInfo.version})`);
3140
+ } else {
3141
+ logger.info("Using Git hooks in .git/hooks/");
3142
+ }
2751
3143
  for (const hookType of hookTypes) {
2752
- await installHook(cwd, hookType);
3144
+ await installHook(cwd, hookType, huskyInfo, noHusky);
2753
3145
  }
2754
3146
  }
2755
3147
  });
@@ -2925,9 +3317,204 @@ var telemetry_default = defineCommand15({
2925
3317
  }
2926
3318
  });
2927
3319
 
3320
+ // src/cli/commands/templates.ts
3321
+ init_fs();
3322
+ import { defineCommand as defineCommand16 } from "citty";
3323
+ import path20 from "path";
3324
+ function generateAgentTemplate(name) {
3325
+ return `---
3326
+ name: ${name}
3327
+ type: agent
3328
+ description: "Short description"
3329
+ version: "1.0.0"
3330
+ tags:
3331
+ - tag1
3332
+ - tag2
3333
+ providers:
3334
+ opencode:
3335
+ model: claude-sonnet-4-5
3336
+ permissions: {}
3337
+ tools: []
3338
+ claude-code:
3339
+ model: claude-opus-4-5
3340
+ allowedTools: []
3341
+ copilot:
3342
+ on:
3343
+ push:
3344
+ branches: [main]
3345
+ permissions:
3346
+ contents: read
3347
+ ---
3348
+
3349
+ # ${name}
3350
+
3351
+ ## Role
3352
+ Describe the agent's primary responsibility here.
3353
+
3354
+ ## Instructions
3355
+ - Instruction 1
3356
+ - Instruction 2
3357
+
3358
+ ## Guidelines
3359
+ - Guideline 1
3360
+ - Guideline 2
3361
+ `;
3362
+ }
3363
+ function generateSkillTemplate(name) {
3364
+ return `---
3365
+ name: ${name}
3366
+ type: skill
3367
+ description: "Short description"
3368
+ version: "1.0.0"
3369
+ tags:
3370
+ - tag1
3371
+ ---
3372
+
3373
+ # ${name}
3374
+
3375
+ ## Overview
3376
+ Describe the skill purpose.
3377
+
3378
+ ## Usage
3379
+ How to use this skill.
3380
+
3381
+ ## Examples
3382
+ - Example 1
3383
+ - Example 2
3384
+ `;
3385
+ }
3386
+ function generateCommandTemplate(name) {
3387
+ return `---
3388
+ name: ${name}
3389
+ type: command
3390
+ description: "Short description"
3391
+ version: "1.0.0"
3392
+ tags:
3393
+ - tag1
3394
+ ---
3395
+
3396
+ # /${name}
3397
+
3398
+ ## Description
3399
+ What this command does.
3400
+
3401
+ ## Usage
3402
+ \`/${name} [options]\`
3403
+
3404
+ ## Examples
3405
+ - \`/${name}\` \u2014 basic usage
3406
+ `;
3407
+ }
3408
+ async function writeTemplateFile(outputDir, name, content, force) {
3409
+ const filePath = path20.join(outputDir, `${name}.asdm.md`);
3410
+ const alreadyExists = await exists(filePath);
3411
+ if (alreadyExists && !force) {
3412
+ logger.error(
3413
+ `File already exists: ${filePath}`,
3414
+ "Use --force to overwrite"
3415
+ );
3416
+ process.exitCode = 1;
3417
+ return;
3418
+ }
3419
+ await writeFile(filePath, content);
3420
+ logger.success(`Created ${filePath}`);
3421
+ }
3422
+ var agentCommand = defineCommand16({
3423
+ meta: {
3424
+ name: "agent",
3425
+ description: "Scaffold an agent definition (.asdm.md)"
3426
+ },
3427
+ args: {
3428
+ name: {
3429
+ type: "positional",
3430
+ description: "Agent identifier (used as filename and frontmatter name)",
3431
+ required: true
3432
+ },
3433
+ output: {
3434
+ type: "string",
3435
+ description: "Output directory (default: current working directory)"
3436
+ },
3437
+ force: {
3438
+ type: "boolean",
3439
+ description: "Overwrite existing file",
3440
+ default: false
3441
+ }
3442
+ },
3443
+ async run(ctx) {
3444
+ const cwd = process.cwd();
3445
+ const outputDir = ctx.args.output ?? cwd;
3446
+ await writeTemplateFile(outputDir, ctx.args.name, generateAgentTemplate(ctx.args.name), ctx.args.force);
3447
+ }
3448
+ });
3449
+ var skillCommand = defineCommand16({
3450
+ meta: {
3451
+ name: "skill",
3452
+ description: "Scaffold a skill definition (.asdm.md)"
3453
+ },
3454
+ args: {
3455
+ name: {
3456
+ type: "positional",
3457
+ description: "Skill identifier (used as filename and frontmatter name)",
3458
+ required: true
3459
+ },
3460
+ output: {
3461
+ type: "string",
3462
+ description: "Output directory (default: current working directory)"
3463
+ },
3464
+ force: {
3465
+ type: "boolean",
3466
+ description: "Overwrite existing file",
3467
+ default: false
3468
+ }
3469
+ },
3470
+ async run(ctx) {
3471
+ const cwd = process.cwd();
3472
+ const outputDir = ctx.args.output ?? cwd;
3473
+ await writeTemplateFile(outputDir, ctx.args.name, generateSkillTemplate(ctx.args.name), ctx.args.force);
3474
+ }
3475
+ });
3476
+ var commandCommand = defineCommand16({
3477
+ meta: {
3478
+ name: "command",
3479
+ description: "Scaffold a slash-command definition (.asdm.md)"
3480
+ },
3481
+ args: {
3482
+ name: {
3483
+ type: "positional",
3484
+ description: "Command identifier (used as filename and frontmatter name)",
3485
+ required: true
3486
+ },
3487
+ output: {
3488
+ type: "string",
3489
+ description: "Output directory (default: current working directory)"
3490
+ },
3491
+ force: {
3492
+ type: "boolean",
3493
+ description: "Overwrite existing file",
3494
+ default: false
3495
+ }
3496
+ },
3497
+ async run(ctx) {
3498
+ const cwd = process.cwd();
3499
+ const outputDir = ctx.args.output ?? cwd;
3500
+ await writeTemplateFile(outputDir, ctx.args.name, generateCommandTemplate(ctx.args.name), ctx.args.force);
3501
+ }
3502
+ });
3503
+ var templates_default = defineCommand16({
3504
+ meta: {
3505
+ name: "templates",
3506
+ description: "Scaffold new .asdm.md asset files from built-in templates"
3507
+ },
3508
+ subCommands: {
3509
+ agent: agentCommand,
3510
+ skill: skillCommand,
3511
+ command: commandCommand
3512
+ }
3513
+ });
3514
+
2928
3515
  // src/core/version-check.ts
2929
3516
  init_fs();
2930
- import path17 from "path";
3517
+ import path21 from "path";
2931
3518
  var CACHE_FILENAME = "version-check-cache.json";
2932
3519
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2933
3520
  var FETCH_TIMEOUT_MS2 = 3e3;
@@ -2948,7 +3535,7 @@ function isNewerVersion(current, latest) {
2948
3535
  }
2949
3536
  async function checkForUpdate(currentVersion) {
2950
3537
  const cacheDir = getAsdmConfigDir();
2951
- const cachePath = path17.join(cacheDir, CACHE_FILENAME);
3538
+ const cachePath = path21.join(cacheDir, CACHE_FILENAME);
2952
3539
  const cache = await readJson(cachePath);
2953
3540
  if (cache) {
2954
3541
  const ageMs = Date.now() - new Date(cache.checkedAt).getTime();
@@ -2984,10 +3571,10 @@ async function checkForUpdate(currentVersion) {
2984
3571
  }
2985
3572
 
2986
3573
  // src/cli/index.ts
2987
- var rootCommand = defineCommand16({
3574
+ var rootCommand = defineCommand17({
2988
3575
  meta: {
2989
3576
  name: "asdm",
2990
- version: "0.1.2",
3577
+ version: "0.3.0",
2991
3578
  description: "Agentic Software Delivery Model \u2014 Write Once, Emit Many"
2992
3579
  },
2993
3580
  subCommands: {
@@ -3005,7 +3592,8 @@ var rootCommand = defineCommand16({
3005
3592
  clean: clean_default,
3006
3593
  hooks: hooks_default,
3007
3594
  gitignore: gitignore_default,
3008
- telemetry: telemetry_default
3595
+ telemetry: telemetry_default,
3596
+ templates: templates_default
3009
3597
  }
3010
3598
  });
3011
3599
  function printUpdateBox(currentVersion, latestVersion) {
@@ -3033,9 +3621,9 @@ async function main() {
3033
3621
  await runMain(rootCommand);
3034
3622
  if (process.exitCode !== void 0 && process.exitCode !== 0) return;
3035
3623
  try {
3036
- const latestVersion = await checkForUpdate("0.1.2");
3624
+ const latestVersion = await checkForUpdate("0.3.0");
3037
3625
  if (latestVersion) {
3038
- printUpdateBox("0.1.2", latestVersion);
3626
+ printUpdateBox("0.3.0", latestVersion);
3039
3627
  }
3040
3628
  } catch {
3041
3629
  }