@hanv89/azure-arch-skill 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/_shared.js +284 -0
- package/dist/adapters/claude-code.js +32 -262
- package/dist/adapters/codex.js +163 -0
- package/dist/adapters/cursor.js +147 -0
- package/dist/adapters/registry.js +16 -0
- package/dist/index.js +5 -9
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.withFatalReturn = exports.stripFrontmatter = exports.parseFrontmatter = exports.fetchManifest = exports.headOk = exports.fetchText = exports.fetchWithTimeout = exports.safeResolveTarget = exports.baseUrl = exports.USER_AGENT = exports.FETCH_TIMEOUT_MS = exports.CANARY_ICON_PATH = exports.MANIFEST_PATH = exports.SKILL_NAME = exports.DEFAULT_BASE_RAW_URL = void 0;
|
|
30
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
31
|
+
const path = __importStar(require("node:path"));
|
|
32
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
33
|
+
// Shared adapter plumbing. Helpers here must be agent-agnostic — anything
|
|
34
|
+
// Claude-Code-specific (default install path, allowed root) lives in the
|
|
35
|
+
// adapter file that imports from here. Codex / Cursor / future adapters
|
|
36
|
+
// re-use these helpers via the same import path.
|
|
37
|
+
exports.DEFAULT_BASE_RAW_URL = "https://raw.githubusercontent.com/hanv89/azure-icons-for-architecture-diagrams/main";
|
|
38
|
+
// SKILL_NAME must stay in lockstep with dist/skill/SKILL.md frontmatter `name`.
|
|
39
|
+
// Renaming the skill is a breaking change requiring a coordinated CLI release;
|
|
40
|
+
// existing installs become un-uninstallable until users upgrade the CLI
|
|
41
|
+
// (uninstall's allow-list refuses folders whose SKILL.md `name` differs).
|
|
42
|
+
exports.SKILL_NAME = "azure-architecture-diagram";
|
|
43
|
+
// Fetched at install/update time from dist/skill/manifest.json. files[0] MUST
|
|
44
|
+
// be SKILL.md so the frontmatter precheck has a stable target.
|
|
45
|
+
exports.MANIFEST_PATH = "dist/skill/manifest.json";
|
|
46
|
+
exports.CANARY_ICON_PATH = "dist/Azure/Compute/AzureVirtualMachine.png";
|
|
47
|
+
exports.FETCH_TIMEOUT_MS = 30_000;
|
|
48
|
+
exports.USER_AGENT = `azure-arch-skill/${package_json_1.default.version}`;
|
|
49
|
+
const ALLOWED_BASE_URL_HOSTS = new Set(["raw.githubusercontent.com"]);
|
|
50
|
+
const ALLOWED_BASE_URL_PATH_PREFIX = "/hanv89/azure-icons-for-architecture-diagrams/";
|
|
51
|
+
function baseUrl() {
|
|
52
|
+
const override = process.env.AZURE_ARCH_SKILL_BASE_URL;
|
|
53
|
+
if (!override)
|
|
54
|
+
return exports.DEFAULT_BASE_RAW_URL;
|
|
55
|
+
let u;
|
|
56
|
+
try {
|
|
57
|
+
u = new URL(override);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`AZURE_ARCH_SKILL_BASE_URL is not a valid URL: ${override}`);
|
|
61
|
+
}
|
|
62
|
+
if (u.protocol !== "https:") {
|
|
63
|
+
throw new Error(`AZURE_ARCH_SKILL_BASE_URL must use https; got ${u.protocol}`);
|
|
64
|
+
}
|
|
65
|
+
if (!ALLOWED_BASE_URL_HOSTS.has(u.hostname)) {
|
|
66
|
+
throw new Error(`AZURE_ARCH_SKILL_BASE_URL host '${u.hostname}' not in allow-list (${[...ALLOWED_BASE_URL_HOSTS].join(", ")})`);
|
|
67
|
+
}
|
|
68
|
+
if (!u.pathname.startsWith(ALLOWED_BASE_URL_PATH_PREFIX)) {
|
|
69
|
+
throw new Error(`AZURE_ARCH_SKILL_BASE_URL path must start with ${ALLOWED_BASE_URL_PATH_PREFIX}`);
|
|
70
|
+
}
|
|
71
|
+
process.stderr.write(`warn: AZURE_ARCH_SKILL_BASE_URL override active: ${override}\n`);
|
|
72
|
+
return override.replace(/\/$/, "");
|
|
73
|
+
}
|
|
74
|
+
exports.baseUrl = baseUrl;
|
|
75
|
+
let envTargetRootWarned = false;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve `target` and assert it lives inside an allowed root. Resolution
|
|
78
|
+
* follows symlinks (via fs.realpath on the deepest existing ancestor) so
|
|
79
|
+
* a symlink inside an allowed root that points outside cannot bypass the
|
|
80
|
+
* check.
|
|
81
|
+
*
|
|
82
|
+
* `defaultAllowedRoot` is supplied by the adapter (e.g. `~/.claude` for the
|
|
83
|
+
* Claude Code adapter, `~/.codex` for a future Codex adapter). Setting the
|
|
84
|
+
* `AZURE_ARCH_SKILL_TARGET_ROOT` env var widens the allow-list to include
|
|
85
|
+
* that root (intended for validation/CI use against a `mktemp -d` directory).
|
|
86
|
+
* Production users should never set the env var.
|
|
87
|
+
*
|
|
88
|
+
* `displayName` controls how the default root appears in error messages
|
|
89
|
+
* when the check fails (e.g. `~/.claude` instead of `/home/user/.claude`).
|
|
90
|
+
* Defaults to the resolved absolute path.
|
|
91
|
+
*/
|
|
92
|
+
async function safeResolveTarget(target, defaultAllowedRoot, displayName = defaultAllowedRoot) {
|
|
93
|
+
const lexicallyResolved = path.resolve(target);
|
|
94
|
+
let probe = lexicallyResolved;
|
|
95
|
+
let realProbe = null;
|
|
96
|
+
while (true) {
|
|
97
|
+
try {
|
|
98
|
+
realProbe = await fs.realpath(probe);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const code = err.code;
|
|
103
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
104
|
+
throw err;
|
|
105
|
+
const parent = path.dirname(probe);
|
|
106
|
+
if (parent === probe) {
|
|
107
|
+
throw new Error(`unable to resolve target ${target}`);
|
|
108
|
+
}
|
|
109
|
+
probe = parent;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const tail = lexicallyResolved.slice(probe.length);
|
|
113
|
+
const realResolved = path.resolve(realProbe + tail);
|
|
114
|
+
const explicit = process.env.AZURE_ARCH_SKILL_TARGET_ROOT;
|
|
115
|
+
if (explicit && !envTargetRootWarned) {
|
|
116
|
+
process.stderr.write(`warn: AZURE_ARCH_SKILL_TARGET_ROOT override active: ${explicit}\n`);
|
|
117
|
+
envTargetRootWarned = true;
|
|
118
|
+
}
|
|
119
|
+
const allowedRoots = [
|
|
120
|
+
path.resolve(defaultAllowedRoot),
|
|
121
|
+
explicit ? path.resolve(explicit) : null,
|
|
122
|
+
].filter((r) => r !== null);
|
|
123
|
+
const inside = allowedRoots.some(root => realResolved === root || realResolved.startsWith(root + path.sep));
|
|
124
|
+
if (!inside) {
|
|
125
|
+
const allowList = `${displayName}${explicit ? `, $AZURE_ARCH_SKILL_TARGET_ROOT=${explicit}` : ""}`;
|
|
126
|
+
throw new Error(`refusing to operate on ${realResolved} (resolved from ${target}) - outside allowed roots (${allowList})`);
|
|
127
|
+
}
|
|
128
|
+
return realResolved;
|
|
129
|
+
}
|
|
130
|
+
exports.safeResolveTarget = safeResolveTarget;
|
|
131
|
+
/**
|
|
132
|
+
* Fetch with timeout and 2-retry exponential backoff on transient 5xx
|
|
133
|
+
* responses. Used by `fetchText` and `headOk`; both inherit the retry
|
|
134
|
+
* behavior. The 2-retry default was added to absorb transient 5xx upstream
|
|
135
|
+
* errors — future agent adapters reusing this helper get the retry path
|
|
136
|
+
* for free.
|
|
137
|
+
*
|
|
138
|
+
* Backoff schedule: 500ms after attempt 0, 1s after attempt 1, 2s after
|
|
139
|
+
* attempt 2. Network errors (AbortError, DNS failures) re-throw only
|
|
140
|
+
* after the final attempt.
|
|
141
|
+
*
|
|
142
|
+
* @internal — exported only so unit tests can mock `globalThis.fetch`
|
|
143
|
+
* around it. Not part of the public adapter API.
|
|
144
|
+
*/
|
|
145
|
+
async function fetchWithTimeout(url, init = {}, retries = 2) {
|
|
146
|
+
let lastError;
|
|
147
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
148
|
+
const ctrl = new AbortController();
|
|
149
|
+
const t = setTimeout(() => ctrl.abort(), exports.FETCH_TIMEOUT_MS);
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(url, {
|
|
152
|
+
...init,
|
|
153
|
+
signal: ctrl.signal,
|
|
154
|
+
headers: { ...(init.headers || {}), "User-Agent": exports.USER_AGENT },
|
|
155
|
+
});
|
|
156
|
+
if (res.status < 500 || attempt === retries) {
|
|
157
|
+
return res;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
lastError = e;
|
|
162
|
+
if (attempt === retries)
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
clearTimeout(t);
|
|
167
|
+
}
|
|
168
|
+
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
|
|
169
|
+
}
|
|
170
|
+
throw lastError ?? new Error("fetchWithTimeout: exhausted retries");
|
|
171
|
+
}
|
|
172
|
+
exports.fetchWithTimeout = fetchWithTimeout;
|
|
173
|
+
async function fetchText(url) {
|
|
174
|
+
const res = await fetchWithTimeout(url);
|
|
175
|
+
if (!res.ok)
|
|
176
|
+
throw new Error(`fetch ${url} returned HTTP ${res.status}`);
|
|
177
|
+
return res.text();
|
|
178
|
+
}
|
|
179
|
+
exports.fetchText = fetchText;
|
|
180
|
+
async function headOk(url) {
|
|
181
|
+
const res = await fetchWithTimeout(url, { method: "HEAD" });
|
|
182
|
+
return res.ok;
|
|
183
|
+
}
|
|
184
|
+
exports.headOk = headOk;
|
|
185
|
+
/**
|
|
186
|
+
* Fetch + parse the bundle manifest. Validates required fields and the
|
|
187
|
+
* SKILL.md-at-index-0 invariant. Throws with a clear error on any issue —
|
|
188
|
+
* callers should not silently fall back.
|
|
189
|
+
*/
|
|
190
|
+
async function fetchManifest(base) {
|
|
191
|
+
const url = `${base}/${exports.MANIFEST_PATH}`;
|
|
192
|
+
const body = await fetchText(url);
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(body);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
throw new Error(`manifest ${url} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
199
|
+
}
|
|
200
|
+
if (!parsed || typeof parsed !== "object") {
|
|
201
|
+
throw new Error(`manifest ${url} did not parse to an object`);
|
|
202
|
+
}
|
|
203
|
+
const m = parsed;
|
|
204
|
+
for (const key of ["name", "version", "requires_icons"]) {
|
|
205
|
+
if (typeof m[key] !== "string" || !m[key]) {
|
|
206
|
+
throw new Error(`manifest ${url} missing required field: ${key}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(m.files) || m.files.length === 0) {
|
|
210
|
+
throw new Error(`manifest ${url} files[] missing or empty`);
|
|
211
|
+
}
|
|
212
|
+
for (const [i, f] of m.files.entries()) {
|
|
213
|
+
if (!f || typeof f !== "object") {
|
|
214
|
+
throw new Error(`manifest ${url} files[${i}] not an object`);
|
|
215
|
+
}
|
|
216
|
+
for (const key of ["src", "dest", "role"]) {
|
|
217
|
+
if (typeof f[key] !== "string") {
|
|
218
|
+
throw new Error(`manifest ${url} files[${i}].${key} missing`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (m.files[0].dest !== "SKILL.md" || m.files[0].role !== "skill") {
|
|
223
|
+
throw new Error(`manifest ${url} files[0] must be SKILL.md (role=skill); got dest=${m.files[0].dest} role=${m.files[0].role}`);
|
|
224
|
+
}
|
|
225
|
+
return m;
|
|
226
|
+
}
|
|
227
|
+
exports.fetchManifest = fetchManifest;
|
|
228
|
+
/**
|
|
229
|
+
* Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
|
|
230
|
+
* pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
|
|
231
|
+
* mappings, lists, comments after `#`, and YAML null/booleans are NOT handled
|
|
232
|
+
* and may silently mis-parse.
|
|
233
|
+
*
|
|
234
|
+
* Intentional: SKILL.md frontmatter currently has 4 single-line scalar keys
|
|
235
|
+
* (name, description, version, requires_icons). Swap in `js-yaml` when the
|
|
236
|
+
* format grows beyond that.
|
|
237
|
+
*/
|
|
238
|
+
function parseFrontmatter(md) {
|
|
239
|
+
const text = md.replace(/^/, "");
|
|
240
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
241
|
+
if (!match)
|
|
242
|
+
return {};
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
245
|
+
const kv = line.match(/^(\w+):\s*(.*?)\s*$/);
|
|
246
|
+
if (!kv)
|
|
247
|
+
continue;
|
|
248
|
+
const [, key] = kv;
|
|
249
|
+
let rawValue = kv[2];
|
|
250
|
+
if (!rawValue.startsWith('"') && !rawValue.startsWith("'")) {
|
|
251
|
+
rawValue = rawValue.replace(/\s+#.*$/, "");
|
|
252
|
+
}
|
|
253
|
+
const value = rawValue.replace(/^["']|["']$/g, "");
|
|
254
|
+
if (key === "name" || key === "version" || key === "requires_icons") {
|
|
255
|
+
out[key] = value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
261
|
+
/**
|
|
262
|
+
* Strip the leading `---\n...\n---\n` YAML frontmatter block from a markdown
|
|
263
|
+
* string. Returns the body unchanged if no frontmatter is detected.
|
|
264
|
+
*
|
|
265
|
+
* Used by adapters that re-render the upstream SKILL.md for a different host
|
|
266
|
+
* (e.g. Cursor's `.mdc` rule files, which carry their own frontmatter shape
|
|
267
|
+
* and embed the SKILL.md body without its original frontmatter).
|
|
268
|
+
*/
|
|
269
|
+
function stripFrontmatter(md) {
|
|
270
|
+
const text = md.replace(/^/, "");
|
|
271
|
+
const match = text.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
272
|
+
return match ? text.slice(match[0].length) : text;
|
|
273
|
+
}
|
|
274
|
+
exports.stripFrontmatter = stripFrontmatter;
|
|
275
|
+
async function withFatalReturn(fn) {
|
|
276
|
+
try {
|
|
277
|
+
return await fn();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
exports.withFatalReturn = withFatalReturn;
|
|
@@ -22,243 +22,27 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
22
22
|
__setModuleDefault(result, mod);
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
26
|
exports.claudeCodeAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
30
27
|
const fs = __importStar(require("node:fs/promises"));
|
|
31
28
|
const path = __importStar(require("node:path"));
|
|
32
29
|
const os = __importStar(require("node:os"));
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// (uninstall's allow-list refuses folders whose SKILL.md `name` differs).
|
|
39
|
-
const SKILL_NAME = "azure-architecture-diagram";
|
|
40
|
-
// Fetched at install/update time from dist/skill/manifest.json. files[0] MUST
|
|
41
|
-
// be SKILL.md so the frontmatter precheck has a stable target.
|
|
42
|
-
const MANIFEST_PATH = "dist/skill/manifest.json";
|
|
43
|
-
const CANARY_ICON_PATH = "dist/Azure/Compute/AzureVirtualMachine.png";
|
|
44
|
-
const FETCH_TIMEOUT_MS = 30_000;
|
|
45
|
-
const USER_AGENT = `azure-arch-skill/${package_json_1.default.version}`;
|
|
46
|
-
const ALLOWED_BASE_URL_HOSTS = new Set(["raw.githubusercontent.com"]);
|
|
47
|
-
const ALLOWED_BASE_URL_PATH_PREFIX = "/hanv89/azure-icons-for-architecture-diagrams/";
|
|
48
|
-
function baseUrl() {
|
|
49
|
-
const override = process.env.AZURE_ARCH_SKILL_BASE_URL;
|
|
50
|
-
if (!override)
|
|
51
|
-
return DEFAULT_BASE_RAW_URL;
|
|
52
|
-
let u;
|
|
53
|
-
try {
|
|
54
|
-
u = new URL(override);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
throw new Error(`AZURE_ARCH_SKILL_BASE_URL is not a valid URL: ${override}`);
|
|
58
|
-
}
|
|
59
|
-
if (u.protocol !== "https:") {
|
|
60
|
-
throw new Error(`AZURE_ARCH_SKILL_BASE_URL must use https; got ${u.protocol}`);
|
|
61
|
-
}
|
|
62
|
-
if (!ALLOWED_BASE_URL_HOSTS.has(u.hostname)) {
|
|
63
|
-
throw new Error(`AZURE_ARCH_SKILL_BASE_URL host '${u.hostname}' not in allow-list (${[...ALLOWED_BASE_URL_HOSTS].join(", ")})`);
|
|
64
|
-
}
|
|
65
|
-
if (!u.pathname.startsWith(ALLOWED_BASE_URL_PATH_PREFIX)) {
|
|
66
|
-
throw new Error(`AZURE_ARCH_SKILL_BASE_URL path must start with ${ALLOWED_BASE_URL_PATH_PREFIX}`);
|
|
67
|
-
}
|
|
68
|
-
process.stderr.write(`warn: AZURE_ARCH_SKILL_BASE_URL override active: ${override}\n`);
|
|
69
|
-
return override.replace(/\/$/, "");
|
|
30
|
+
const _shared_1 = require("./_shared");
|
|
31
|
+
Object.defineProperty(exports, "fetchWithTimeout", { enumerable: true, get: function () { return _shared_1.fetchWithTimeout; } });
|
|
32
|
+
Object.defineProperty(exports, "parseFrontmatter", { enumerable: true, get: function () { return _shared_1.parseFrontmatter; } });
|
|
33
|
+
function claudeRootDir() {
|
|
34
|
+
return path.join(os.homedir(), ".claude");
|
|
70
35
|
}
|
|
71
36
|
function defaultTarget() {
|
|
72
|
-
return path.join(
|
|
37
|
+
return path.join(claudeRootDir(), "skills", _shared_1.SKILL_NAME);
|
|
73
38
|
}
|
|
74
39
|
function defaultSkillsRoot() {
|
|
75
|
-
return path.join(
|
|
76
|
-
}
|
|
77
|
-
let envTargetRootWarned = false;
|
|
78
|
-
/**
|
|
79
|
-
* Resolve `target` and assert it lives inside an allowed root. Resolution
|
|
80
|
-
* follows symlinks (via fs.realpath on the deepest existing ancestor) so
|
|
81
|
-
* a symlink inside an allowed root that points outside cannot bypass the
|
|
82
|
-
* check.
|
|
83
|
-
*
|
|
84
|
-
* The default allow-list is `~/.claude/` only. Setting the
|
|
85
|
-
* `AZURE_ARCH_SKILL_TARGET_ROOT` env var widens it to include that root
|
|
86
|
-
* (intended for validation/CI use against a `mktemp -d` directory).
|
|
87
|
-
* Production users should never set the env var.
|
|
88
|
-
*/
|
|
89
|
-
async function safeResolveTarget(target) {
|
|
90
|
-
const lexicallyResolved = path.resolve(target);
|
|
91
|
-
// Walk up to the deepest existing ancestor and realpath it — install creates
|
|
92
|
-
// a not-yet-existing target so we can't realpath the leaf directly.
|
|
93
|
-
let probe = lexicallyResolved;
|
|
94
|
-
let realProbe = null;
|
|
95
|
-
while (true) {
|
|
96
|
-
try {
|
|
97
|
-
realProbe = await fs.realpath(probe);
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
const code = err.code;
|
|
102
|
-
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
103
|
-
throw err;
|
|
104
|
-
const parent = path.dirname(probe);
|
|
105
|
-
if (parent === probe) {
|
|
106
|
-
throw new Error(`unable to resolve target ${target}`);
|
|
107
|
-
}
|
|
108
|
-
probe = parent;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const tail = lexicallyResolved.slice(probe.length);
|
|
112
|
-
const realResolved = path.resolve(realProbe + tail);
|
|
113
|
-
const home = os.homedir();
|
|
114
|
-
const explicit = process.env.AZURE_ARCH_SKILL_TARGET_ROOT;
|
|
115
|
-
if (explicit && !envTargetRootWarned) {
|
|
116
|
-
process.stderr.write(`warn: AZURE_ARCH_SKILL_TARGET_ROOT override active: ${explicit}\n`);
|
|
117
|
-
envTargetRootWarned = true;
|
|
118
|
-
}
|
|
119
|
-
const allowedRoots = [
|
|
120
|
-
path.resolve(path.join(home, ".claude")),
|
|
121
|
-
explicit ? path.resolve(explicit) : null,
|
|
122
|
-
].filter((r) => r !== null);
|
|
123
|
-
const inside = allowedRoots.some(root => realResolved === root || realResolved.startsWith(root + path.sep));
|
|
124
|
-
if (!inside) {
|
|
125
|
-
const allowList = `~/.claude${explicit ? `, $AZURE_ARCH_SKILL_TARGET_ROOT=${explicit}` : ""}`;
|
|
126
|
-
throw new Error(`refusing to operate on ${realResolved} (resolved from ${target}) - outside allowed roots (${allowList})`);
|
|
127
|
-
}
|
|
128
|
-
return realResolved;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Fetch with timeout and 2-retry exponential backoff on transient 5xx
|
|
132
|
-
* responses. Used by `fetchText` and `headOk`; both inherit the retry
|
|
133
|
-
* behavior. The 2-retry default was added to absorb transient 5xx
|
|
134
|
-
* upstream errors — future agent adapters reusing this helper get the
|
|
135
|
-
* retry path for free.
|
|
136
|
-
*
|
|
137
|
-
* Backoff schedule: 500ms after attempt 0, 1s after attempt 1, 2s after
|
|
138
|
-
* attempt 2. Network errors (AbortError, DNS failures) re-throw only
|
|
139
|
-
* after the final attempt.
|
|
140
|
-
*
|
|
141
|
-
* @internal — exported only so unit tests can mock `globalThis.fetch`
|
|
142
|
-
* around it. Not part of the public adapter API.
|
|
143
|
-
*/
|
|
144
|
-
async function fetchWithTimeout(url, init = {}, retries = 2) {
|
|
145
|
-
let lastError;
|
|
146
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
147
|
-
const ctrl = new AbortController();
|
|
148
|
-
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
149
|
-
try {
|
|
150
|
-
const res = await fetch(url, {
|
|
151
|
-
...init,
|
|
152
|
-
signal: ctrl.signal,
|
|
153
|
-
headers: { ...(init.headers || {}), "User-Agent": USER_AGENT },
|
|
154
|
-
});
|
|
155
|
-
if (res.status < 500 || attempt === retries) {
|
|
156
|
-
return res;
|
|
157
|
-
}
|
|
158
|
-
// 5xx with retries remaining: fall through to backoff.
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
lastError = e;
|
|
162
|
-
if (attempt === retries)
|
|
163
|
-
throw e;
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
clearTimeout(t);
|
|
167
|
-
}
|
|
168
|
-
// Exponential backoff: 500ms, 1s, 2s.
|
|
169
|
-
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
|
|
170
|
-
}
|
|
171
|
-
// Unreachable: the loop body always returns or throws on the final attempt.
|
|
172
|
-
throw lastError ?? new Error("fetchWithTimeout: exhausted retries");
|
|
173
|
-
}
|
|
174
|
-
exports.fetchWithTimeout = fetchWithTimeout;
|
|
175
|
-
async function fetchText(url) {
|
|
176
|
-
const res = await fetchWithTimeout(url);
|
|
177
|
-
if (!res.ok)
|
|
178
|
-
throw new Error(`fetch ${url} returned HTTP ${res.status}`);
|
|
179
|
-
return res.text();
|
|
40
|
+
return path.join(claudeRootDir(), "skills");
|
|
180
41
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
42
|
+
const CLAUDE_ROOT_DISPLAY = "~/.claude";
|
|
43
|
+
async function resolveTarget(target) {
|
|
44
|
+
return (0, _shared_1.safeResolveTarget)(target, claudeRootDir(), CLAUDE_ROOT_DISPLAY);
|
|
184
45
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Fetch + parse the bundle manifest. Validates required fields and the
|
|
187
|
-
* SKILL.md-at-index-0 invariant. Throws with a clear error on any issue —
|
|
188
|
-
* callers should not silently fall back.
|
|
189
|
-
*/
|
|
190
|
-
async function fetchManifest(base) {
|
|
191
|
-
const url = `${base}/${MANIFEST_PATH}`;
|
|
192
|
-
const body = await fetchText(url);
|
|
193
|
-
let parsed;
|
|
194
|
-
try {
|
|
195
|
-
parsed = JSON.parse(body);
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
throw new Error(`manifest ${url} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
199
|
-
}
|
|
200
|
-
if (!parsed || typeof parsed !== "object") {
|
|
201
|
-
throw new Error(`manifest ${url} did not parse to an object`);
|
|
202
|
-
}
|
|
203
|
-
const m = parsed;
|
|
204
|
-
for (const key of ["name", "version", "requires_icons"]) {
|
|
205
|
-
if (typeof m[key] !== "string" || !m[key]) {
|
|
206
|
-
throw new Error(`manifest ${url} missing required field: ${key}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!Array.isArray(m.files) || m.files.length === 0) {
|
|
210
|
-
throw new Error(`manifest ${url} files[] missing or empty`);
|
|
211
|
-
}
|
|
212
|
-
for (const [i, f] of m.files.entries()) {
|
|
213
|
-
if (!f || typeof f !== "object") {
|
|
214
|
-
throw new Error(`manifest ${url} files[${i}] not an object`);
|
|
215
|
-
}
|
|
216
|
-
for (const key of ["src", "dest", "role"]) {
|
|
217
|
-
if (typeof f[key] !== "string") {
|
|
218
|
-
throw new Error(`manifest ${url} files[${i}].${key} missing`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (m.files[0].dest !== "SKILL.md" || m.files[0].role !== "skill") {
|
|
223
|
-
throw new Error(`manifest ${url} files[0] must be SKILL.md (role=skill); got dest=${m.files[0].dest} role=${m.files[0].role}`);
|
|
224
|
-
}
|
|
225
|
-
return m;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
|
|
229
|
-
* pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
|
|
230
|
-
* mappings, lists, comments after `#`, and YAML null/booleans are NOT handled
|
|
231
|
-
* and may silently mis-parse.
|
|
232
|
-
*
|
|
233
|
-
* Intentional: SKILL.md frontmatter currently has 4 single-line scalar keys
|
|
234
|
-
* (name, description, version, requires_icons). Swap in `js-yaml` when the
|
|
235
|
-
* format grows beyond that.
|
|
236
|
-
*/
|
|
237
|
-
function parseFrontmatter(md) {
|
|
238
|
-
// Strip optional UTF-8 BOM (some editors emit it on save).
|
|
239
|
-
const text = md.replace(/^/, "");
|
|
240
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
241
|
-
if (!match)
|
|
242
|
-
return {};
|
|
243
|
-
const out = {};
|
|
244
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
245
|
-
const kv = line.match(/^(\w+):\s*(.*?)\s*$/);
|
|
246
|
-
if (!kv)
|
|
247
|
-
continue;
|
|
248
|
-
const [, key] = kv;
|
|
249
|
-
let rawValue = kv[2];
|
|
250
|
-
// Strip trailing ` # comment` from unquoted scalars.
|
|
251
|
-
if (!rawValue.startsWith('"') && !rawValue.startsWith("'")) {
|
|
252
|
-
rawValue = rawValue.replace(/\s+#.*$/, "");
|
|
253
|
-
}
|
|
254
|
-
const value = rawValue.replace(/^["']|["']$/g, "");
|
|
255
|
-
if (key === "name" || key === "version" || key === "requires_icons") {
|
|
256
|
-
out[key] = value;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
261
|
-
exports.parseFrontmatter = parseFrontmatter;
|
|
262
46
|
/**
|
|
263
47
|
* Returns true iff `dir` contains a SKILL.md whose frontmatter `name` field
|
|
264
48
|
* matches our skill. Used by uninstall to refuse deleting paths that aren't
|
|
@@ -267,37 +51,24 @@ exports.parseFrontmatter = parseFrontmatter;
|
|
|
267
51
|
async function isOurSkillDir(dir) {
|
|
268
52
|
try {
|
|
269
53
|
const skillMd = await fs.readFile(path.join(dir, "SKILL.md"), "utf8");
|
|
270
|
-
const fm = parseFrontmatter(skillMd);
|
|
271
|
-
return fm.name === SKILL_NAME;
|
|
54
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
55
|
+
return fm.name === _shared_1.SKILL_NAME;
|
|
272
56
|
}
|
|
273
57
|
catch {
|
|
274
58
|
return false;
|
|
275
59
|
}
|
|
276
60
|
}
|
|
277
|
-
async function withFatalReturn(fn) {
|
|
278
|
-
try {
|
|
279
|
-
return await fn();
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
283
|
-
return 1;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
61
|
async function install(opts) {
|
|
287
|
-
return withFatalReturn(async () => {
|
|
288
|
-
const target = await
|
|
289
|
-
const base = baseUrl();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== SKILL_NAME) {
|
|
293
|
-
throw new Error(`refusing to install at ${target} - target basename must be '${SKILL_NAME}' (default ~/.claude/skills/${SKILL_NAME}/). Set AZURE_ARCH_SKILL_TARGET_ROOT to install into a custom test root.`);
|
|
62
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
63
|
+
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
64
|
+
const base = (0, _shared_1.baseUrl)();
|
|
65
|
+
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== _shared_1.SKILL_NAME) {
|
|
66
|
+
throw new Error(`refusing to install at ${target} - target basename must be '${_shared_1.SKILL_NAME}' (default ~/.claude/skills/${_shared_1.SKILL_NAME}/). Set AZURE_ARCH_SKILL_TARGET_ROOT to install into a custom test root.`);
|
|
294
67
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
throw new Error(`manifest name mismatch: expected '${SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
68
|
+
const manifest = await (0, _shared_1.fetchManifest)(base);
|
|
69
|
+
if (manifest.name !== _shared_1.SKILL_NAME) {
|
|
70
|
+
throw new Error(`manifest name mismatch: expected '${_shared_1.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
299
71
|
}
|
|
300
|
-
// Detect partial vs complete prior installs across manifest.files.
|
|
301
72
|
const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
|
|
302
73
|
dest,
|
|
303
74
|
exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
|
|
@@ -310,32 +81,31 @@ async function install(opts) {
|
|
|
310
81
|
: `${target} contains a partial install (${presence.filter(p => !p.exists).map(p => p.dest).join(", ")} missing). Run 'azure-arch-skill update --agent=claude-code' to repair.`);
|
|
311
82
|
}
|
|
312
83
|
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
313
|
-
const skillMd = await fetchText(skillUrl);
|
|
314
|
-
const fm = parseFrontmatter(skillMd);
|
|
84
|
+
const skillMd = await (0, _shared_1.fetchText)(skillUrl);
|
|
85
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
315
86
|
if (!fm.requires_icons) {
|
|
316
87
|
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
317
88
|
}
|
|
318
|
-
const canaryUrl = `${base}/${CANARY_ICON_PATH}`;
|
|
319
|
-
const reachable = await headOk(canaryUrl);
|
|
89
|
+
const canaryUrl = `${base}/${_shared_1.CANARY_ICON_PATH}`;
|
|
90
|
+
const reachable = await (0, _shared_1.headOk)(canaryUrl);
|
|
320
91
|
if (!reachable) {
|
|
321
92
|
throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${fm.requires_icons}; this release verifies reachability only, strict semver match planned)`);
|
|
322
93
|
}
|
|
323
|
-
// Mkdir the parent of every bundle dest so future deeper-nested entries work.
|
|
324
94
|
for (const { dest } of manifest.files) {
|
|
325
95
|
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
326
96
|
}
|
|
327
97
|
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
328
98
|
for (const { src, dest } of manifest.files.slice(1)) {
|
|
329
|
-
const body = await fetchText(`${base}/${src}`);
|
|
99
|
+
const body = await (0, _shared_1.fetchText)(`${base}/${src}`);
|
|
330
100
|
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
331
101
|
}
|
|
332
|
-
process.stdout.write(`installed ${SKILL_NAME} to ${target}\n`);
|
|
102
|
+
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${target}\n`);
|
|
333
103
|
return 0;
|
|
334
104
|
});
|
|
335
105
|
}
|
|
336
106
|
async function uninstall(opts) {
|
|
337
|
-
return withFatalReturn(async () => {
|
|
338
|
-
const target = await
|
|
107
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
108
|
+
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
339
109
|
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
340
110
|
if (!exists) {
|
|
341
111
|
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
@@ -356,7 +126,7 @@ async function uninstall(opts) {
|
|
|
356
126
|
}
|
|
357
127
|
throw err;
|
|
358
128
|
}
|
|
359
|
-
process.stdout.write(`uninstalled ${SKILL_NAME} from ${target}\n`);
|
|
129
|
+
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${target}\n`);
|
|
360
130
|
return 0;
|
|
361
131
|
});
|
|
362
132
|
}
|
|
@@ -364,8 +134,8 @@ async function update(opts) {
|
|
|
364
134
|
return install({ ...opts, overwrite: true });
|
|
365
135
|
}
|
|
366
136
|
async function list(opts) {
|
|
367
|
-
return withFatalReturn(async () => {
|
|
368
|
-
const root = await
|
|
137
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
138
|
+
const root = await resolveTarget(opts.target ?? defaultSkillsRoot());
|
|
369
139
|
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
370
140
|
if (!exists) {
|
|
371
141
|
process.stdout.write("(no skills installed)\n");
|
|
@@ -379,7 +149,7 @@ async function list(opts) {
|
|
|
379
149
|
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
380
150
|
try {
|
|
381
151
|
const md = await fs.readFile(skillMdPath, "utf8");
|
|
382
|
-
const fm = parseFrontmatter(md);
|
|
152
|
+
const fm = (0, _shared_1.parseFrontmatter)(md);
|
|
383
153
|
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
384
154
|
}
|
|
385
155
|
catch {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.codexAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
27
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
28
|
+
const path = __importStar(require("node:path"));
|
|
29
|
+
const os = __importStar(require("node:os"));
|
|
30
|
+
const _shared_1 = require("./_shared");
|
|
31
|
+
Object.defineProperty(exports, "fetchWithTimeout", { enumerable: true, get: function () { return _shared_1.fetchWithTimeout; } });
|
|
32
|
+
Object.defineProperty(exports, "parseFrontmatter", { enumerable: true, get: function () { return _shared_1.parseFrontmatter; } });
|
|
33
|
+
function codexRootDir() {
|
|
34
|
+
const explicit = process.env.CODEX_HOME;
|
|
35
|
+
if (explicit)
|
|
36
|
+
return path.resolve(explicit);
|
|
37
|
+
return path.join(os.homedir(), ".codex");
|
|
38
|
+
}
|
|
39
|
+
function defaultTarget() {
|
|
40
|
+
return path.join(codexRootDir(), "skills", _shared_1.SKILL_NAME);
|
|
41
|
+
}
|
|
42
|
+
function defaultSkillsRoot() {
|
|
43
|
+
return path.join(codexRootDir(), "skills");
|
|
44
|
+
}
|
|
45
|
+
function codexRootDisplay() {
|
|
46
|
+
return process.env.CODEX_HOME ? `$CODEX_HOME=${process.env.CODEX_HOME}` : "~/.codex";
|
|
47
|
+
}
|
|
48
|
+
async function resolveTarget(target) {
|
|
49
|
+
return (0, _shared_1.safeResolveTarget)(target, codexRootDir(), codexRootDisplay());
|
|
50
|
+
}
|
|
51
|
+
async function isOurSkillDir(dir) {
|
|
52
|
+
try {
|
|
53
|
+
const skillMd = await fs.readFile(path.join(dir, "SKILL.md"), "utf8");
|
|
54
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
55
|
+
return fm.name === _shared_1.SKILL_NAME;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function install(opts) {
|
|
62
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
63
|
+
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
64
|
+
const base = (0, _shared_1.baseUrl)();
|
|
65
|
+
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== _shared_1.SKILL_NAME) {
|
|
66
|
+
throw new Error(`refusing to install at ${target} - target basename must be '${_shared_1.SKILL_NAME}' (default ${codexRootDisplay()}/skills/${_shared_1.SKILL_NAME}/). Set AZURE_ARCH_SKILL_TARGET_ROOT to install into a custom test root.`);
|
|
67
|
+
}
|
|
68
|
+
const manifest = await (0, _shared_1.fetchManifest)(base);
|
|
69
|
+
if (manifest.name !== _shared_1.SKILL_NAME) {
|
|
70
|
+
throw new Error(`manifest name mismatch: expected '${_shared_1.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
71
|
+
}
|
|
72
|
+
const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
|
|
73
|
+
dest,
|
|
74
|
+
exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
|
|
75
|
+
})));
|
|
76
|
+
const someExist = presence.some(p => p.exists);
|
|
77
|
+
const allExist = presence.every(p => p.exists);
|
|
78
|
+
if (someExist && !opts.overwrite) {
|
|
79
|
+
throw new Error(allExist
|
|
80
|
+
? `${target} already contains an install. Run 'azure-arch-skill update --agent=codex' to refresh.`
|
|
81
|
+
: `${target} contains a partial install (${presence.filter(p => !p.exists).map(p => p.dest).join(", ")} missing). Run 'azure-arch-skill update --agent=codex' to repair.`);
|
|
82
|
+
}
|
|
83
|
+
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
84
|
+
const skillMd = await (0, _shared_1.fetchText)(skillUrl);
|
|
85
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
86
|
+
if (!fm.requires_icons) {
|
|
87
|
+
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
88
|
+
}
|
|
89
|
+
const canaryUrl = `${base}/${_shared_1.CANARY_ICON_PATH}`;
|
|
90
|
+
const reachable = await (0, _shared_1.headOk)(canaryUrl);
|
|
91
|
+
if (!reachable) {
|
|
92
|
+
throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${fm.requires_icons}; this release verifies reachability only, strict semver match planned)`);
|
|
93
|
+
}
|
|
94
|
+
for (const { dest } of manifest.files) {
|
|
95
|
+
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
98
|
+
for (const { src, dest } of manifest.files.slice(1)) {
|
|
99
|
+
const body = await (0, _shared_1.fetchText)(`${base}/${src}`);
|
|
100
|
+
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${target}\n`);
|
|
103
|
+
return 0;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function uninstall(opts) {
|
|
107
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
108
|
+
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
109
|
+
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
110
|
+
if (!exists) {
|
|
111
|
+
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const ours = await isOurSkillDir(target);
|
|
115
|
+
if (!ours) {
|
|
116
|
+
throw new Error(`refusing to remove ${target} - not an azure-architecture-diagram skill folder (no matching SKILL.md). Move/rename the directory or remove it manually if intentional.`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await fs.rm(target, { recursive: true, force: false });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const stillExists = await fs.stat(target).then(() => true).catch(() => false);
|
|
123
|
+
if (stillExists) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${target}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function update(opts) {
|
|
134
|
+
return install({ ...opts, overwrite: true });
|
|
135
|
+
}
|
|
136
|
+
async function list(opts) {
|
|
137
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
138
|
+
const root = await resolveTarget(opts.target ?? defaultSkillsRoot());
|
|
139
|
+
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
140
|
+
if (!exists) {
|
|
141
|
+
process.stdout.write("(no skills installed)\n");
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
145
|
+
const rows = [];
|
|
146
|
+
for (const e of entries) {
|
|
147
|
+
if (!e.isDirectory())
|
|
148
|
+
continue;
|
|
149
|
+
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
150
|
+
try {
|
|
151
|
+
const md = await fs.readFile(skillMdPath, "utf8");
|
|
152
|
+
const fm = (0, _shared_1.parseFrontmatter)(md);
|
|
153
|
+
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// not a skill folder; skip silently
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write(rows.length ? rows.join("\n") + "\n" : "(no skills installed)\n");
|
|
160
|
+
return 0;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
exports.codexAdapter = { install, uninstall, update, list };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.cursorAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
27
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
28
|
+
const path = __importStar(require("node:path"));
|
|
29
|
+
const _shared_1 = require("./_shared");
|
|
30
|
+
Object.defineProperty(exports, "fetchWithTimeout", { enumerable: true, get: function () { return _shared_1.fetchWithTimeout; } });
|
|
31
|
+
Object.defineProperty(exports, "parseFrontmatter", { enumerable: true, get: function () { return _shared_1.parseFrontmatter; } });
|
|
32
|
+
const RULE_BASENAME = "azure-arch-skill.mdc";
|
|
33
|
+
const RULE_DESCRIPTION = "Use this rule when drawing Microsoft Azure or Microsoft Fabric architecture diagrams using PlantUML. " +
|
|
34
|
+
"Triggers on \"draw Azure architecture\", \"create deployment diagram\", \"Lakehouse + Notebook + Warehouse diagram\", " +
|
|
35
|
+
"\"vẽ Azure\", \"PlantUML diagram for [project]\".";
|
|
36
|
+
function defaultTarget() {
|
|
37
|
+
return path.join(process.cwd(), ".cursor", "rules");
|
|
38
|
+
}
|
|
39
|
+
const CWD_DISPLAY = "<cwd>/.cursor/rules";
|
|
40
|
+
async function resolveTarget(target) {
|
|
41
|
+
return (0, _shared_1.safeResolveTarget)(target, process.cwd(), CWD_DISPLAY);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Provenance marker emitted into the rendered .mdc body so `list` / `uninstall`
|
|
45
|
+
* can recognise our rule without re-fetching upstream. The marker is the first
|
|
46
|
+
* non-frontmatter line after the closing `---`.
|
|
47
|
+
*/
|
|
48
|
+
function provenanceMarker(version, requiresIcons) {
|
|
49
|
+
return `<!-- ${_shared_1.SKILL_NAME} v${version} (requires_icons: ${requiresIcons}) -->`;
|
|
50
|
+
}
|
|
51
|
+
const PROVENANCE_RE = new RegExp(`<!--\\s*${_shared_1.SKILL_NAME}\\s+v([^\\s]+)\\s+\\(requires_icons:\\s*([^)]+)\\)\\s*-->`);
|
|
52
|
+
function renderRule(skillBody, version, requiresIcons) {
|
|
53
|
+
return [
|
|
54
|
+
"---",
|
|
55
|
+
`description: ${RULE_DESCRIPTION}`,
|
|
56
|
+
"alwaysApply: false",
|
|
57
|
+
"---",
|
|
58
|
+
"",
|
|
59
|
+
provenanceMarker(version, requiresIcons),
|
|
60
|
+
"",
|
|
61
|
+
skillBody.trimStart(),
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
async function isOurRuleFile(file) {
|
|
65
|
+
try {
|
|
66
|
+
const body = await fs.readFile(file, "utf8");
|
|
67
|
+
const match = body.match(PROVENANCE_RE);
|
|
68
|
+
if (!match)
|
|
69
|
+
return { ours: false };
|
|
70
|
+
return { ours: true, version: match[1] };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { ours: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function install(opts) {
|
|
77
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
78
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
79
|
+
const base = (0, _shared_1.baseUrl)();
|
|
80
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
81
|
+
// Fetch manifest first so we know which file is the canonical SKILL.md
|
|
82
|
+
// and what version / requires_icons to embed in the provenance marker.
|
|
83
|
+
const manifest = await (0, _shared_1.fetchManifest)(base);
|
|
84
|
+
if (manifest.name !== _shared_1.SKILL_NAME) {
|
|
85
|
+
throw new Error(`manifest name mismatch: expected '${_shared_1.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
86
|
+
}
|
|
87
|
+
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
88
|
+
const skillMd = await (0, _shared_1.fetchText)(skillUrl);
|
|
89
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
90
|
+
if (!fm.requires_icons) {
|
|
91
|
+
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
92
|
+
}
|
|
93
|
+
const canaryUrl = `${base}/${_shared_1.CANARY_ICON_PATH}`;
|
|
94
|
+
const reachable = await (0, _shared_1.headOk)(canaryUrl);
|
|
95
|
+
if (!reachable) {
|
|
96
|
+
throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${fm.requires_icons}; this release verifies reachability only, strict semver match planned)`);
|
|
97
|
+
}
|
|
98
|
+
const exists = await fs.stat(ruleFile).then(() => true).catch(() => false);
|
|
99
|
+
if (exists && !opts.overwrite) {
|
|
100
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
101
|
+
throw new Error(probe.ours
|
|
102
|
+
? `${ruleFile} already contains an install. Run 'azure-arch-skill update --agent=cursor' to refresh.`
|
|
103
|
+
: `${ruleFile} exists but is not one of ours (no '${_shared_1.SKILL_NAME}' provenance marker). Move/rename the file or remove it manually if intentional.`);
|
|
104
|
+
}
|
|
105
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
106
|
+
const body = (0, _shared_1.stripFrontmatter)(skillMd);
|
|
107
|
+
const rendered = renderRule(body, fm.version ?? manifest.version, fm.requires_icons);
|
|
108
|
+
await fs.writeFile(ruleFile, rendered, "utf8");
|
|
109
|
+
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${ruleFile}\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function uninstall(opts) {
|
|
114
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
115
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
116
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
117
|
+
const exists = await fs.stat(ruleFile).then(() => true).catch(() => false);
|
|
118
|
+
if (!exists) {
|
|
119
|
+
process.stdout.write(`(nothing to uninstall at ${ruleFile})\n`);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
123
|
+
if (!probe.ours) {
|
|
124
|
+
throw new Error(`refusing to remove ${ruleFile} - missing '${_shared_1.SKILL_NAME}' provenance marker. Move/rename the file or remove it manually if intentional.`);
|
|
125
|
+
}
|
|
126
|
+
await fs.unlink(ruleFile);
|
|
127
|
+
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${ruleFile}\n`);
|
|
128
|
+
return 0;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function update(opts) {
|
|
132
|
+
return install({ ...opts, overwrite: true });
|
|
133
|
+
}
|
|
134
|
+
async function list(opts) {
|
|
135
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
136
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
137
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
138
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
139
|
+
if (!probe.ours) {
|
|
140
|
+
process.stdout.write("(no skills installed)\n");
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(`${_shared_1.SKILL_NAME}\t${probe.version ?? "?"}\n`);
|
|
144
|
+
return 0;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
exports.cursorAdapter = { install, uninstall, update, list };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_AGENTS = exports.ADAPTERS = void 0;
|
|
4
|
+
const claude_code_1 = require("./claude-code");
|
|
5
|
+
const codex_1 = require("./codex");
|
|
6
|
+
const cursor_1 = require("./cursor");
|
|
7
|
+
// Single source of truth for the supported-agent set. Add a new adapter by
|
|
8
|
+
// importing it here and adding one entry below; both `src/index.ts` (CLI
|
|
9
|
+
// dispatch + --agent help text) and `adapters-roundtrip.test.ts` (test loop)
|
|
10
|
+
// read from this map directly.
|
|
11
|
+
exports.ADAPTERS = {
|
|
12
|
+
"claude-code": claude_code_1.claudeCodeAdapter,
|
|
13
|
+
"codex": codex_1.codexAdapter,
|
|
14
|
+
"cursor": cursor_1.cursorAdapter,
|
|
15
|
+
};
|
|
16
|
+
exports.SUPPORTED_AGENTS = Object.keys(exports.ADAPTERS);
|
package/dist/index.js
CHANGED
|
@@ -6,16 +6,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
9
|
-
const
|
|
10
|
-
const ADAPTERS = {
|
|
11
|
-
"claude-code": claude_code_1.claudeCodeAdapter,
|
|
12
|
-
};
|
|
13
|
-
const SUPPORTED_AGENTS = Object.keys(ADAPTERS);
|
|
9
|
+
const registry_1 = require("./adapters/registry");
|
|
14
10
|
function pickAdapter(agent) {
|
|
15
|
-
if (!(agent in ADAPTERS)) {
|
|
16
|
-
throw new Error(`unknown agent: ${agent} (supported: ${SUPPORTED_AGENTS.join(", ")})`);
|
|
11
|
+
if (!(agent in registry_1.ADAPTERS)) {
|
|
12
|
+
throw new Error(`unknown agent: ${agent} (supported: ${registry_1.SUPPORTED_AGENTS.join(", ")})`);
|
|
17
13
|
}
|
|
18
|
-
return ADAPTERS[agent];
|
|
14
|
+
return registry_1.ADAPTERS[agent];
|
|
19
15
|
}
|
|
20
16
|
const program = new commander_1.Command()
|
|
21
17
|
.name("azure-arch-skill")
|
|
@@ -25,7 +21,7 @@ function defineSubcommand(name, description) {
|
|
|
25
21
|
program
|
|
26
22
|
.command(name)
|
|
27
23
|
.description(description)
|
|
28
|
-
.requiredOption("--agent <name>", `target AI agent (${SUPPORTED_AGENTS.join("|")})`)
|
|
24
|
+
.requiredOption("--agent <name>", `target AI agent (${registry_1.SUPPORTED_AGENTS.join("|")})`)
|
|
29
25
|
.option("--target <dir>", "override target directory (validation use)")
|
|
30
26
|
.action(async (opts) => {
|
|
31
27
|
// Set process.exitCode so any pending async cleanup (file handles, the
|
package/package.json
CHANGED