asdm-cli 0.1.3 → 0.4.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.4.0",
783
860
  ...event
784
861
  };
785
862
  const line = JSON.stringify(fullEvent) + "\n";
@@ -927,8 +1004,77 @@ var logger = {
927
1004
  }
928
1005
  };
929
1006
 
1007
+ // src/utils/prompt.ts
1008
+ import readline from "readline";
1009
+ var RESET2 = "\x1B[0m";
1010
+ var BOLD2 = "\x1B[1m";
1011
+ var DIM2 = "\x1B[2m";
1012
+ var FG_CYAN2 = "\x1B[36m";
1013
+ function renderOptions(options) {
1014
+ for (let i = 0; i < options.length; i++) {
1015
+ console.log(` ${DIM2}${i + 1}.${RESET2} ${options[i].label}`);
1016
+ }
1017
+ }
1018
+ function ask(rl, prompt) {
1019
+ return new Promise((resolve) => {
1020
+ rl.question(prompt, (answer) => resolve(answer));
1021
+ });
1022
+ }
1023
+ async function selectOne(question, options) {
1024
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1025
+ return void 0;
1026
+ }
1027
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1028
+ console.log(`
1029
+ ${BOLD2}${FG_CYAN2}${question}${RESET2} ${DIM2}(enter number)${RESET2}`);
1030
+ renderOptions(options);
1031
+ while (true) {
1032
+ const answer = await ask(rl, "\u276F ");
1033
+ const num = parseInt(answer.trim(), 10);
1034
+ if (!isNaN(num) && num >= 1 && num <= options.length) {
1035
+ rl.close();
1036
+ return options[num - 1].value;
1037
+ }
1038
+ console.log(` ${DIM2}Enter a number between 1 and ${options.length}.${RESET2}`);
1039
+ }
1040
+ }
1041
+ async function selectMany(question, options) {
1042
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1043
+ return [];
1044
+ }
1045
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1046
+ console.log(`
1047
+ ${BOLD2}${FG_CYAN2}${question}${RESET2} ${DIM2}(comma-separated numbers, e.g: 1,2)${RESET2}`);
1048
+ renderOptions(options);
1049
+ while (true) {
1050
+ const answer = await ask(rl, "\u276F ");
1051
+ const parts = answer.split(",").map((s) => s.trim()).filter(Boolean);
1052
+ const nums = parts.map((p) => parseInt(p, 10));
1053
+ const allValid = nums.length > 0 && nums.every((n) => !isNaN(n) && n >= 1 && n <= options.length);
1054
+ if (allValid) {
1055
+ rl.close();
1056
+ return nums.map((n) => options[n - 1].value);
1057
+ }
1058
+ console.log(` ${DIM2}Select at least one. Enter numbers between 1 and ${options.length}.${RESET2}`);
1059
+ }
1060
+ }
1061
+
930
1062
  // src/cli/commands/init.ts
931
1063
  var DEFAULT_REGISTRY = "github://lennonalvesdias/asdm";
1064
+ var DEFAULT_PROVIDERS = ["opencode"];
1065
+ var PROVIDER_OPTIONS = [
1066
+ { label: "opencode \u2014 OpenCode IDE integration (.opencode/)", value: "opencode" },
1067
+ { label: "claude-code \u2014 Claude Code IDE integration (.claude/)", value: "claude-code" },
1068
+ { label: "copilot \u2014 GitHub Copilot integration (.github/)", value: "copilot" },
1069
+ { label: "agents-dir \u2014 Cross-provider agents directory (.agents/)", value: "agents-dir" }
1070
+ ];
1071
+ var PROFILE_OPTIONS = [
1072
+ { label: "base \u2014 Base configuration with common agents and skills", value: "base" },
1073
+ { label: "data-analytics \u2014 Data analysis, SQL, pandas, and reporting", value: "data-analytics" },
1074
+ { label: "fullstack-engineer \u2014 Full-stack web development", value: "fullstack-engineer" },
1075
+ { label: "mobile \u2014 Mobile development (iOS + Android)", value: "mobile" },
1076
+ { label: "security \u2014 Security auditing and threat modeling", value: "security" }
1077
+ ];
932
1078
  var init_default = defineCommand({
933
1079
  meta: {
934
1080
  name: "init",
@@ -948,7 +1094,12 @@ var init_default = defineCommand({
948
1094
  },
949
1095
  force: {
950
1096
  type: "boolean",
951
- description: "Overwrite existing .asdm.json",
1097
+ description: "Overwrite existing config",
1098
+ default: false
1099
+ },
1100
+ global: {
1101
+ type: "boolean",
1102
+ description: "Write config to ~/.config/asdm/config.json instead of .asdm.json",
952
1103
  default: false
953
1104
  },
954
1105
  gitignore: {
@@ -959,15 +1110,49 @@ var init_default = defineCommand({
959
1110
  },
960
1111
  async run(ctx) {
961
1112
  const cwd = process.cwd();
1113
+ const registry = ctx.args.registry || DEFAULT_REGISTRY;
1114
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
1115
+ let profile;
1116
+ let providers;
1117
+ if (isTTY) {
1118
+ const selectedProviders = await selectMany("Select providers", PROVIDER_OPTIONS);
1119
+ providers = selectedProviders.length > 0 ? selectedProviders : DEFAULT_PROVIDERS;
1120
+ const selectedProfile = await selectOne("Select profile", PROFILE_OPTIONS);
1121
+ profile = selectedProfile ?? "base";
1122
+ console.log("");
1123
+ logger.info(` Profile: ${profile}`);
1124
+ logger.info(` Providers: ${providers.join(", ")}`);
1125
+ } else {
1126
+ profile = ctx.args.profile || "base";
1127
+ providers = DEFAULT_PROVIDERS;
1128
+ }
1129
+ if (ctx.args.global) {
1130
+ const targetPath = getGlobalConfigPath();
1131
+ const alreadyExists2 = await exists(targetPath);
1132
+ if (alreadyExists2 && !ctx.args.force) {
1133
+ logger.warn(`Global config already exists at ${targetPath}. Use --force to overwrite.`);
1134
+ return;
1135
+ }
1136
+ try {
1137
+ await ensureDir(path5.dirname(targetPath));
1138
+ await createProjectConfigAtPath(targetPath, registry, profile, providers);
1139
+ logger.success(`Global config written to ${targetPath}`);
1140
+ logger.info(`Registry: ${registry}`);
1141
+ logger.info("Next step: run `asdm sync --global` to install agents, skills, and commands");
1142
+ } catch (err) {
1143
+ const message = err instanceof Error ? err.message : String(err);
1144
+ logger.error(message);
1145
+ process.exitCode = 1;
1146
+ return;
1147
+ }
1148
+ return;
1149
+ }
962
1150
  const configPath = path5.join(cwd, ".asdm.json");
963
1151
  const alreadyExists = await exists(configPath);
964
1152
  if (alreadyExists && !ctx.args.force) {
965
1153
  logger.warn(".asdm.json already exists. Use --force to overwrite.");
966
1154
  return;
967
1155
  }
968
- const profile = ctx.args.profile || "base";
969
- const registry = ctx.args.registry || DEFAULT_REGISTRY;
970
- const providers = ["opencode"];
971
1156
  try {
972
1157
  await createProjectConfig(cwd, registry, profile, providers);
973
1158
  logger.success(`Initialized .asdm.json with profile "${profile}"`);
@@ -997,9 +1182,10 @@ var init_default = defineCommand({
997
1182
 
998
1183
  // src/cli/commands/sync.ts
999
1184
  import { defineCommand as defineCommand2 } from "citty";
1185
+ import path13 from "path";
1000
1186
 
1001
1187
  // src/core/syncer.ts
1002
- import path11 from "path";
1188
+ import path12 from "path";
1003
1189
 
1004
1190
  // src/core/registry-client.ts
1005
1191
  var GITHUB_API_BASE = "https://api.github.com";
@@ -1268,16 +1454,16 @@ function diffManifest(manifest, localShas, assetPaths) {
1268
1454
  function getProfileAssetPaths(manifest, agentNames, skillNames, commandNames) {
1269
1455
  const paths = [];
1270
1456
  for (const name of agentNames) {
1271
- const path18 = `agents/${name}.asdm.md`;
1272
- if (manifest.assets[path18]) paths.push(path18);
1457
+ const path22 = `agents/${name}.asdm.md`;
1458
+ if (manifest.assets[path22]) paths.push(path22);
1273
1459
  }
1274
1460
  for (const name of skillNames) {
1275
- const path18 = `skills/${name}/SKILL.asdm.md`;
1276
- if (manifest.assets[path18]) paths.push(path18);
1461
+ const path22 = `skills/${name}/SKILL.asdm.md`;
1462
+ if (manifest.assets[path22]) paths.push(path22);
1277
1463
  }
1278
1464
  for (const name of commandNames) {
1279
- const path18 = `commands/${name}.asdm.md`;
1280
- if (manifest.assets[path18]) paths.push(path18);
1465
+ const path22 = `commands/${name}.asdm.md`;
1466
+ if (manifest.assets[path22]) paths.push(path22);
1281
1467
  }
1282
1468
  return paths;
1283
1469
  }
@@ -1378,6 +1564,28 @@ function parseAsset(content, sourcePath, provider = "opencode") {
1378
1564
  }
1379
1565
 
1380
1566
  // src/core/syncer.ts
1567
+ var ADAPTER_SCAN_DIRS = {
1568
+ "opencode": [".opencode/agents", ".opencode/skills", ".opencode/commands"],
1569
+ "claude-code": [".claude/agents", ".claude/skills", ".claude/commands"],
1570
+ "copilot": [".github/agents", ".github/skills"],
1571
+ "agents-dir": [".agents"]
1572
+ };
1573
+ async function findManagedFilesInDir(dir) {
1574
+ let allFiles;
1575
+ try {
1576
+ allFiles = await listFiles(dir);
1577
+ } catch {
1578
+ return [];
1579
+ }
1580
+ const result = [];
1581
+ for (const filePath of allFiles) {
1582
+ const content = await readFile(filePath);
1583
+ if (content?.includes("ASDM MANAGED FILE")) {
1584
+ result.push(filePath);
1585
+ }
1586
+ }
1587
+ return result;
1588
+ }
1381
1589
  async function loadAdapters(providers) {
1382
1590
  const adapters = [];
1383
1591
  for (const provider of providers) {
@@ -1397,12 +1605,17 @@ async function loadAdapters(providers) {
1397
1605
  adapters.push(createCopilotAdapter2());
1398
1606
  break;
1399
1607
  }
1608
+ case "agents-dir": {
1609
+ const { createAgentsDirAdapter: createAgentsDirAdapter2 } = await Promise.resolve().then(() => (init_agents_dir(), agents_dir_exports));
1610
+ adapters.push(createAgentsDirAdapter2());
1611
+ break;
1612
+ }
1400
1613
  }
1401
1614
  }
1402
1615
  return adapters;
1403
1616
  }
1404
1617
  async function getCliVersion() {
1405
- return "0.1.3";
1618
+ return "0.4.0";
1406
1619
  }
1407
1620
  async function sync(options) {
1408
1621
  const startTime = Date.now();
@@ -1410,7 +1623,8 @@ async function sync(options) {
1410
1623
  options.telemetry?.write({ event: "sync.started" }).catch(() => {
1411
1624
  });
1412
1625
  try {
1413
- const projectConfig = await readProjectConfig(cwd);
1626
+ const configFilePath = options.configPath ?? path12.join(cwd, ".asdm.json");
1627
+ const projectConfig = await readProjectConfigFromPath(configFilePath);
1414
1628
  const userConfig = await readUserConfig(cwd);
1415
1629
  const client = new RegistryClient(projectConfig.registry);
1416
1630
  const manifest = await client.getLatestManifest();
@@ -1424,9 +1638,9 @@ async function sync(options) {
1424
1638
  resolvedProfile.commands
1425
1639
  );
1426
1640
  const lockfilePath = options.global ? getGlobalLockfilePath() : void 0;
1427
- const existingLockfile = options.force ? null : await readLockfile(cwd, lockfilePath);
1641
+ const existingLockfile = await readLockfile(cwd, lockfilePath);
1428
1642
  const localSourceShas = {};
1429
- if (existingLockfile) {
1643
+ if (!options.force && existingLockfile) {
1430
1644
  for (const [, entry] of Object.entries(existingLockfile.files)) {
1431
1645
  if (entry.managed && entry.source) {
1432
1646
  localSourceShas[entry.source] = entry.sha256;
@@ -1435,7 +1649,7 @@ async function sync(options) {
1435
1649
  }
1436
1650
  const diff = diffManifest(manifest, localSourceShas, assetPaths);
1437
1651
  const toDownload = options.force ? assetPaths : [...diff.added, ...diff.updated];
1438
- const cacheDir = path11.join(getAsdmCacheDir(), manifest.version);
1652
+ const cacheDir = path12.join(getAsdmCacheDir(), manifest.version);
1439
1653
  await ensureDir(cacheDir);
1440
1654
  const downloadedAssets = /* @__PURE__ */ new Map();
1441
1655
  for (const assetPath of toDownload) {
@@ -1449,22 +1663,21 @@ async function sync(options) {
1449
1663
  "The asset may have been tampered with or the manifest is stale. Run `asdm sync --force`."
1450
1664
  );
1451
1665
  }
1452
- const cachedPath = path11.join(cacheDir, assetPath);
1666
+ const cachedPath = path12.join(cacheDir, assetPath);
1453
1667
  if (!options.dryRun) {
1454
1668
  await writeFile(cachedPath, content);
1455
1669
  }
1456
1670
  downloadedAssets.set(assetPath, content);
1457
1671
  }
1458
1672
  for (const assetPath of diff.unchanged) {
1459
- const cachedPath = path11.join(cacheDir, assetPath);
1673
+ const cachedPath = path12.join(cacheDir, assetPath);
1460
1674
  try {
1461
- const { readFile: readFile2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1462
- const cached = await readFile2(cachedPath);
1675
+ const cached = await readFile(cachedPath);
1463
1676
  if (cached) downloadedAssets.set(assetPath, cached);
1464
1677
  } catch {
1465
1678
  }
1466
1679
  }
1467
- if (options.dryRun || options.noEmit) {
1680
+ if (options.noEmit) {
1468
1681
  const stats2 = {
1469
1682
  filesAdded: diff.added.length,
1470
1683
  filesUpdated: diff.updated.length,
@@ -1484,7 +1697,7 @@ async function sync(options) {
1484
1697
  durationMs: stats2.duration
1485
1698
  }).catch(() => {
1486
1699
  });
1487
- return { stats: stats2, emittedFiles: [], dryRun: !!options.dryRun };
1700
+ return { stats: stats2, emittedFiles: [], dryRun: false };
1488
1701
  }
1489
1702
  const adapters = await loadAdapters(activeProviders);
1490
1703
  const allEmittedFiles = [];
@@ -1518,9 +1731,70 @@ async function sync(options) {
1518
1731
  const configFiles = adapter.emitConfig(resolvedProfile, cwd);
1519
1732
  allEmittedFiles.push(...configFiles);
1520
1733
  }
1734
+ const newRelativePaths = new Set(allEmittedFiles.map((f) => f.relativePath));
1735
+ const orphansToDelete = [];
1736
+ if (options.clean) {
1737
+ const activeAdapterSet = new Set(
1738
+ options.provider ? [options.provider] : activeProviders
1739
+ );
1740
+ if (existingLockfile) {
1741
+ for (const [relPath, entry] of Object.entries(existingLockfile.files)) {
1742
+ if (!entry.managed || !activeAdapterSet.has(entry.adapter)) continue;
1743
+ if (newRelativePaths.has(relPath)) continue;
1744
+ let absPath;
1745
+ if (options.global) {
1746
+ const p = resolveGlobalEmitPath(relPath, entry.adapter);
1747
+ if (!p) continue;
1748
+ absPath = p;
1749
+ } else {
1750
+ absPath = path12.join(cwd, relPath);
1751
+ }
1752
+ orphansToDelete.push(absPath);
1753
+ }
1754
+ }
1755
+ if (!options.global) {
1756
+ const orphanAbsPaths = new Set(orphansToDelete);
1757
+ for (const provider of activeProviders) {
1758
+ const scanDirs = ADAPTER_SCAN_DIRS[provider] ?? [];
1759
+ for (const scanDir of scanDirs) {
1760
+ const absDir = path12.join(cwd, scanDir);
1761
+ const managed = await findManagedFilesInDir(absDir);
1762
+ for (const absFile of managed) {
1763
+ const relPath = path12.relative(cwd, absFile).split(path12.sep).join("/");
1764
+ if (!newRelativePaths.has(relPath) && !orphanAbsPaths.has(absFile)) {
1765
+ orphansToDelete.push(absFile);
1766
+ orphanAbsPaths.add(absFile);
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ }
1773
+ if (options.dryRun) {
1774
+ const stats2 = {
1775
+ filesAdded: diff.added.length,
1776
+ filesUpdated: diff.updated.length,
1777
+ filesUnchanged: diff.unchanged.length,
1778
+ filesRemoved: diff.removed.length + orphansToDelete.length,
1779
+ duration: Date.now() - startTime,
1780
+ manifestVersion: manifest.version,
1781
+ profile: resolvedConfig.profile,
1782
+ providers: activeProviders
1783
+ };
1784
+ options.telemetry?.write({
1785
+ event: "sync.completed",
1786
+ profile: resolvedConfig.profile,
1787
+ registry: resolvedConfig.registry,
1788
+ providers: activeProviders,
1789
+ assetCount: 0,
1790
+ durationMs: stats2.duration
1791
+ }).catch(() => {
1792
+ });
1793
+ return { stats: stats2, emittedFiles: [], dryRun: true };
1794
+ }
1521
1795
  const resolvedPaths = /* @__PURE__ */ new Map();
1522
1796
  for (const emittedFile of allEmittedFiles) {
1523
- const absolutePath = options.global ? resolveGlobalEmitPath(emittedFile.relativePath, emittedFile.adapter) : path11.join(cwd, emittedFile.relativePath);
1797
+ const absolutePath = options.global ? resolveGlobalEmitPath(emittedFile.relativePath, emittedFile.adapter) : path12.join(cwd, emittedFile.relativePath);
1524
1798
  if (absolutePath !== null) {
1525
1799
  resolvedPaths.set(emittedFile.relativePath, absolutePath);
1526
1800
  }
@@ -1530,6 +1804,11 @@ async function sync(options) {
1530
1804
  if (absolutePath === void 0) continue;
1531
1805
  await writeFile(absolutePath, emittedFile.content);
1532
1806
  }
1807
+ let orphanFilesRemoved = 0;
1808
+ for (const absPath of orphansToDelete) {
1809
+ await removeFile(absPath);
1810
+ orphanFilesRemoved++;
1811
+ }
1533
1812
  const lockfileFiles = {};
1534
1813
  for (const emittedFile of allEmittedFiles) {
1535
1814
  if (!resolvedPaths.has(emittedFile.relativePath)) continue;
@@ -1554,7 +1833,7 @@ async function sync(options) {
1554
1833
  filesAdded: diff.added.length,
1555
1834
  filesUpdated: diff.updated.length,
1556
1835
  filesUnchanged: diff.unchanged.length,
1557
- filesRemoved: diff.removed.length,
1836
+ filesRemoved: diff.removed.length + orphanFilesRemoved,
1558
1837
  duration: Date.now() - startTime,
1559
1838
  manifestVersion: manifest.version,
1560
1839
  profile: resolvedConfig.profile,
@@ -1581,6 +1860,19 @@ async function sync(options) {
1581
1860
  }
1582
1861
 
1583
1862
  // src/cli/commands/sync.ts
1863
+ init_fs();
1864
+ async function resolveConfigPath(cwd, isGlobal) {
1865
+ const localPath = path13.join(cwd, ".asdm.json");
1866
+ if (await exists(localPath)) return localPath;
1867
+ if (isGlobal) {
1868
+ const globalPath = getGlobalConfigPath();
1869
+ if (await exists(globalPath)) return globalPath;
1870
+ }
1871
+ throw new ConfigError(
1872
+ "No config found.",
1873
+ isGlobal ? "Run `asdm init` (project) or `asdm init --global` (machine-wide setup)." : "Run `asdm init` to initialize this project."
1874
+ );
1875
+ }
1584
1876
  var sync_default = defineCommand2({
1585
1877
  meta: {
1586
1878
  name: "sync",
@@ -1597,6 +1889,11 @@ var sync_default = defineCommand2({
1597
1889
  description: "Re-download all assets even if SHA matches",
1598
1890
  default: false
1599
1891
  },
1892
+ clean: {
1893
+ type: "boolean",
1894
+ description: "Remove managed files from previous profile that are no longer in use",
1895
+ default: false
1896
+ },
1600
1897
  verbose: {
1601
1898
  type: "boolean",
1602
1899
  description: "Print verbose output",
@@ -1623,13 +1920,16 @@ var sync_default = defineCommand2({
1623
1920
  logger.asdm("Starting sync\u2026");
1624
1921
  const telemetry = new TelemetryWriter(cwd);
1625
1922
  try {
1923
+ const configPath = await resolveConfigPath(cwd, ctx.args.global ?? false);
1626
1924
  const result = await sync({
1627
1925
  cwd,
1926
+ configPath,
1628
1927
  force: ctx.args.force,
1629
1928
  dryRun,
1630
1929
  verbose,
1631
1930
  provider: ctx.args.provider,
1632
1931
  global: ctx.args.global ?? false,
1932
+ clean: ctx.args.clean,
1633
1933
  telemetry
1634
1934
  });
1635
1935
  const { stats } = result;
@@ -1650,6 +1950,7 @@ var sync_default = defineCommand2({
1650
1950
  ["Files added", String(stats.filesAdded)],
1651
1951
  ["Files updated", String(stats.filesUpdated)],
1652
1952
  ["Files unchanged", String(stats.filesUnchanged)],
1953
+ ["Files removed", String(stats.filesRemoved)],
1653
1954
  ["Duration", `${stats.duration}ms`]
1654
1955
  ]);
1655
1956
  } catch (err) {
@@ -1666,7 +1967,7 @@ var sync_default = defineCommand2({
1666
1967
  import { defineCommand as defineCommand3 } from "citty";
1667
1968
 
1668
1969
  // src/core/verifier.ts
1669
- import path12 from "path";
1970
+ import path14 from "path";
1670
1971
  init_hash();
1671
1972
  init_fs();
1672
1973
  var VERIFY_EXIT_CODES = {
@@ -1691,7 +1992,7 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry,
1691
1992
  let checkedFiles = 0;
1692
1993
  const filesToCheck = onlyManaged ? Object.entries(lockfile.files).filter(([, entry]) => entry.managed) : Object.entries(lockfile.files);
1693
1994
  for (const [relativePath, entry] of filesToCheck) {
1694
- const absolutePath = isGlobal ? resolveGlobalEmitPath(relativePath, entry.adapter) ?? path12.join(cwd, relativePath) : path12.join(cwd, relativePath);
1995
+ const absolutePath = isGlobal ? resolveGlobalEmitPath(relativePath, entry.adapter) ?? path14.join(cwd, relativePath) : path14.join(cwd, relativePath);
1695
1996
  checkedFiles++;
1696
1997
  const fileExists = await exists(absolutePath);
1697
1998
  if (!fileExists) {
@@ -1736,6 +2037,16 @@ async function verify(cwd, latestManifestVersion, onlyManaged = true, telemetry,
1736
2037
 
1737
2038
  // src/cli/commands/verify.ts
1738
2039
  init_fs();
2040
+ async function fetchLatestManifestVersion(cwd) {
2041
+ try {
2042
+ const config = await readProjectConfig(cwd);
2043
+ const client = new RegistryClient(config.registry);
2044
+ const manifest = await client.getLatestManifest();
2045
+ return manifest.version;
2046
+ } catch {
2047
+ return void 0;
2048
+ }
2049
+ }
1739
2050
  var verify_default = defineCommand3({
1740
2051
  meta: {
1741
2052
  name: "verify",
@@ -1761,6 +2072,11 @@ var verify_default = defineCommand3({
1761
2072
  type: "boolean",
1762
2073
  description: "Verify files installed to global provider config directories",
1763
2074
  default: false
2075
+ },
2076
+ offline: {
2077
+ type: "boolean",
2078
+ description: "Skip remote manifest version check (exit code 3 will never trigger)",
2079
+ default: false
1764
2080
  }
1765
2081
  },
1766
2082
  async run(ctx) {
@@ -1788,8 +2104,12 @@ var verify_default = defineCommand3({
1788
2104
  process.exitCode = result.exitCode;
1789
2105
  return;
1790
2106
  }
2107
+ let latestManifestVersion;
2108
+ if (!ctx.args.offline && !ctx.args.global) {
2109
+ latestManifestVersion = await fetchLatestManifestVersion(cwd);
2110
+ }
1791
2111
  try {
1792
- const result = await verify(cwd, void 0, true, telemetry, lockfilePath);
2112
+ const result = await verify(cwd, latestManifestVersion, true, telemetry, lockfilePath);
1793
2113
  if (useJson) {
1794
2114
  console.log(JSON.stringify(result, null, 2));
1795
2115
  process.exitCode = result.exitCode;
@@ -2300,7 +2620,7 @@ var version_default = defineCommand10({
2300
2620
  description: "Print CLI version and environment info"
2301
2621
  },
2302
2622
  run(_ctx) {
2303
- console.log(`asdm v${"0.1.3"}`);
2623
+ console.log(`asdm v${"0.4.0"}`);
2304
2624
  console.log(`node ${process.version}`);
2305
2625
  console.log(`os ${os3.type()} ${os3.release()} (${process.platform})`);
2306
2626
  }
@@ -2308,11 +2628,11 @@ var version_default = defineCommand10({
2308
2628
 
2309
2629
  // src/cli/commands/doctor.ts
2310
2630
  import { defineCommand as defineCommand11 } from "citty";
2311
- import path14 from "path";
2631
+ import path16 from "path";
2312
2632
 
2313
2633
  // src/core/overlay.ts
2314
2634
  init_fs();
2315
- import path13 from "path";
2635
+ import path15 from "path";
2316
2636
  import { promises as fs4 } from "fs";
2317
2637
  var OVERLAY_SEPARATOR = [
2318
2638
  "",
@@ -2323,7 +2643,7 @@ var OVERLAY_SEPARATOR = [
2323
2643
  ""
2324
2644
  ].join("\n");
2325
2645
  async function readOverlays(projectRoot) {
2326
- const overlaysDir = path13.join(projectRoot, "overlays");
2646
+ const overlaysDir = path15.join(projectRoot, "overlays");
2327
2647
  const dirExists = await exists(overlaysDir);
2328
2648
  if (!dirExists) return /* @__PURE__ */ new Map();
2329
2649
  const entries = await fs4.readdir(overlaysDir, { withFileTypes: true });
@@ -2331,7 +2651,7 @@ async function readOverlays(projectRoot) {
2331
2651
  for (const entry of entries) {
2332
2652
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2333
2653
  const agentId = entry.name.slice(0, -".md".length);
2334
- const overlayPath = path13.join(overlaysDir, entry.name);
2654
+ const overlayPath = path15.join(overlaysDir, entry.name);
2335
2655
  const content = await readFile(overlayPath);
2336
2656
  if (content === null) continue;
2337
2657
  overlayMap.set(agentId, { agentId, content, path: overlayPath });
@@ -2362,7 +2682,7 @@ var doctor_default = defineCommand11({
2362
2682
  let anyFailed = false;
2363
2683
  let projectConfig = null;
2364
2684
  let registryClient = null;
2365
- const configPath = path14.join(cwd, ".asdm.json");
2685
+ const configPath = path16.join(cwd, ".asdm.json");
2366
2686
  const hasConfig = await exists(configPath);
2367
2687
  checks.push({
2368
2688
  label: ".asdm.json present",
@@ -2427,10 +2747,10 @@ var doctor_default = defineCommand11({
2427
2747
  }
2428
2748
  const managedEntries = Object.entries(lockfile.files).filter(([, e]) => e.managed);
2429
2749
  const missingFiles = [];
2430
- const resolvedCwd = path14.resolve(cwd);
2750
+ const resolvedCwd = path16.resolve(cwd);
2431
2751
  for (const [filePath] of managedEntries) {
2432
- const absPath = path14.resolve(cwd, filePath);
2433
- if (!absPath.startsWith(resolvedCwd + path14.sep) && absPath !== resolvedCwd) {
2752
+ const absPath = path16.resolve(cwd, filePath);
2753
+ if (!absPath.startsWith(resolvedCwd + path16.sep) && absPath !== resolvedCwd) {
2434
2754
  continue;
2435
2755
  }
2436
2756
  const fileExists = await exists(absPath);
@@ -2485,7 +2805,7 @@ var doctor_default = defineCommand11({
2485
2805
  detail: "Skipped \u2014 no lockfile"
2486
2806
  });
2487
2807
  }
2488
- const gitignoreContent = await readFile(path14.join(cwd, ".gitignore"));
2808
+ const gitignoreContent = await readFile(path16.join(cwd, ".gitignore"));
2489
2809
  const hasAsdmBlock = gitignoreContent?.includes(ASDM_MARKER_START) ?? false;
2490
2810
  checks.push({
2491
2811
  label: ".gitignore has ASDM block",
@@ -2539,9 +2859,9 @@ var doctor_default = defineCommand11({
2539
2859
 
2540
2860
  // src/cli/commands/clean.ts
2541
2861
  import { defineCommand as defineCommand12 } from "citty";
2542
- import path15 from "path";
2862
+ import path17 from "path";
2543
2863
  import { promises as fs5 } from "fs";
2544
- import readline from "readline";
2864
+ import readline2 from "readline";
2545
2865
  init_fs();
2546
2866
  var LOCKFILE_NAME = ".asdm-lock.json";
2547
2867
  async function getFileSizeBytes(filePath) {
@@ -2559,7 +2879,7 @@ function formatBytes(bytes) {
2559
2879
  return `${(kb / 1024).toFixed(2)} MB`;
2560
2880
  }
2561
2881
  async function confirmPrompt(question) {
2562
- const rl = readline.createInterface({
2882
+ const rl = readline2.createInterface({
2563
2883
  input: process.stdin,
2564
2884
  output: process.stdout
2565
2885
  });
@@ -2581,9 +2901,14 @@ var clean_default = defineCommand12({
2581
2901
  description: "Preview what would be removed without deleting",
2582
2902
  default: false
2583
2903
  },
2904
+ global: {
2905
+ type: "boolean",
2906
+ description: "Clean files installed to global provider config directories",
2907
+ default: false
2908
+ },
2584
2909
  target: {
2585
2910
  type: "string",
2586
- description: "Only clean files for a specific provider (opencode | claude-code | copilot)",
2911
+ description: "Only clean files for a specific provider (opencode | claude-code | copilot | agents-dir)",
2587
2912
  alias: "t"
2588
2913
  }
2589
2914
  },
@@ -2591,119 +2916,226 @@ var clean_default = defineCommand12({
2591
2916
  const cwd = process.cwd();
2592
2917
  const dryRun = ctx.args["dry-run"];
2593
2918
  const target = ctx.args.target;
2919
+ const isGlobal = ctx.args.global ?? false;
2594
2920
  if (dryRun) {
2595
2921
  logger.info("Dry run \u2014 no files will be removed");
2596
2922
  }
2597
- const lockfile = await readLockfile(cwd);
2598
- if (!lockfile) {
2599
- logger.warn("No lockfile found \u2014 nothing to clean");
2923
+ if (isGlobal) {
2924
+ await runGlobalClean(dryRun, target);
2600
2925
  return;
2601
2926
  }
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
- }
2927
+ await runLocalClean(cwd, dryRun, target);
2928
+ }
2929
+ });
2930
+ async function runGlobalClean(dryRun, target) {
2931
+ const globalLockfilePath = getGlobalLockfilePath();
2932
+ const lockfile = await readLockfile(process.cwd(), globalLockfilePath);
2933
+ if (!lockfile) {
2934
+ logger.warn("No global lockfile found \u2014 nothing to clean");
2935
+ return;
2936
+ }
2937
+ const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
2938
+ if (!entry.managed) return false;
2939
+ if (target) return entry.adapter === target;
2940
+ return true;
2941
+ });
2942
+ if (managedEntries.length === 0) {
2943
+ if (target) {
2944
+ logger.warn(`No globally managed files found for provider "${target}"`);
2945
+ } else {
2946
+ logger.warn("No globally managed files found \u2014 nothing to clean");
2947
+ }
2948
+ return;
2949
+ }
2950
+ if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
2951
+ const suffix = target ? ` for provider "${target}"` : "";
2952
+ const confirmed = await confirmPrompt(
2953
+ `About to delete ${managedEntries.length} globally managed file(s)${suffix}. Continue? [y/N] `
2954
+ );
2955
+ if (!confirmed) {
2956
+ logger.info("Aborted \u2014 no files were removed");
2613
2957
  return;
2614
2958
  }
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] `
2959
+ }
2960
+ logger.asdm(`Cleaning ${managedEntries.length} globally managed file(s)\u2026`);
2961
+ logger.divider();
2962
+ let removed = 0;
2963
+ let skippedMissing = 0;
2964
+ let totalBytesFreed = 0;
2965
+ for (const [relativePath, entry] of managedEntries) {
2966
+ const absolutePath = resolveGlobalEmitPath(relativePath, entry.adapter);
2967
+ if (absolutePath === null) {
2968
+ logger.dim(` skip ${relativePath} (project-root file, not applicable in global mode)`);
2969
+ skippedMissing++;
2970
+ continue;
2971
+ }
2972
+ const filePresent = await exists(absolutePath);
2973
+ if (!filePresent) {
2974
+ logger.dim(` skip ${absolutePath} (not found)`);
2975
+ skippedMissing++;
2976
+ continue;
2977
+ }
2978
+ if (dryRun) {
2979
+ logger.bullet(`would remove: ${absolutePath}`);
2980
+ removed++;
2981
+ continue;
2982
+ }
2983
+ const fileSize = await getFileSizeBytes(absolutePath);
2984
+ await removeFile(absolutePath);
2985
+ totalBytesFreed += fileSize;
2986
+ logger.bullet(`removed: ${absolutePath}`);
2987
+ removed++;
2988
+ }
2989
+ const lockfilePresent = await exists(globalLockfilePath);
2990
+ if (target) {
2991
+ if (!dryRun && lockfilePresent) {
2992
+ const updatedFiles = Object.fromEntries(
2993
+ Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
2629
2994
  );
2630
- if (!confirmed) {
2631
- logger.info("Aborted \u2014 no files were removed");
2632
- return;
2995
+ const hasRemainingEntries = Object.keys(updatedFiles).length > 0;
2996
+ if (hasRemainingEntries) {
2997
+ await writeLockfile(process.cwd(), { ...lockfile, files: updatedFiles }, globalLockfilePath);
2998
+ logger.bullet(`updated: global lockfile (removed ${target} entries)`);
2999
+ } else {
3000
+ await removeFile(globalLockfilePath);
3001
+ logger.bullet(`removed: global lockfile (no entries remaining)`);
2633
3002
  }
3003
+ } else if (dryRun) {
3004
+ logger.bullet(`would update: global lockfile (remove ${target} entries)`);
2634
3005
  }
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
- }
3006
+ } else {
3007
+ if (lockfilePresent) {
2648
3008
  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++;
3009
+ logger.bullet(`would remove: global lockfile`);
3010
+ } else {
3011
+ const lockfileSize = await getFileSizeBytes(globalLockfilePath);
3012
+ await removeFile(globalLockfilePath);
3013
+ totalBytesFreed += lockfileSize;
3014
+ logger.bullet(`removed: global lockfile`);
3015
+ }
2658
3016
  }
2659
- const lockfilePath = path15.join(cwd, LOCKFILE_NAME);
2660
- const lockfileOnDisk = await exists(lockfilePath);
3017
+ }
3018
+ logger.divider();
3019
+ if (dryRun) {
3020
+ const suffix = target ? ` for provider "${target}"` : "";
3021
+ logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} skipped`);
3022
+ logger.info("Run without --dry-run to actually remove them");
3023
+ } else {
3024
+ const suffix = target ? ` (${target})` : "";
3025
+ logger.success(`Cleaned ${removed} globally managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
3026
+ if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
3027
+ logger.info("Run `asdm sync --global` to reinstall");
3028
+ }
3029
+ }
3030
+ async function runLocalClean(cwd, dryRun, target) {
3031
+ const lockfile = await readLockfile(cwd);
3032
+ if (!lockfile) {
3033
+ logger.warn("No lockfile found \u2014 nothing to clean");
3034
+ return;
3035
+ }
3036
+ const managedEntries = Object.entries(lockfile.files).filter(([, entry]) => {
3037
+ if (!entry.managed) return false;
3038
+ if (target) return entry.adapter === target;
3039
+ return true;
3040
+ });
3041
+ if (managedEntries.length === 0) {
2661
3042
  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
- }
3043
+ logger.warn(`No managed files found for provider "${target}"`);
2671
3044
  } 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
- }
3045
+ logger.warn("No managed files found \u2014 nothing to clean");
3046
+ }
3047
+ return;
3048
+ }
3049
+ const managedPaths = managedEntries.map(([filePath]) => filePath);
3050
+ const resolvedCwd = path17.resolve(cwd);
3051
+ const safePaths = managedPaths.filter((relativePath) => {
3052
+ const absPath = path17.resolve(cwd, relativePath);
3053
+ return absPath.startsWith(resolvedCwd + path17.sep) || absPath === resolvedCwd;
3054
+ });
3055
+ const skippedSuspicious = managedPaths.length - safePaths.length;
3056
+ if (skippedSuspicious > 0) {
3057
+ logger.warn(`Skipping ${skippedSuspicious} path(s) outside project root`);
3058
+ }
3059
+ if (!dryRun && process.stdout.isTTY && process.stdin.isTTY) {
3060
+ const suffix = target ? ` for provider "${target}"` : "";
3061
+ const confirmed = await confirmPrompt(
3062
+ `About to delete ${safePaths.length} file(s)${suffix}. Continue? [y/N] `
3063
+ );
3064
+ if (!confirmed) {
3065
+ logger.info("Aborted \u2014 no files were removed");
3066
+ return;
3067
+ }
3068
+ }
3069
+ logger.asdm(`Cleaning ${safePaths.length} managed file(s)\u2026`);
3070
+ logger.divider();
3071
+ let removed = 0;
3072
+ let skippedMissing = 0;
3073
+ let totalBytesFreed = 0;
3074
+ for (const relativePath of safePaths) {
3075
+ const absolutePath = path17.resolve(cwd, relativePath);
3076
+ const filePresent = await exists(absolutePath);
3077
+ if (!filePresent) {
3078
+ logger.dim(` skip ${relativePath} (not found)`);
3079
+ skippedMissing++;
3080
+ continue;
2682
3081
  }
2683
- logger.divider();
2684
3082
  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");
3083
+ logger.bullet(`would remove: ${relativePath}`);
3084
+ removed++;
3085
+ continue;
3086
+ }
3087
+ const fileSize = await getFileSizeBytes(absolutePath);
3088
+ await removeFile(absolutePath);
3089
+ totalBytesFreed += fileSize;
3090
+ logger.bullet(`removed: ${relativePath}`);
3091
+ removed++;
3092
+ }
3093
+ const lockfilePath = path17.join(cwd, LOCKFILE_NAME);
3094
+ const lockfileOnDisk = await exists(lockfilePath);
3095
+ if (target) {
3096
+ if (!dryRun && lockfileOnDisk) {
3097
+ const updatedFiles = Object.fromEntries(
3098
+ Object.entries(lockfile.files).filter(([, entry]) => entry.adapter !== target)
3099
+ );
3100
+ await writeLockfile(cwd, { ...lockfile, files: updatedFiles });
3101
+ logger.bullet(`updated: ${LOCKFILE_NAME} (removed ${target} entries)`);
3102
+ } else if (dryRun) {
3103
+ logger.bullet(`would update: ${LOCKFILE_NAME} (remove ${target} entries)`);
3104
+ }
3105
+ } else {
3106
+ if (lockfileOnDisk) {
3107
+ if (dryRun) {
3108
+ logger.bullet(`would remove: ${LOCKFILE_NAME}`);
3109
+ } else {
3110
+ const lockfileSize = await getFileSizeBytes(lockfilePath);
3111
+ await removeFile(lockfilePath);
3112
+ totalBytesFreed += lockfileSize;
3113
+ logger.bullet(`removed: ${LOCKFILE_NAME}`);
3114
+ }
2693
3115
  }
2694
3116
  }
2695
- });
3117
+ logger.divider();
3118
+ if (dryRun) {
3119
+ const suffix = target ? ` for provider "${target}"` : "";
3120
+ logger.info(`${removed} file(s) would be removed${suffix}, ${skippedMissing} not found`);
3121
+ logger.info("Run without --dry-run to actually remove them");
3122
+ } else {
3123
+ const suffix = target ? ` (${target})` : "";
3124
+ logger.success(`Cleaned ${removed} managed file(s)${suffix} \u2014 ${formatBytes(totalBytesFreed)} freed`);
3125
+ if (skippedMissing > 0) logger.dim(` ${skippedMissing} file(s) were already missing`);
3126
+ logger.info("Run `asdm sync` to reinstall");
3127
+ }
3128
+ }
2696
3129
 
2697
3130
  // src/cli/commands/hooks.ts
2698
3131
  init_fs();
2699
3132
  import { defineCommand as defineCommand13 } from "citty";
2700
- import path16 from "path";
3133
+ import path19 from "path";
2701
3134
  import { promises as fs6 } from "fs";
2702
3135
 
2703
3136
  // src/utils/post-merge-hook.ts
2704
- function generatePostMergeHook() {
2705
- return `#!/usr/bin/env sh
2706
- # ASDM MANAGED \u2014 post-merge hook
3137
+ function generatePostMergeHookBody() {
3138
+ return `# ASDM MANAGED \u2014 post-merge hook
2707
3139
  if [ -f ".asdm.yaml" ] || [ -f ".asdm.json" ]; then
2708
3140
  echo "\u{1F504} ASDM: syncing after merge..."
2709
3141
  npx asdm sync
@@ -2711,43 +3143,108 @@ fi
2711
3143
  `;
2712
3144
  }
2713
3145
 
3146
+ // src/utils/husky-detect.ts
3147
+ init_fs();
3148
+ import path18 from "path";
3149
+ function parseMajorVersion(versionString) {
3150
+ const stripped = versionString.replace(/^[^0-9]*/, "");
3151
+ const majorPart = stripped.split(".")[0] ?? "";
3152
+ const major = parseInt(majorPart, 10);
3153
+ return isNaN(major) ? null : major;
3154
+ }
3155
+ function majorToHuskyVersion(major) {
3156
+ return major >= 9 ? "v9+" : "v8";
3157
+ }
3158
+ async function detectHusky(cwd) {
3159
+ const huskyDirPath = path18.join(cwd, ".husky");
3160
+ const huskyDirExists = await exists(huskyDirPath);
3161
+ const pkg = await readJson(path18.join(cwd, "package.json"));
3162
+ const huskyVersionString = pkg?.devDependencies?.["husky"] ?? pkg?.dependencies?.["husky"];
3163
+ if (huskyVersionString !== void 0) {
3164
+ const major = parseMajorVersion(huskyVersionString);
3165
+ const version2 = major !== null ? majorToHuskyVersion(major) : "v9+";
3166
+ return {
3167
+ detected: true,
3168
+ version: version2,
3169
+ huskyDir: huskyDirExists ? huskyDirPath : null
3170
+ };
3171
+ }
3172
+ if (!huskyDirExists) {
3173
+ return { detected: false, version: null, huskyDir: null };
3174
+ }
3175
+ const huskyShExists = await exists(path18.join(huskyDirPath, "_", "husky.sh"));
3176
+ const version = huskyShExists ? "v8" : "v9+";
3177
+ return {
3178
+ detected: true,
3179
+ version,
3180
+ huskyDir: huskyDirPath
3181
+ };
3182
+ }
3183
+
2714
3184
  // 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
3185
+ var HOOK_BODIES = {
3186
+ "pre-commit": `# ASDM \u2014 managed pre-commit hook
2720
3187
  # Verifies integrity of managed files before allowing commits.
2721
3188
  npx asdm verify --strict --quiet
2722
3189
  `,
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
- }
3190
+ "post-merge": generatePostMergeHookBody()
3191
+ };
3192
+ var HOOK_MARKERS = {
3193
+ "pre-commit": "ASDM \u2014 managed pre-commit hook",
3194
+ "post-merge": "ASDM MANAGED \u2014 post-merge hook"
3195
+ };
3196
+ var HOOK_DESCRIPTIONS = {
3197
+ "pre-commit": "runs `asdm verify --strict --quiet` before every commit",
3198
+ "post-merge": "runs `asdm sync` after git pull/merge"
2732
3199
  };
2733
3200
  function resolveHookTypes(hookFlag) {
2734
- if (hookFlag === "pre-commit" || hookFlag === "post-merge") {
2735
- return [hookFlag];
2736
- }
3201
+ if (hookFlag === "pre-commit" || hookFlag === "post-merge") return [hookFlag];
2737
3202
  return ["pre-commit", "post-merge"];
2738
3203
  }
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);
3204
+ function determineHookMode(huskyInfo, noHusky) {
3205
+ if (noHusky || !huskyInfo.detected) return "git";
3206
+ if (huskyInfo.version === "v8") return "husky-v8";
3207
+ return "husky-v9+";
3208
+ }
3209
+ function buildHookContent(body, mode) {
3210
+ if (mode === "git") return `#!/usr/bin/env sh
3211
+ ${body}`;
3212
+ if (mode === "husky-v8") return `#!/usr/bin/env sh
3213
+ . "$(dirname -- "$0")/_/husky.sh"
3214
+
3215
+ ${body}`;
3216
+ return body;
3217
+ }
3218
+ function resolveHookDefinition(cwd, hookType, huskyInfo, noHusky) {
3219
+ const mode = determineHookMode(huskyInfo, noHusky);
3220
+ const body = HOOK_BODIES[hookType];
3221
+ const marker = HOOK_MARKERS[hookType];
3222
+ const description = HOOK_DESCRIPTIONS[hookType];
3223
+ const relativePath = mode === "git" ? `.git/hooks/${hookType}` : `.husky/${hookType}`;
3224
+ const content = buildHookContent(body, mode);
3225
+ return {
3226
+ absolutePath: path19.join(cwd, relativePath),
3227
+ relativePath,
3228
+ content,
3229
+ marker,
3230
+ description
3231
+ };
3232
+ }
3233
+ async function installHook(cwd, hookType, huskyInfo, noHusky) {
3234
+ const mode = determineHookMode(huskyInfo, noHusky);
3235
+ const def = resolveHookDefinition(cwd, hookType, huskyInfo, noHusky);
3236
+ if (mode === "git") {
3237
+ const hasGit = await exists(path19.join(cwd, ".git"));
3238
+ if (!hasGit) {
3239
+ logger.error("No .git directory found", "Run `git init` first");
3240
+ process.exit(1);
3241
+ }
3242
+ } else {
3243
+ await ensureDir(path19.join(cwd, ".husky"));
2747
3244
  }
2748
- const hookExists = await exists(hookPath);
3245
+ const hookExists = await exists(def.absolutePath);
2749
3246
  if (hookExists) {
2750
- const existing = await fs6.readFile(hookPath, "utf-8");
3247
+ const existing = await fs6.readFile(def.absolutePath, "utf-8");
2751
3248
  if (existing.includes(def.marker)) {
2752
3249
  logger.info(`ASDM ${hookType} hook is already installed`);
2753
3250
  return;
@@ -2756,30 +3253,42 @@ async function installHook(cwd, hookType) {
2756
3253
  logger.warn(`Manual action required: add the ASDM logic to ${def.relativePath}`);
2757
3254
  process.exit(1);
2758
3255
  }
2759
- await writeFile(hookPath, def.content);
3256
+ await writeFile(def.absolutePath, def.content);
2760
3257
  try {
2761
- await fs6.chmod(hookPath, 493);
3258
+ await fs6.chmod(def.absolutePath, 493);
2762
3259
  } catch {
2763
3260
  }
2764
3261
  logger.success(`Installed ${hookType} hook at ${def.relativePath}`);
2765
3262
  logger.info(`The hook ${def.description}`);
2766
3263
  }
2767
3264
  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;
3265
+ const marker = HOOK_MARKERS[hookType];
3266
+ const candidates = [
3267
+ {
3268
+ relativePath: `.git/hooks/${hookType}`,
3269
+ absolutePath: path19.join(cwd, ".git", "hooks", hookType)
3270
+ },
3271
+ {
3272
+ relativePath: `.husky/${hookType}`,
3273
+ absolutePath: path19.join(cwd, ".husky", hookType)
3274
+ }
3275
+ ];
3276
+ let removed = false;
3277
+ for (const { relativePath, absolutePath } of candidates) {
3278
+ const hookExists = await exists(absolutePath);
3279
+ if (!hookExists) continue;
3280
+ const content = await fs6.readFile(absolutePath, "utf-8");
3281
+ if (!content.includes(marker)) {
3282
+ logger.warn(`A ${hookType} hook at ${relativePath} was not installed by ASDM \u2014 skipping`);
3283
+ continue;
3284
+ }
3285
+ await removeFile(absolutePath);
3286
+ logger.success(`Removed ASDM ${hookType} hook from ${relativePath}`);
3287
+ removed = true;
2774
3288
  }
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);
3289
+ if (!removed) {
3290
+ logger.info(`No ASDM-managed ${hookType} hook found \u2014 nothing to remove`);
2780
3291
  }
2781
- await removeFile(hookPath);
2782
- logger.success(`Removed ASDM ${hookType} hook from ${def.relativePath}`);
2783
3292
  }
2784
3293
  var installCommand = defineCommand13({
2785
3294
  meta: {
@@ -2791,13 +3300,25 @@ var installCommand = defineCommand13({
2791
3300
  type: "string",
2792
3301
  description: "Which hook to install: pre-commit | post-merge | all",
2793
3302
  default: "all"
3303
+ },
3304
+ "no-husky": {
3305
+ type: "boolean",
3306
+ description: "Force .git/hooks/ mode even when Husky is detected",
3307
+ default: false
2794
3308
  }
2795
3309
  },
2796
3310
  async run(ctx) {
2797
3311
  const cwd = process.cwd();
2798
3312
  const hookTypes = resolveHookTypes(ctx.args.hook);
3313
+ const noHusky = ctx.args["no-husky"] ?? false;
3314
+ const huskyInfo = noHusky ? { detected: false, version: null, huskyDir: null } : await detectHusky(cwd);
3315
+ if (huskyInfo.detected) {
3316
+ logger.info(`Using Husky hooks in .husky/ (${huskyInfo.version})`);
3317
+ } else {
3318
+ logger.info("Using Git hooks in .git/hooks/");
3319
+ }
2799
3320
  for (const hookType of hookTypes) {
2800
- await installHook(cwd, hookType);
3321
+ await installHook(cwd, hookType, huskyInfo, noHusky);
2801
3322
  }
2802
3323
  }
2803
3324
  });
@@ -2973,9 +3494,204 @@ var telemetry_default = defineCommand15({
2973
3494
  }
2974
3495
  });
2975
3496
 
3497
+ // src/cli/commands/templates.ts
3498
+ init_fs();
3499
+ import { defineCommand as defineCommand16 } from "citty";
3500
+ import path20 from "path";
3501
+ function generateAgentTemplate(name) {
3502
+ return `---
3503
+ name: ${name}
3504
+ type: agent
3505
+ description: "Short description"
3506
+ version: "1.0.0"
3507
+ tags:
3508
+ - tag1
3509
+ - tag2
3510
+ providers:
3511
+ opencode:
3512
+ model: claude-sonnet-4-5
3513
+ permissions: {}
3514
+ tools: []
3515
+ claude-code:
3516
+ model: claude-opus-4-5
3517
+ allowedTools: []
3518
+ copilot:
3519
+ on:
3520
+ push:
3521
+ branches: [main]
3522
+ permissions:
3523
+ contents: read
3524
+ ---
3525
+
3526
+ # ${name}
3527
+
3528
+ ## Role
3529
+ Describe the agent's primary responsibility here.
3530
+
3531
+ ## Instructions
3532
+ - Instruction 1
3533
+ - Instruction 2
3534
+
3535
+ ## Guidelines
3536
+ - Guideline 1
3537
+ - Guideline 2
3538
+ `;
3539
+ }
3540
+ function generateSkillTemplate(name) {
3541
+ return `---
3542
+ name: ${name}
3543
+ type: skill
3544
+ description: "Short description"
3545
+ version: "1.0.0"
3546
+ tags:
3547
+ - tag1
3548
+ ---
3549
+
3550
+ # ${name}
3551
+
3552
+ ## Overview
3553
+ Describe the skill purpose.
3554
+
3555
+ ## Usage
3556
+ How to use this skill.
3557
+
3558
+ ## Examples
3559
+ - Example 1
3560
+ - Example 2
3561
+ `;
3562
+ }
3563
+ function generateCommandTemplate(name) {
3564
+ return `---
3565
+ name: ${name}
3566
+ type: command
3567
+ description: "Short description"
3568
+ version: "1.0.0"
3569
+ tags:
3570
+ - tag1
3571
+ ---
3572
+
3573
+ # /${name}
3574
+
3575
+ ## Description
3576
+ What this command does.
3577
+
3578
+ ## Usage
3579
+ \`/${name} [options]\`
3580
+
3581
+ ## Examples
3582
+ - \`/${name}\` \u2014 basic usage
3583
+ `;
3584
+ }
3585
+ async function writeTemplateFile(outputDir, name, content, force) {
3586
+ const filePath = path20.join(outputDir, `${name}.asdm.md`);
3587
+ const alreadyExists = await exists(filePath);
3588
+ if (alreadyExists && !force) {
3589
+ logger.error(
3590
+ `File already exists: ${filePath}`,
3591
+ "Use --force to overwrite"
3592
+ );
3593
+ process.exitCode = 1;
3594
+ return;
3595
+ }
3596
+ await writeFile(filePath, content);
3597
+ logger.success(`Created ${filePath}`);
3598
+ }
3599
+ var agentCommand = defineCommand16({
3600
+ meta: {
3601
+ name: "agent",
3602
+ description: "Scaffold an agent definition (.asdm.md)"
3603
+ },
3604
+ args: {
3605
+ name: {
3606
+ type: "positional",
3607
+ description: "Agent identifier (used as filename and frontmatter name)",
3608
+ required: true
3609
+ },
3610
+ output: {
3611
+ type: "string",
3612
+ description: "Output directory (default: current working directory)"
3613
+ },
3614
+ force: {
3615
+ type: "boolean",
3616
+ description: "Overwrite existing file",
3617
+ default: false
3618
+ }
3619
+ },
3620
+ async run(ctx) {
3621
+ const cwd = process.cwd();
3622
+ const outputDir = ctx.args.output ?? cwd;
3623
+ await writeTemplateFile(outputDir, ctx.args.name, generateAgentTemplate(ctx.args.name), ctx.args.force);
3624
+ }
3625
+ });
3626
+ var skillCommand = defineCommand16({
3627
+ meta: {
3628
+ name: "skill",
3629
+ description: "Scaffold a skill definition (.asdm.md)"
3630
+ },
3631
+ args: {
3632
+ name: {
3633
+ type: "positional",
3634
+ description: "Skill identifier (used as filename and frontmatter name)",
3635
+ required: true
3636
+ },
3637
+ output: {
3638
+ type: "string",
3639
+ description: "Output directory (default: current working directory)"
3640
+ },
3641
+ force: {
3642
+ type: "boolean",
3643
+ description: "Overwrite existing file",
3644
+ default: false
3645
+ }
3646
+ },
3647
+ async run(ctx) {
3648
+ const cwd = process.cwd();
3649
+ const outputDir = ctx.args.output ?? cwd;
3650
+ await writeTemplateFile(outputDir, ctx.args.name, generateSkillTemplate(ctx.args.name), ctx.args.force);
3651
+ }
3652
+ });
3653
+ var commandCommand = defineCommand16({
3654
+ meta: {
3655
+ name: "command",
3656
+ description: "Scaffold a slash-command definition (.asdm.md)"
3657
+ },
3658
+ args: {
3659
+ name: {
3660
+ type: "positional",
3661
+ description: "Command identifier (used as filename and frontmatter name)",
3662
+ required: true
3663
+ },
3664
+ output: {
3665
+ type: "string",
3666
+ description: "Output directory (default: current working directory)"
3667
+ },
3668
+ force: {
3669
+ type: "boolean",
3670
+ description: "Overwrite existing file",
3671
+ default: false
3672
+ }
3673
+ },
3674
+ async run(ctx) {
3675
+ const cwd = process.cwd();
3676
+ const outputDir = ctx.args.output ?? cwd;
3677
+ await writeTemplateFile(outputDir, ctx.args.name, generateCommandTemplate(ctx.args.name), ctx.args.force);
3678
+ }
3679
+ });
3680
+ var templates_default = defineCommand16({
3681
+ meta: {
3682
+ name: "templates",
3683
+ description: "Scaffold new .asdm.md asset files from built-in templates"
3684
+ },
3685
+ subCommands: {
3686
+ agent: agentCommand,
3687
+ skill: skillCommand,
3688
+ command: commandCommand
3689
+ }
3690
+ });
3691
+
2976
3692
  // src/core/version-check.ts
2977
3693
  init_fs();
2978
- import path17 from "path";
3694
+ import path21 from "path";
2979
3695
  var CACHE_FILENAME = "version-check-cache.json";
2980
3696
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2981
3697
  var FETCH_TIMEOUT_MS2 = 3e3;
@@ -2996,7 +3712,7 @@ function isNewerVersion(current, latest) {
2996
3712
  }
2997
3713
  async function checkForUpdate(currentVersion) {
2998
3714
  const cacheDir = getAsdmConfigDir();
2999
- const cachePath = path17.join(cacheDir, CACHE_FILENAME);
3715
+ const cachePath = path21.join(cacheDir, CACHE_FILENAME);
3000
3716
  const cache = await readJson(cachePath);
3001
3717
  if (cache) {
3002
3718
  const ageMs = Date.now() - new Date(cache.checkedAt).getTime();
@@ -3032,10 +3748,10 @@ async function checkForUpdate(currentVersion) {
3032
3748
  }
3033
3749
 
3034
3750
  // src/cli/index.ts
3035
- var rootCommand = defineCommand16({
3751
+ var rootCommand = defineCommand17({
3036
3752
  meta: {
3037
3753
  name: "asdm",
3038
- version: "0.1.3",
3754
+ version: "0.4.0",
3039
3755
  description: "Agentic Software Delivery Model \u2014 Write Once, Emit Many"
3040
3756
  },
3041
3757
  subCommands: {
@@ -3053,13 +3769,14 @@ var rootCommand = defineCommand16({
3053
3769
  clean: clean_default,
3054
3770
  hooks: hooks_default,
3055
3771
  gitignore: gitignore_default,
3056
- telemetry: telemetry_default
3772
+ telemetry: telemetry_default,
3773
+ templates: templates_default
3057
3774
  }
3058
3775
  });
3059
3776
  function printUpdateBox(currentVersion, latestVersion) {
3060
3777
  const YELLOW = "\x1B[33m";
3061
- const BOLD2 = "\x1B[1m";
3062
- const RESET2 = "\x1B[0m";
3778
+ const BOLD3 = "\x1B[1m";
3779
+ const RESET3 = "\x1B[0m";
3063
3780
  const updateLine = ` Update available: ${currentVersion} \u2192 ${latestVersion}`;
3064
3781
  const cmdLine = ` Run: npm install -g asdm-cli`;
3065
3782
  const MIN_WIDTH = 45;
@@ -3072,18 +3789,18 @@ function printUpdateBox(currentVersion, latestVersion) {
3072
3789
  const r2 = `\u2502${cmdLine.padEnd(innerWidth)}\u2502`;
3073
3790
  const bot = `\u2570${"\u2500".repeat(innerWidth)}\u256F`;
3074
3791
  console.log(`
3075
- ${YELLOW}${BOLD2}${top}
3792
+ ${YELLOW}${BOLD3}${top}
3076
3793
  ${r1}
3077
3794
  ${r2}
3078
- ${bot}${RESET2}`);
3795
+ ${bot}${RESET3}`);
3079
3796
  }
3080
3797
  async function main() {
3081
3798
  await runMain(rootCommand);
3082
3799
  if (process.exitCode !== void 0 && process.exitCode !== 0) return;
3083
3800
  try {
3084
- const latestVersion = await checkForUpdate("0.1.3");
3801
+ const latestVersion = await checkForUpdate("0.4.0");
3085
3802
  if (latestVersion) {
3086
- printUpdateBox("0.1.3", latestVersion);
3803
+ printUpdateBox("0.4.0", latestVersion);
3087
3804
  }
3088
3805
  } catch {
3089
3806
  }