@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.
@@ -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 package_json_1 = __importDefault(require("../../package.json"));
34
- const DEFAULT_BASE_RAW_URL = "https://raw.githubusercontent.com/hanv89/azure-icons-for-architecture-diagrams/main";
35
- // SKILL_NAME must stay in lockstep with dist/skill/SKILL.md frontmatter `name`.
36
- // Renaming the skill is a breaking change requiring a coordinated CLI release;
37
- // existing installs become un-uninstallable until users upgrade the CLI
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(os.homedir(), ".claude", "skills", SKILL_NAME);
37
+ return path.join(claudeRootDir(), "skills", _shared_1.SKILL_NAME);
73
38
  }
74
39
  function defaultSkillsRoot() {
75
- return path.join(os.homedir(), ".claude", "skills");
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
- async function headOk(url) {
182
- const res = await fetchWithTimeout(url, { method: "HEAD" });
183
- return res.ok;
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 safeResolveTarget(opts.target ?? defaultTarget());
289
- const base = baseUrl();
290
- // Strict install path: target must end with the canonical skill folder name
291
- // unless the caller has opted into a wider AZURE_ARCH_SKILL_TARGET_ROOT.
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
- // Fetch the bundle manifest FIRST. Anything downstream relies on it.
296
- const manifest = await fetchManifest(base);
297
- if (manifest.name !== SKILL_NAME) {
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 safeResolveTarget(opts.target ?? defaultTarget());
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 safeResolveTarget(opts.target ?? defaultSkillsRoot());
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 claude_code_1 = require("./adapters/claude-code");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanv89/azure-arch-skill",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Install the Azure architecture diagram skill into your AI coding agent (Claude Code, Codex CLI, Cursor).",
5
5
  "bin": {
6
6
  "azure-arch-skill": "dist/index.js"