@hanv89/azure-arch-skill 0.2.2 → 0.3.1
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/claude-code.js +59 -15
- package/package.json +1 -1
|
@@ -37,13 +37,9 @@ const DEFAULT_BASE_RAW_URL = "https://raw.githubusercontent.com/hanv89/azure-ico
|
|
|
37
37
|
// existing installs become un-uninstallable until users upgrade the CLI
|
|
38
38
|
// (uninstall's allow-list refuses folders whose SKILL.md `name` differs).
|
|
39
39
|
const SKILL_NAME = "azure-architecture-diagram";
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
{ src: "dist/skill/SKILL.md", dest: "SKILL.md" },
|
|
44
|
-
{ src: "dist/skill/examples/01-context.puml", dest: "examples/01-context.puml" },
|
|
45
|
-
{ src: "dist/skill/examples/02-fabric-data-pipeline.puml", dest: "examples/02-fabric-data-pipeline.puml" },
|
|
46
|
-
];
|
|
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";
|
|
47
43
|
const CANARY_ICON_PATH = "dist/Azure/Compute/AzureVirtualMachine.png";
|
|
48
44
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
49
45
|
const USER_AGENT = `azure-arch-skill/${package_json_1.default.version}`;
|
|
@@ -134,8 +130,9 @@ async function safeResolveTarget(target) {
|
|
|
134
130
|
/**
|
|
135
131
|
* Fetch with timeout and 2-retry exponential backoff on transient 5xx
|
|
136
132
|
* responses. Used by `fetchText` and `headOk`; both inherit the retry
|
|
137
|
-
* behavior.
|
|
138
|
-
* future agent adapters reusing this helper get the
|
|
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.
|
|
139
136
|
*
|
|
140
137
|
* Backoff schedule: 500ms after attempt 0, 1s after attempt 1, 2s after
|
|
141
138
|
* attempt 2. Network errors (AbortError, DNS failures) re-throw only
|
|
@@ -185,6 +182,48 @@ async function headOk(url) {
|
|
|
185
182
|
const res = await fetchWithTimeout(url, { method: "HEAD" });
|
|
186
183
|
return res.ok;
|
|
187
184
|
}
|
|
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
|
+
}
|
|
188
227
|
/**
|
|
189
228
|
* Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
|
|
190
229
|
* pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
|
|
@@ -253,8 +292,13 @@ async function install(opts) {
|
|
|
253
292
|
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== SKILL_NAME) {
|
|
254
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.`);
|
|
255
294
|
}
|
|
256
|
-
//
|
|
257
|
-
const
|
|
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.`);
|
|
299
|
+
}
|
|
300
|
+
// Detect partial vs complete prior installs across manifest.files.
|
|
301
|
+
const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
|
|
258
302
|
dest,
|
|
259
303
|
exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
|
|
260
304
|
})));
|
|
@@ -265,7 +309,7 @@ async function install(opts) {
|
|
|
265
309
|
? `${target} already contains an install. Run 'azure-arch-skill update --agent=claude-code' to refresh.`
|
|
266
310
|
: `${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.`);
|
|
267
311
|
}
|
|
268
|
-
const skillUrl = `${base}/${
|
|
312
|
+
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
269
313
|
const skillMd = await fetchText(skillUrl);
|
|
270
314
|
const fm = parseFrontmatter(skillMd);
|
|
271
315
|
if (!fm.requires_icons) {
|
|
@@ -277,11 +321,11 @@ async function install(opts) {
|
|
|
277
321
|
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)`);
|
|
278
322
|
}
|
|
279
323
|
// Mkdir the parent of every bundle dest so future deeper-nested entries work.
|
|
280
|
-
for (const { dest } of
|
|
324
|
+
for (const { dest } of manifest.files) {
|
|
281
325
|
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
282
326
|
}
|
|
283
|
-
await fs.writeFile(path.join(target,
|
|
284
|
-
for (const { src, dest } of
|
|
327
|
+
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
328
|
+
for (const { src, dest } of manifest.files.slice(1)) {
|
|
285
329
|
const body = await fetchText(`${base}/${src}`);
|
|
286
330
|
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
287
331
|
}
|
package/package.json
CHANGED