@hanv89/azure-arch-skill 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/_shared.js +270 -0
- package/dist/adapters/claude-code.js +32 -261
- package/dist/adapters/codex.js +163 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,270 @@
|
|
|
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.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
|
+
async function withFatalReturn(fn) {
|
|
262
|
+
try {
|
|
263
|
+
return await fn();
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
exports.withFatalReturn = withFatalReturn;
|
|
@@ -22,242 +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. Default `retries = 2` matches the R30 fix (Phase 1.0) —
|
|
134
|
-
* future agent adapters reusing this helper get the retry path for free.
|
|
135
|
-
*
|
|
136
|
-
* Backoff schedule: 500ms after attempt 0, 1s after attempt 1, 2s after
|
|
137
|
-
* attempt 2. Network errors (AbortError, DNS failures) re-throw only
|
|
138
|
-
* after the final attempt.
|
|
139
|
-
*
|
|
140
|
-
* @internal — exported only so unit tests can mock `globalThis.fetch`
|
|
141
|
-
* around it. Not part of the public adapter API.
|
|
142
|
-
*/
|
|
143
|
-
async function fetchWithTimeout(url, init = {}, retries = 2) {
|
|
144
|
-
let lastError;
|
|
145
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
146
|
-
const ctrl = new AbortController();
|
|
147
|
-
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
148
|
-
try {
|
|
149
|
-
const res = await fetch(url, {
|
|
150
|
-
...init,
|
|
151
|
-
signal: ctrl.signal,
|
|
152
|
-
headers: { ...(init.headers || {}), "User-Agent": USER_AGENT },
|
|
153
|
-
});
|
|
154
|
-
if (res.status < 500 || attempt === retries) {
|
|
155
|
-
return res;
|
|
156
|
-
}
|
|
157
|
-
// 5xx with retries remaining: fall through to backoff.
|
|
158
|
-
}
|
|
159
|
-
catch (e) {
|
|
160
|
-
lastError = e;
|
|
161
|
-
if (attempt === retries)
|
|
162
|
-
throw e;
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
clearTimeout(t);
|
|
166
|
-
}
|
|
167
|
-
// Exponential backoff: 500ms, 1s, 2s.
|
|
168
|
-
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
|
|
169
|
-
}
|
|
170
|
-
// Unreachable: the loop body always returns or throws on the final attempt.
|
|
171
|
-
throw lastError ?? new Error("fetchWithTimeout: exhausted retries");
|
|
172
|
-
}
|
|
173
|
-
exports.fetchWithTimeout = fetchWithTimeout;
|
|
174
|
-
async function fetchText(url) {
|
|
175
|
-
const res = await fetchWithTimeout(url);
|
|
176
|
-
if (!res.ok)
|
|
177
|
-
throw new Error(`fetch ${url} returned HTTP ${res.status}`);
|
|
178
|
-
return res.text();
|
|
40
|
+
return path.join(claudeRootDir(), "skills");
|
|
179
41
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return
|
|
42
|
+
const CLAUDE_ROOT_DISPLAY = "~/.claude";
|
|
43
|
+
async function resolveTarget(target) {
|
|
44
|
+
return (0, _shared_1.safeResolveTarget)(target, claudeRootDir(), CLAUDE_ROOT_DISPLAY);
|
|
183
45
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Fetch + parse the bundle manifest. Validates required fields and the
|
|
186
|
-
* SKILL.md-at-index-0 invariant. Throws with a clear error on any issue —
|
|
187
|
-
* callers should not silently fall back.
|
|
188
|
-
*/
|
|
189
|
-
async function fetchManifest(base) {
|
|
190
|
-
const url = `${base}/${MANIFEST_PATH}`;
|
|
191
|
-
const body = await fetchText(url);
|
|
192
|
-
let parsed;
|
|
193
|
-
try {
|
|
194
|
-
parsed = JSON.parse(body);
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
throw new Error(`manifest ${url} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
-
}
|
|
199
|
-
if (!parsed || typeof parsed !== "object") {
|
|
200
|
-
throw new Error(`manifest ${url} did not parse to an object`);
|
|
201
|
-
}
|
|
202
|
-
const m = parsed;
|
|
203
|
-
for (const key of ["name", "version", "requires_icons"]) {
|
|
204
|
-
if (typeof m[key] !== "string" || !m[key]) {
|
|
205
|
-
throw new Error(`manifest ${url} missing required field: ${key}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (!Array.isArray(m.files) || m.files.length === 0) {
|
|
209
|
-
throw new Error(`manifest ${url} files[] missing or empty`);
|
|
210
|
-
}
|
|
211
|
-
for (const [i, f] of m.files.entries()) {
|
|
212
|
-
if (!f || typeof f !== "object") {
|
|
213
|
-
throw new Error(`manifest ${url} files[${i}] not an object`);
|
|
214
|
-
}
|
|
215
|
-
for (const key of ["src", "dest", "role"]) {
|
|
216
|
-
if (typeof f[key] !== "string") {
|
|
217
|
-
throw new Error(`manifest ${url} files[${i}].${key} missing`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (m.files[0].dest !== "SKILL.md" || m.files[0].role !== "skill") {
|
|
222
|
-
throw new Error(`manifest ${url} files[0] must be SKILL.md (role=skill); got dest=${m.files[0].dest} role=${m.files[0].role}`);
|
|
223
|
-
}
|
|
224
|
-
return m;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
|
|
228
|
-
* pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
|
|
229
|
-
* mappings, lists, comments after `#`, and YAML null/booleans are NOT handled
|
|
230
|
-
* and may silently mis-parse.
|
|
231
|
-
*
|
|
232
|
-
* Intentional: SKILL.md frontmatter currently has 4 single-line scalar keys
|
|
233
|
-
* (name, description, version, requires_icons). Swap in `js-yaml` when the
|
|
234
|
-
* format grows beyond that.
|
|
235
|
-
*/
|
|
236
|
-
function parseFrontmatter(md) {
|
|
237
|
-
// Strip optional UTF-8 BOM (some editors emit it on save).
|
|
238
|
-
const text = md.replace(/^/, "");
|
|
239
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
240
|
-
if (!match)
|
|
241
|
-
return {};
|
|
242
|
-
const out = {};
|
|
243
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
244
|
-
const kv = line.match(/^(\w+):\s*(.*?)\s*$/);
|
|
245
|
-
if (!kv)
|
|
246
|
-
continue;
|
|
247
|
-
const [, key] = kv;
|
|
248
|
-
let rawValue = kv[2];
|
|
249
|
-
// Strip trailing ` # comment` from unquoted scalars.
|
|
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
46
|
/**
|
|
262
47
|
* Returns true iff `dir` contains a SKILL.md whose frontmatter `name` field
|
|
263
48
|
* matches our skill. Used by uninstall to refuse deleting paths that aren't
|
|
@@ -266,37 +51,24 @@ exports.parseFrontmatter = parseFrontmatter;
|
|
|
266
51
|
async function isOurSkillDir(dir) {
|
|
267
52
|
try {
|
|
268
53
|
const skillMd = await fs.readFile(path.join(dir, "SKILL.md"), "utf8");
|
|
269
|
-
const fm = parseFrontmatter(skillMd);
|
|
270
|
-
return fm.name === SKILL_NAME;
|
|
54
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
55
|
+
return fm.name === _shared_1.SKILL_NAME;
|
|
271
56
|
}
|
|
272
57
|
catch {
|
|
273
58
|
return false;
|
|
274
59
|
}
|
|
275
60
|
}
|
|
276
|
-
async function withFatalReturn(fn) {
|
|
277
|
-
try {
|
|
278
|
-
return await fn();
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
282
|
-
return 1;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
61
|
async function install(opts) {
|
|
286
|
-
return withFatalReturn(async () => {
|
|
287
|
-
const target = await
|
|
288
|
-
const base = baseUrl();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== SKILL_NAME) {
|
|
292
|
-
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.`);
|
|
293
67
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
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.`);
|
|
298
71
|
}
|
|
299
|
-
// Detect partial vs complete prior installs across manifest.files.
|
|
300
72
|
const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
|
|
301
73
|
dest,
|
|
302
74
|
exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
|
|
@@ -309,32 +81,31 @@ async function install(opts) {
|
|
|
309
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.`);
|
|
310
82
|
}
|
|
311
83
|
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
312
|
-
const skillMd = await fetchText(skillUrl);
|
|
313
|
-
const fm = parseFrontmatter(skillMd);
|
|
84
|
+
const skillMd = await (0, _shared_1.fetchText)(skillUrl);
|
|
85
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
314
86
|
if (!fm.requires_icons) {
|
|
315
87
|
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
316
88
|
}
|
|
317
|
-
const canaryUrl = `${base}/${CANARY_ICON_PATH}`;
|
|
318
|
-
const reachable = await headOk(canaryUrl);
|
|
89
|
+
const canaryUrl = `${base}/${_shared_1.CANARY_ICON_PATH}`;
|
|
90
|
+
const reachable = await (0, _shared_1.headOk)(canaryUrl);
|
|
319
91
|
if (!reachable) {
|
|
320
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)`);
|
|
321
93
|
}
|
|
322
|
-
// Mkdir the parent of every bundle dest so future deeper-nested entries work.
|
|
323
94
|
for (const { dest } of manifest.files) {
|
|
324
95
|
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
325
96
|
}
|
|
326
97
|
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
327
98
|
for (const { src, dest } of manifest.files.slice(1)) {
|
|
328
|
-
const body = await fetchText(`${base}/${src}`);
|
|
99
|
+
const body = await (0, _shared_1.fetchText)(`${base}/${src}`);
|
|
329
100
|
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
330
101
|
}
|
|
331
|
-
process.stdout.write(`installed ${SKILL_NAME} to ${target}\n`);
|
|
102
|
+
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${target}\n`);
|
|
332
103
|
return 0;
|
|
333
104
|
});
|
|
334
105
|
}
|
|
335
106
|
async function uninstall(opts) {
|
|
336
|
-
return withFatalReturn(async () => {
|
|
337
|
-
const target = await
|
|
107
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
108
|
+
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
338
109
|
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
339
110
|
if (!exists) {
|
|
340
111
|
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
@@ -355,7 +126,7 @@ async function uninstall(opts) {
|
|
|
355
126
|
}
|
|
356
127
|
throw err;
|
|
357
128
|
}
|
|
358
|
-
process.stdout.write(`uninstalled ${SKILL_NAME} from ${target}\n`);
|
|
129
|
+
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${target}\n`);
|
|
359
130
|
return 0;
|
|
360
131
|
});
|
|
361
132
|
}
|
|
@@ -363,8 +134,8 @@ async function update(opts) {
|
|
|
363
134
|
return install({ ...opts, overwrite: true });
|
|
364
135
|
}
|
|
365
136
|
async function list(opts) {
|
|
366
|
-
return withFatalReturn(async () => {
|
|
367
|
-
const root = await
|
|
137
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
138
|
+
const root = await resolveTarget(opts.target ?? defaultSkillsRoot());
|
|
368
139
|
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
369
140
|
if (!exists) {
|
|
370
141
|
process.stdout.write("(no skills installed)\n");
|
|
@@ -378,7 +149,7 @@ async function list(opts) {
|
|
|
378
149
|
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
379
150
|
try {
|
|
380
151
|
const md = await fs.readFile(skillMdPath, "utf8");
|
|
381
|
-
const fm = parseFrontmatter(md);
|
|
152
|
+
const fm = (0, _shared_1.parseFrontmatter)(md);
|
|
382
153
|
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
383
154
|
}
|
|
384
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 };
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
9
9
|
const claude_code_1 = require("./adapters/claude-code");
|
|
10
|
+
const codex_1 = require("./adapters/codex");
|
|
10
11
|
const ADAPTERS = {
|
|
11
12
|
"claude-code": claude_code_1.claudeCodeAdapter,
|
|
13
|
+
"codex": codex_1.codexAdapter,
|
|
12
14
|
};
|
|
13
15
|
const SUPPORTED_AGENTS = Object.keys(ADAPTERS);
|
|
14
16
|
function pickAdapter(agent) {
|
package/package.json
CHANGED