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/README.md +43 -10
- package/dist/index.mjs +939 -222
- package/package.json +11 -2
- package/registry/latest.json +6 -4
- package/registry/policy.yaml +1 -0
- package/registry/profiles/base/profile.yaml +1 -0
- package/registry/v0.1.4.json +253 -0
- package/registry/v0.2.0.json +255 -0
- package/registry/v0.3.0.json +255 -0
- package/registry/v0.4.0.json +255 -0
- package/schemas/config.schema.json +1 -1
- package/schemas/manifest.schema.json +1 -1
- package/schemas/profile.schema.json +5 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1272
|
-
if (manifest.assets[
|
|
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
|
|
1276
|
-
if (manifest.assets[
|
|
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
|
|
1280
|
-
if (manifest.assets[
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1673
|
+
const cachedPath = path12.join(cacheDir, assetPath);
|
|
1460
1674
|
try {
|
|
1461
|
-
const
|
|
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.
|
|
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:
|
|
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) :
|
|
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
|
|
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) ??
|
|
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,
|
|
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.
|
|
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
|
|
2631
|
+
import path16 from "path";
|
|
2312
2632
|
|
|
2313
2633
|
// src/core/overlay.ts
|
|
2314
2634
|
init_fs();
|
|
2315
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2750
|
+
const resolvedCwd = path16.resolve(cwd);
|
|
2431
2751
|
for (const [filePath] of managedEntries) {
|
|
2432
|
-
const absPath =
|
|
2433
|
-
if (!absPath.startsWith(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(
|
|
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
|
|
2862
|
+
import path17 from "path";
|
|
2543
2863
|
import { promises as fs5 } from "fs";
|
|
2544
|
-
import
|
|
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 =
|
|
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
|
-
|
|
2598
|
-
|
|
2599
|
-
logger.warn("No lockfile found \u2014 nothing to clean");
|
|
2923
|
+
if (isGlobal) {
|
|
2924
|
+
await runGlobalClean(dryRun, target);
|
|
2600
2925
|
return;
|
|
2601
2926
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
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
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
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
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
-
|
|
2636
|
-
|
|
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:
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
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
|
-
|
|
2660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
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
|
|
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
|
|
2705
|
-
return
|
|
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
|
|
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
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
"
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
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
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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(
|
|
3245
|
+
const hookExists = await exists(def.absolutePath);
|
|
2749
3246
|
if (hookExists) {
|
|
2750
|
-
const existing = await fs6.readFile(
|
|
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(
|
|
3256
|
+
await writeFile(def.absolutePath, def.content);
|
|
2760
3257
|
try {
|
|
2761
|
-
await fs6.chmod(
|
|
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
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
-
|
|
2776
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
3751
|
+
var rootCommand = defineCommand17({
|
|
3036
3752
|
meta: {
|
|
3037
3753
|
name: "asdm",
|
|
3038
|
-
version: "0.
|
|
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
|
|
3062
|
-
const
|
|
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}${
|
|
3792
|
+
${YELLOW}${BOLD3}${top}
|
|
3076
3793
|
${r1}
|
|
3077
3794
|
${r2}
|
|
3078
|
-
${bot}${
|
|
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.
|
|
3801
|
+
const latestVersion = await checkForUpdate("0.4.0");
|
|
3085
3802
|
if (latestVersion) {
|
|
3086
|
-
printUpdateBox("0.
|
|
3803
|
+
printUpdateBox("0.4.0", latestVersion);
|
|
3087
3804
|
}
|
|
3088
3805
|
} catch {
|
|
3089
3806
|
}
|