asdm-cli 0.1.3 → 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,35 +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
- getGlobalLockfilePath: () => getGlobalLockfilePath,
21
- listFiles: () => listFiles,
22
- normalizePath: () => normalizePath,
23
- readFile: () => readFile,
24
- readJson: () => readJson,
25
- removeFile: () => removeFile,
26
- resolveGlobalEmitPath: () => resolveGlobalEmitPath,
27
- writeFile: () => writeFile,
28
- writeJson: () => writeJson
29
- });
30
13
  import { promises as fs } from "fs";
31
14
  import path from "path";
32
15
  import os from "os";
33
- function normalizePath(p) {
34
- return p.replace(/\\/g, "/");
35
- }
36
16
  async function ensureDir(dirPath) {
37
17
  await fs.mkdir(dirPath, { recursive: true });
38
18
  }
39
19
  async function writeFile(filePath, content) {
40
20
  await ensureDir(path.dirname(filePath));
41
- 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
+ }
42
26
  }
43
27
  async function readFile(filePath) {
44
28
  try {
@@ -88,11 +72,17 @@ function resolveGlobalEmitPath(relativePath, provider) {
88
72
  if (!prefix || !globalDir) return null;
89
73
  if (!relativePath.startsWith(prefix)) return null;
90
74
  const stripped = relativePath.slice(prefix.length);
91
- return path.join(globalDir, stripped);
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;
92
79
  }
93
80
  function getGlobalLockfilePath() {
94
81
  return path.join(getAsdmConfigDir(), "global-lock.json");
95
82
  }
83
+ function getGlobalConfigPath() {
84
+ return path.join(getAsdmConfigDir(), "config.json");
85
+ }
96
86
  function getAsdmCacheDir() {
97
87
  const xdgCache = process.env["XDG_CACHE_HOME"];
98
88
  if (xdgCache) return path.join(xdgCache, "asdm");
@@ -106,10 +96,6 @@ async function readJson(filePath) {
106
96
  async function writeJson(filePath, data) {
107
97
  await writeFile(filePath, JSON.stringify(data, null, 2));
108
98
  }
109
- async function copyFile(src, dest) {
110
- await ensureDir(path.dirname(dest));
111
- await fs.copyFile(src, dest);
112
- }
113
99
  var PROVIDER_GLOBAL_DIRS, PROVIDER_PATH_PREFIXES;
114
100
  var init_fs = __esm({
115
101
  "src/utils/fs.ts"() {
@@ -117,12 +103,15 @@ var init_fs = __esm({
117
103
  PROVIDER_GLOBAL_DIRS = {
118
104
  opencode: process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "opencode") : path.join(os.homedir(), ".config", "opencode"),
119
105
  "claude-code": process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "Claude") : path.join(os.homedir(), ".claude"),
120
- copilot: process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "GitHub Copilot") : path.join(os.homedir(), ".config", "github-copilot")
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")
121
109
  };
122
110
  PROVIDER_PATH_PREFIXES = {
123
111
  opencode: ".opencode/",
124
112
  "claude-code": ".claude/",
125
- copilot: ".github/"
113
+ copilot: ".github/",
114
+ "agents-dir": ".agents/"
126
115
  };
127
116
  }
128
117
  });
@@ -555,8 +544,79 @@ var init_copilot = __esm({
555
544
  }
556
545
  });
557
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
+
558
618
  // src/cli/index.ts
559
- import { defineCommand as defineCommand16, runMain } from "citty";
619
+ import { defineCommand as defineCommand17, runMain } from "citty";
560
620
 
561
621
  // src/cli/commands/init.ts
562
622
  init_fs();
@@ -703,6 +763,26 @@ function parseRegistryUrl(url) {
703
763
  }
704
764
  return { org: match[1], repo: match[2] };
705
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
+ }
706
786
  async function readProjectConfig(cwd) {
707
787
  const filePath = path3.join(cwd, PROJECT_CONFIG_FILE);
708
788
  const config = await readJson(filePath);
@@ -712,13 +792,7 @@ async function readProjectConfig(cwd) {
712
792
  "Run `asdm init --profile <name>` to initialize"
713
793
  );
714
794
  }
715
- if (!config.registry) {
716
- throw new ConfigError(`Missing required field 'registry' in ${PROJECT_CONFIG_FILE}`);
717
- }
718
- if (!config.profile) {
719
- throw new ConfigError(`Missing required field 'profile' in ${PROJECT_CONFIG_FILE}`);
720
- }
721
- parseRegistryUrl(config.registry);
795
+ validateProjectConfig(config, PROJECT_CONFIG_FILE);
722
796
  return config;
723
797
  }
724
798
  async function readUserConfig(cwd) {
@@ -753,8 +827,7 @@ Run \`asdm profiles\` to see available profiles`
753
827
  policy
754
828
  };
755
829
  }
756
- async function createProjectConfig(cwd, registry, profile, providers = ["opencode"]) {
757
- const filePath = path3.join(cwd, PROJECT_CONFIG_FILE);
830
+ async function createProjectConfigAtPath(filePath, registry, profile, providers = ["opencode"]) {
758
831
  const config = {
759
832
  $schema: "https://asdm.dev/schemas/config.schema.json",
760
833
  registry,
@@ -763,6 +836,10 @@ async function createProjectConfig(cwd, registry, profile, providers = ["opencod
763
836
  };
764
837
  await writeJson(filePath, config);
765
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
+ }
766
843
 
767
844
  // src/core/telemetry.ts
768
845
  init_hash();
@@ -779,7 +856,7 @@ var TelemetryWriter = class {
779
856
  const fullEvent = {
780
857
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
781
858
  machineId: machineId(),
782
- version: "0.1.3",
859
+ version: "0.3.0",
783
860
  ...event
784
861
  };
785
862
  const line = JSON.stringify(fullEvent) + "\n";
@@ -948,7 +1025,12 @@ var init_default = defineCommand({
948
1025
  },
949
1026
  force: {
950
1027
  type: "boolean",
951
- 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",
952
1034
  default: false
953
1035
  },
954
1036
  gitignore: {
@@ -959,15 +1041,36 @@ var init_default = defineCommand({
959
1041
  },
960
1042
  async run(ctx) {
961
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
+ }
962
1068
  const configPath = path5.join(cwd, ".asdm.json");
963
1069
  const alreadyExists = await exists(configPath);
964
1070
  if (alreadyExists && !ctx.args.force) {
965
1071
  logger.warn(".asdm.json already exists. Use --force to overwrite.");
966
1072
  return;
967
1073
  }
968
- const profile = ctx.args.profile || "base";
969
- const registry = ctx.args.registry || DEFAULT_REGISTRY;
970
- const providers = ["opencode"];
971
1074
  try {
972
1075
  await createProjectConfig(cwd, registry, profile, providers);
973
1076
  logger.success(`Initialized .asdm.json with profile "${profile}"`);
@@ -997,9 +1100,10 @@ var init_default = defineCommand({
997
1100
 
998
1101
  // src/cli/commands/sync.ts
999
1102
  import { defineCommand as defineCommand2 } from "citty";
1103
+ import path13 from "path";
1000
1104
 
1001
1105
  // src/core/syncer.ts
1002
- import path11 from "path";
1106
+ import path12 from "path";
1003
1107
 
1004
1108
  // src/core/registry-client.ts
1005
1109
  var GITHUB_API_BASE = "https://api.github.com";
@@ -1268,16 +1372,16 @@ function diffManifest(manifest, localShas, assetPaths) {
1268
1372
  function getProfileAssetPaths(manifest, agentNames, skillNames, commandNames) {
1269
1373
  const paths = [];
1270
1374
  for (const name of agentNames) {
1271
- const path18 = `agents/${name}.asdm.md`;
1272
- if (manifest.assets[path18]) paths.push(path18);
1375
+ const path22 = `agents/${name}.asdm.md`;
1376
+ if (manifest.assets[path22]) paths.push(path22);
1273
1377
  }
1274
1378
  for (const name of skillNames) {
1275
- const path18 = `skills/${name}/SKILL.asdm.md`;
1276
- if (manifest.assets[path18]) paths.push(path18);
1379
+ const path22 = `skills/${name}/SKILL.asdm.md`;
1380
+ if (manifest.assets[path22]) paths.push(path22);
1277
1381
  }
1278
1382
  for (const name of commandNames) {
1279
- const path18 = `commands/${name}.asdm.md`;
1280
- if (manifest.assets[path18]) paths.push(path18);
1383
+ const path22 = `commands/${name}.asdm.md`;
1384
+ if (manifest.assets[path22]) paths.push(path22);
1281
1385
  }
1282
1386
  return paths;
1283
1387
  }
@@ -1397,12 +1501,17 @@ async function loadAdapters(providers) {
1397
1501
  adapters.push(createCopilotAdapter2());
1398
1502
  break;
1399
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
+ }
1400
1509
  }
1401
1510
  }
1402
1511
  return adapters;
1403
1512
  }
1404
1513
  async function getCliVersion() {
1405
- return "0.1.3";
1514
+ return "0.3.0";
1406
1515
  }
1407
1516
  async function sync(options) {
1408
1517
  const startTime = Date.now();
@@ -1410,7 +1519,8 @@ async function sync(options) {
1410
1519
  options.telemetry?.write({ event: "sync.started" }).catch(() => {
1411
1520
  });
1412
1521
  try {
1413
- const projectConfig = await readProjectConfig(cwd);
1522
+ const configFilePath = options.configPath ?? path12.join(cwd, ".asdm.json");
1523
+ const projectConfig = await readProjectConfigFromPath(configFilePath);
1414
1524
  const userConfig = await readUserConfig(cwd);
1415
1525
  const client = new RegistryClient(projectConfig.registry);
1416
1526
  const manifest = await client.getLatestManifest();
@@ -1435,7 +1545,7 @@ async function sync(options) {
1435
1545
  }
1436
1546
  const diff = diffManifest(manifest, localSourceShas, assetPaths);
1437
1547
  const toDownload = options.force ? assetPaths : [...diff.added, ...diff.updated];
1438
- const cacheDir = path11.join(getAsdmCacheDir(), manifest.version);
1548
+ const cacheDir = path12.join(getAsdmCacheDir(), manifest.version);
1439
1549
  await ensureDir(cacheDir);
1440
1550
  const downloadedAssets = /* @__PURE__ */ new Map();
1441
1551
  for (const assetPath of toDownload) {
@@ -1449,17 +1559,16 @@ async function sync(options) {
1449
1559
  "The asset may have been tampered with or the manifest is stale. Run `asdm sync --force`."
1450
1560
  );
1451
1561
  }
1452
- const cachedPath = path11.join(cacheDir, assetPath);
1562
+ const cachedPath = path12.join(cacheDir, assetPath);
1453
1563
  if (!options.dryRun) {
1454
1564
  await writeFile(cachedPath, content);
1455
1565
  }
1456
1566
  downloadedAssets.set(assetPath, content);
1457
1567
  }
1458
1568
  for (const assetPath of diff.unchanged) {
1459
- const cachedPath = path11.join(cacheDir, assetPath);
1569
+ const cachedPath = path12.join(cacheDir, assetPath);
1460
1570
  try {
1461
- const { readFile: readFile2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1462
- const cached = await readFile2(cachedPath);
1571
+ const cached = await readFile(cachedPath);
1463
1572
  if (cached) downloadedAssets.set(assetPath, cached);
1464
1573
  } catch {
1465
1574
  }
@@ -1520,7 +1629,7 @@ async function sync(options) {
1520
1629
  }
1521
1630
  const resolvedPaths = /* @__PURE__ */ new Map();
1522
1631
  for (const emittedFile of allEmittedFiles) {
1523
- const absolutePath = options.global ? resolveGlobalEmitPath(emittedFile.relativePath, emittedFile.adapter) : path11.join(cwd, emittedFile.relativePath);
1632
+ const absolutePath = options.global ? resolveGlobalEmitPath(emittedFile.relativePath, emittedFile.adapter) : path12.join(cwd, emittedFile.relativePath);
1524
1633
  if (absolutePath !== null) {
1525
1634
  resolvedPaths.set(emittedFile.relativePath, absolutePath);
1526
1635
  }
@@ -1581,6 +1690,19 @@ async function sync(options) {
1581
1690
  }
1582
1691
 
1583
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
+ }
1584
1706
  var sync_default = defineCommand2({
1585
1707
  meta: {
1586
1708
  name: "sync",
@@ -1623,8 +1745,10 @@ var sync_default = defineCommand2({
1623
1745
  logger.asdm("Starting sync\u2026");
1624
1746
  const telemetry = new TelemetryWriter(cwd);
1625
1747
  try {
1748
+ const configPath = await resolveConfigPath(cwd, ctx.args.global ?? false);
1626
1749
  const result = await sync({
1627
1750
  cwd,
1751
+ configPath,
1628
1752
  force: ctx.args.force,
1629
1753
  dryRun,
1630
1754
  verbose,
@@ -1666,7 +1790,7 @@ var sync_default = defineCommand2({
1666
1790
  import { defineCommand as defineCommand3 } from "citty";
1667
1791
 
1668
1792
  // src/core/verifier.ts
1669
- import path12 from "path";
1793
+ import path14 from "path";
1670
1794
  init_hash();
1671
1795
  init_fs();
1672
1796
  var VERIFY_EXIT_CODES = {
@@ -1691,7 +1815,7 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry,
1691
1815
  let checkedFiles = 0;
1692
1816
  const filesToCheck = onlyManaged ? Object.entries(lockfile.files).filter(([, entry]) => entry.managed) : Object.entries(lockfile.files);
1693
1817
  for (const [relativePath, entry] of filesToCheck) {
1694
- const absolutePath = isGlobal ? resolveGlobalEmitPath(relativePath, entry.adapter) ?? path12.join(cwd, relativePath) : path12.join(cwd, relativePath);
1818
+ const absolutePath = isGlobal ? resolveGlobalEmitPath(relativePath, entry.adapter) ?? path14.join(cwd, relativePath) : path14.join(cwd, relativePath);
1695
1819
  checkedFiles++;
1696
1820
  const fileExists = await exists(absolutePath);
1697
1821
  if (!fileExists) {
@@ -1736,6 +1860,16 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry,
1736
1860
 
1737
1861
  // src/cli/commands/verify.ts
1738
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
+ }
1739
1873
  var verify_default = defineCommand3({
1740
1874
  meta: {
1741
1875
  name: "verify",
@@ -1761,6 +1895,11 @@ var verify_default = defineCommand3({
1761
1895
  type: "boolean",
1762
1896
  description: "Verify files installed to global provider config directories",
1763
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
1764
1903
  }
1765
1904
  },
1766
1905
  async run(ctx) {
@@ -1788,8 +1927,12 @@ var verify_default = defineCommand3({
1788
1927
  process.exitCode = result.exitCode;
1789
1928
  return;
1790
1929
  }
1930
+ let latestManifestVersion;
1931
+ if (!ctx.args.offline && !ctx.args.global) {
1932
+ latestManifestVersion = await fetchLatestManifestVersion(cwd);
1933
+ }
1791
1934
  try {
1792
- const result = await verify(cwd, void 0, true, telemetry, lockfilePath);
1935
+ const result = await verify(cwd, latestManifestVersion, true, telemetry, lockfilePath);
1793
1936
  if (useJson) {
1794
1937
  console.log(JSON.stringify(result, null, 2));
1795
1938
  process.exitCode = result.exitCode;
@@ -2300,7 +2443,7 @@ var version_default = defineCommand10({
2300
2443
  description: "Print CLI version and environment info"
2301
2444
  },
2302
2445
  run(_ctx) {
2303
- console.log(`asdm v${"0.1.3"}`);
2446
+ console.log(`asdm v${"0.3.0"}`);
2304
2447
  console.log(`node ${process.version}`);
2305
2448
  console.log(`os ${os3.type()} ${os3.release()} (${process.platform})`);
2306
2449
  }
@@ -2308,11 +2451,11 @@ var version_default = defineCommand10({
2308
2451
 
2309
2452
  // src/cli/commands/doctor.ts
2310
2453
  import { defineCommand as defineCommand11 } from "citty";
2311
- import path14 from "path";
2454
+ import path16 from "path";
2312
2455
 
2313
2456
  // src/core/overlay.ts
2314
2457
  init_fs();
2315
- import path13 from "path";
2458
+ import path15 from "path";
2316
2459
  import { promises as fs4 } from "fs";
2317
2460
  var OVERLAY_SEPARATOR = [
2318
2461
  "",
@@ -2323,7 +2466,7 @@ var OVERLAY_SEPARATOR = [
2323
2466
  ""
2324
2467
  ].join("\n");
2325
2468
  async function readOverlays(projectRoot) {
2326
- const overlaysDir = path13.join(projectRoot, "overlays");
2469
+ const overlaysDir = path15.join(projectRoot, "overlays");
2327
2470
  const dirExists = await exists(overlaysDir);
2328
2471
  if (!dirExists) return /* @__PURE__ */ new Map();
2329
2472
  const entries = await fs4.readdir(overlaysDir, { withFileTypes: true });
@@ -2331,7 +2474,7 @@ async function readOverlays(projectRoot) {
2331
2474
  for (const entry of entries) {
2332
2475
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2333
2476
  const agentId = entry.name.slice(0, -".md".length);
2334
- const overlayPath = path13.join(overlaysDir, entry.name);
2477
+ const overlayPath = path15.join(overlaysDir, entry.name);
2335
2478
  const content = await readFile(overlayPath);
2336
2479
  if (content === null) continue;
2337
2480
  overlayMap.set(agentId, { agentId, content, path: overlayPath });
@@ -2362,7 +2505,7 @@ var doctor_default = defineCommand11({
2362
2505
  let anyFailed = false;
2363
2506
  let projectConfig = null;
2364
2507
  let registryClient = null;
2365
- const configPath = path14.join(cwd, ".asdm.json");
2508
+ const configPath = path16.join(cwd, ".asdm.json");
2366
2509
  const hasConfig = await exists(configPath);
2367
2510
  checks.push({
2368
2511
  label: ".asdm.json present",
@@ -2427,10 +2570,10 @@ var doctor_default = defineCommand11({
2427
2570
  }
2428
2571
  const managedEntries = Object.entries(lockfile.files).filter(([, e]) => e.managed);
2429
2572
  const missingFiles = [];
2430
- const resolvedCwd = path14.resolve(cwd);
2573
+ const resolvedCwd = path16.resolve(cwd);
2431
2574
  for (const [filePath] of managedEntries) {
2432
- const absPath = path14.resolve(cwd, filePath);
2433
- if (!absPath.startsWith(resolvedCwd + path14.sep) && absPath !== resolvedCwd) {
2575
+ const absPath = path16.resolve(cwd, filePath);
2576
+ if (!absPath.startsWith(resolvedCwd + path16.sep) && absPath !== resolvedCwd) {
2434
2577
  continue;
2435
2578
  }
2436
2579
  const fileExists = await exists(absPath);
@@ -2485,7 +2628,7 @@ var doctor_default = defineCommand11({
2485
2628
  detail: "Skipped \u2014 no lockfile"
2486
2629
  });
2487
2630
  }
2488
- const gitignoreContent = await readFile(path14.join(cwd, ".gitignore"));
2631
+ const gitignoreContent = await readFile(path16.join(cwd, ".gitignore"));
2489
2632
  const hasAsdmBlock = gitignoreContent?.includes(ASDM_MARKER_START) ?? false;
2490
2633
  checks.push({
2491
2634
  label: ".gitignore has ASDM block",
@@ -2539,7 +2682,7 @@ var doctor_default = defineCommand11({
2539
2682
 
2540
2683
  // src/cli/commands/clean.ts
2541
2684
  import { defineCommand as defineCommand12 } from "citty";
2542
- import path15 from "path";
2685
+ import path17 from "path";
2543
2686
  import { promises as fs5 } from "fs";
2544
2687
  import readline from "readline";
2545
2688
  init_fs();
@@ -2581,9 +2724,14 @@ var clean_default = defineCommand12({
2581
2724
  description: "Preview what would be removed without deleting",
2582
2725
  default: false
2583
2726
  },
2727
+ global: {
2728
+ type: "boolean",
2729
+ description: "Clean files installed to global provider config directories",
2730
+ default: false
2731
+ },
2584
2732
  target: {
2585
2733
  type: "string",
2586
- 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)",
2587
2735
  alias: "t"
2588
2736
  }
2589
2737
  },
@@ -2591,119 +2739,226 @@ var clean_default = defineCommand12({
2591
2739
  const cwd = process.cwd();
2592
2740
  const dryRun = ctx.args["dry-run"];
2593
2741
  const target = ctx.args.target;
2742
+ const isGlobal = ctx.args.global ?? false;
2594
2743
  if (dryRun) {
2595
2744
  logger.info("Dry run \u2014 no files will be removed");
2596
2745
  }
2597
- const lockfile = await readLockfile(cwd);
2598
- if (!lockfile) {
2599
- logger.warn("No lockfile found \u2014 nothing to clean");
2746
+ if (isGlobal) {
2747
+ await runGlobalClean(dryRun, target);
2600
2748
  return;
2601
2749
  }
2602
- const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
2603
- if (!entry.managed) return false;
2604
- if (target) return entry.adapter === target;
2605
- return true;
2606
- });
2607
- if (managedEntries.length === 0) {
2608
- if (target) {
2609
- logger.warn(`No managed files found for provider "${target}"`);
2610
- } else {
2611
- logger.warn("No managed files found \u2014 nothing to clean");
2612
- }
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");
2613
2780
  return;
2614
2781
  }
2615
- const managedPaths = managedEntries.map(([filePath]) => filePath);
2616
- const resolvedCwd = path15.resolve(cwd);
2617
- const safePaths = managedPaths.filter((relativePath) => {
2618
- const absPath = path15.resolve(cwd, relativePath);
2619
- return absPath.startsWith(resolvedCwd + path15.sep) || absPath === resolvedCwd;
2620
- });
2621
- const skippedSuspicious = managedPaths.length - safePaths.length;
2622
- if (skippedSuspicious > 0) {
2623
- logger.warn(`Skipping ${skippedSuspicious} path(s) outside project root`);
2624
- }
2625
- if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
2626
- const suffix = target ? ` for provider "${target}"` : "";
2627
- const confirmed = await confirmPrompt(
2628
- `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)
2629
2817
  );
2630
- if (!confirmed) {
2631
- logger.info("Aborted \u2014 no files were removed");
2632
- 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)`);
2633
2825
  }
2826
+ } else if (dryRun) {
2827
+ logger.bullet(`would update: global lockfile (remove ${target} entries)`);
2634
2828
  }
2635
- logger.asdm(`Cleaning ${safePaths.length} managed file(s)\u2026`);
2636
- logger.divider();
2637
- let removed = 0;
2638
- let skippedMissing = 0;
2639
- let totalBytesFreed = 0;
2640
- for (const relativePath of safePaths) {
2641
- const absolutePath = path15.resolve(cwd, relativePath);
2642
- const fileExists = await exists(absolutePath);
2643
- if (!fileExists) {
2644
- logger.dim(` skip ${relativePath} (not found)`);
2645
- skippedMissing++;
2646
- continue;
2647
- }
2829
+ } else {
2830
+ if (lockfilePresent) {
2648
2831
  if (dryRun) {
2649
- logger.bullet(`would remove: ${relativePath}`);
2650
- removed++;
2651
- continue;
2652
- }
2653
- const fileSize = await getFileSizeBytes(absolutePath);
2654
- await removeFile(absolutePath);
2655
- totalBytesFreed += fileSize;
2656
- logger.bullet(`removed: ${relativePath}`);
2657
- 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
+ }
2658
2839
  }
2659
- const lockfilePath = path15.join(cwd, LOCKFILE_NAME);
2660
- 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) {
2661
2865
  if (target) {
2662
- if (!dryRun && lockfileOnDisk) {
2663
- const updatedFiles = Object.fromEntries(
2664
- Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
2665
- );
2666
- await writeLockfile(cwd, { ...lockfile, files: updatedFiles });
2667
- logger.bullet(`updated: ${LOCKFILE_NAME} (removed ${target} entries)`);
2668
- } else if (dryRun) {
2669
- logger.bullet(`would update: ${LOCKFILE_NAME} (remove ${target} entries)`);
2670
- }
2866
+ logger.warn(`No managed files found for provider "${target}"`);
2671
2867
  } else {
2672
- if (lockfileOnDisk) {
2673
- if (dryRun) {
2674
- logger.bullet(`would remove: ${LOCKFILE_NAME}`);
2675
- } else {
2676
- const lockfileSize = await getFileSizeBytes(lockfilePath);
2677
- await removeFile(lockfilePath);
2678
- totalBytesFreed += lockfileSize;
2679
- logger.bullet(`removed: ${LOCKFILE_NAME}`);
2680
- }
2681
- }
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;
2682
2904
  }
2683
- logger.divider();
2684
2905
  if (dryRun) {
2685
- const suffix = target ? ` for provider "${target}"` : "";
2686
- logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} not found`);
2687
- logger.info("Run without --dry-run to actually remove them");
2688
- } else {
2689
- const suffix = target ? ` (${target})` : "";
2690
- logger.success(`Cleaned ${removed} managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
2691
- if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
2692
- 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
+ }
2693
2938
  }
2694
2939
  }
2695
- });
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
+ }
2696
2952
 
2697
2953
  // src/cli/commands/hooks.ts
2698
2954
  init_fs();
2699
2955
  import { defineCommand as defineCommand13 } from "citty";
2700
- import path16 from "path";
2956
+ import path19 from "path";
2701
2957
  import { promises as fs6 } from "fs";
2702
2958
 
2703
2959
  // src/utils/post-merge-hook.ts
2704
- function generatePostMergeHook() {
2705
- return `#!/usr/bin/env sh
2706
- # ASDM MANAGED \u2014 post-merge hook
2960
+ function generatePostMergeHookBody() {
2961
+ return `# ASDM MANAGED \u2014 post-merge hook
2707
2962
  if [ -f ".asdm.yaml" ] || [ -f ".asdm.json" ]; then
2708
2963
  echo "\u{1F504} ASDM: syncing after merge..."
2709
2964
  npx asdm sync
@@ -2711,43 +2966,108 @@ fi
2711
2966
  `;
2712
2967
  }
2713
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
+
2714
3007
  // src/cli/commands/hooks.ts
2715
- var HOOK_DEFINITIONS = {
2716
- "pre-commit": {
2717
- relativePath: ".git/hooks/pre-commit",
2718
- content: `#!/usr/bin/env sh
2719
- # ASDM \u2014 managed pre-commit hook
3008
+ var HOOK_BODIES = {
3009
+ "pre-commit": `# ASDM \u2014 managed pre-commit hook
2720
3010
  # Verifies integrity of managed files before allowing commits.
2721
3011
  npx asdm verify --strict --quiet
2722
3012
  `,
2723
- marker: "ASDM \u2014 managed pre-commit hook",
2724
- description: "runs `asdm verify --strict --quiet` before every commit"
2725
- },
2726
- "post-merge": {
2727
- relativePath: ".git/hooks/post-merge",
2728
- content: generatePostMergeHook(),
2729
- marker: "ASDM MANAGED \u2014 post-merge hook",
2730
- description: "runs `asdm sync` after git pull/merge"
2731
- }
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"
2732
3022
  };
2733
3023
  function resolveHookTypes(hookFlag) {
2734
- if (hookFlag === "pre-commit" || hookFlag === "post-merge") {
2735
- return [hookFlag];
2736
- }
3024
+ if (hookFlag === "pre-commit" || hookFlag === "post-merge") return [hookFlag];
2737
3025
  return ["pre-commit", "post-merge"];
2738
3026
  }
2739
- async function installHook(cwd, hookType) {
2740
- const def = HOOK_DEFINITIONS[hookType];
2741
- const hookPath = path16.join(cwd, def.relativePath);
2742
- const gitDir = path16.join(cwd, ".git");
2743
- const hasGit = await exists(gitDir);
2744
- if (!hasGit) {
2745
- logger.error("No .git directory found", "Run `git init` first");
2746
- 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"));
2747
3067
  }
2748
- const hookExists = await exists(hookPath);
3068
+ const hookExists = await exists(def.absolutePath);
2749
3069
  if (hookExists) {
2750
- const existing = await fs6.readFile(hookPath, "utf-8");
3070
+ const existing = await fs6.readFile(def.absolutePath, "utf-8");
2751
3071
  if (existing.includes(def.marker)) {
2752
3072
  logger.info(`ASDM ${hookType} hook is already installed`);
2753
3073
  return;
@@ -2756,30 +3076,42 @@ async function installHook(cwd, hookType) {
2756
3076
  logger.warn(`Manual action required: add the ASDM logic to ${def.relativePath}`);
2757
3077
  process.exit(1);
2758
3078
  }
2759
- await writeFile(hookPath, def.content);
3079
+ await writeFile(def.absolutePath, def.content);
2760
3080
  try {
2761
- await fs6.chmod(hookPath, 493);
3081
+ await fs6.chmod(def.absolutePath, 493);
2762
3082
  } catch {
2763
3083
  }
2764
3084
  logger.success(`Installed ${hookType} hook at ${def.relativePath}`);
2765
3085
  logger.info(`The hook ${def.description}`);
2766
3086
  }
2767
3087
  async function uninstallHook(cwd, hookType) {
2768
- const def = HOOK_DEFINITIONS[hookType];
2769
- const hookPath = path16.join(cwd, def.relativePath);
2770
- const hookExists = await exists(hookPath);
2771
- if (!hookExists) {
2772
- logger.info(`No ${hookType} hook found \u2014 nothing to remove`);
2773
- 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;
2774
3111
  }
2775
- const content = await fs6.readFile(hookPath, "utf-8");
2776
- if (!content.includes(def.marker)) {
2777
- logger.warn(`The ${hookType} hook was not installed by ASDM \u2014 not removing it`);
2778
- logger.info(`If you want to remove it manually: rm ${def.relativePath}`);
2779
- process.exit(1);
3112
+ if (!removed) {
3113
+ logger.info(`No ASDM-managed ${hookType} hook found \u2014 nothing to remove`);
2780
3114
  }
2781
- await removeFile(hookPath);
2782
- logger.success(`Removed ASDM ${hookType} hook from ${def.relativePath}`);
2783
3115
  }
2784
3116
  var installCommand = defineCommand13({
2785
3117
  meta: {
@@ -2791,13 +3123,25 @@ var installCommand = defineCommand13({
2791
3123
  type: "string",
2792
3124
  description: "Which hook to install: pre-commit | post-merge | all",
2793
3125
  default: "all"
3126
+ },
3127
+ "no-husky": {
3128
+ type: "boolean",
3129
+ description: "Force .git/hooks/ mode even when Husky is detected",
3130
+ default: false
2794
3131
  }
2795
3132
  },
2796
3133
  async run(ctx) {
2797
3134
  const cwd = process.cwd();
2798
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
+ }
2799
3143
  for (const hookType of hookTypes) {
2800
- await installHook(cwd, hookType);
3144
+ await installHook(cwd, hookType, huskyInfo, noHusky);
2801
3145
  }
2802
3146
  }
2803
3147
  });
@@ -2973,9 +3317,204 @@ var telemetry_default = defineCommand15({
2973
3317
  }
2974
3318
  });
2975
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
+
2976
3515
  // src/core/version-check.ts
2977
3516
  init_fs();
2978
- import path17 from "path";
3517
+ import path21 from "path";
2979
3518
  var CACHE_FILENAME = "version-check-cache.json";
2980
3519
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2981
3520
  var FETCH_TIMEOUT_MS2 = 3e3;
@@ -2996,7 +3535,7 @@ function isNewerVersion(current, latest) {
2996
3535
  }
2997
3536
  async function checkForUpdate(currentVersion) {
2998
3537
  const cacheDir = getAsdmConfigDir();
2999
- const cachePath = path17.join(cacheDir, CACHE_FILENAME);
3538
+ const cachePath = path21.join(cacheDir, CACHE_FILENAME);
3000
3539
  const cache = await readJson(cachePath);
3001
3540
  if (cache) {
3002
3541
  const ageMs = Date.now() - new Date(cache.checkedAt).getTime();
@@ -3032,10 +3571,10 @@ async function checkForUpdate(currentVersion) {
3032
3571
  }
3033
3572
 
3034
3573
  // src/cli/index.ts
3035
- var rootCommand = defineCommand16({
3574
+ var rootCommand = defineCommand17({
3036
3575
  meta: {
3037
3576
  name: "asdm",
3038
- version: "0.1.3",
3577
+ version: "0.3.0",
3039
3578
  description: "Agentic Software Delivery Model \u2014 Write Once, Emit Many"
3040
3579
  },
3041
3580
  subCommands: {
@@ -3053,7 +3592,8 @@ var rootCommand = defineCommand16({
3053
3592
  clean: clean_default,
3054
3593
  hooks: hooks_default,
3055
3594
  gitignore: gitignore_default,
3056
- telemetry: telemetry_default
3595
+ telemetry: telemetry_default,
3596
+ templates: templates_default
3057
3597
  }
3058
3598
  });
3059
3599
  function printUpdateBox(currentVersion, latestVersion) {
@@ -3081,9 +3621,9 @@ async function main() {
3081
3621
  await runMain(rootCommand);
3082
3622
  if (process.exitCode !== void 0 && process.exitCode !== 0) return;
3083
3623
  try {
3084
- const latestVersion = await checkForUpdate("0.1.3");
3624
+ const latestVersion = await checkForUpdate("0.3.0");
3085
3625
  if (latestVersion) {
3086
- printUpdateBox("0.1.3", latestVersion);
3626
+ printUpdateBox("0.3.0", latestVersion);
3087
3627
  }
3088
3628
  } catch {
3089
3629
  }