@hanv89/azure-arch-skill 0.2.2 → 0.3.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.
@@ -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
- // Hard-coded bundle list. Keep SKILL.md as index 0 — install() relies on
41
- // BUNDLE_FILES[0] for the frontmatter-parsing precheck.
42
- const BUNDLE_FILES = [
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}`;
@@ -185,6 +181,48 @@ async function headOk(url) {
185
181
  const res = await fetchWithTimeout(url, { method: "HEAD" });
186
182
  return res.ok;
187
183
  }
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
+ }
188
226
  /**
189
227
  * Minimal YAML frontmatter parser — supports only single-line scalar `key: value`
190
228
  * pairs with optional `"` or `'` quoting. Multi-line scalars (`|`, `>`), nested
@@ -253,8 +291,13 @@ async function install(opts) {
253
291
  if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== SKILL_NAME) {
254
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.`);
255
293
  }
256
- // Detect partial vs complete prior installs across BUNDLE_FILES.
257
- const presence = await Promise.all(BUNDLE_FILES.map(async ({ dest }) => ({
294
+ // Fetch the bundle manifest FIRST. Anything downstream relies on it.
295
+ const manifest = await fetchManifest(base);
296
+ if (manifest.name !== SKILL_NAME) {
297
+ throw new Error(`manifest name mismatch: expected '${SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
298
+ }
299
+ // Detect partial vs complete prior installs across manifest.files.
300
+ const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
258
301
  dest,
259
302
  exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
260
303
  })));
@@ -265,7 +308,7 @@ async function install(opts) {
265
308
  ? `${target} already contains an install. Run 'azure-arch-skill update --agent=claude-code' to refresh.`
266
309
  : `${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
310
  }
268
- const skillUrl = `${base}/${BUNDLE_FILES[0].src}`;
311
+ const skillUrl = `${base}/${manifest.files[0].src}`;
269
312
  const skillMd = await fetchText(skillUrl);
270
313
  const fm = parseFrontmatter(skillMd);
271
314
  if (!fm.requires_icons) {
@@ -277,11 +320,11 @@ async function install(opts) {
277
320
  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
321
  }
279
322
  // Mkdir the parent of every bundle dest so future deeper-nested entries work.
280
- for (const { dest } of BUNDLE_FILES) {
323
+ for (const { dest } of manifest.files) {
281
324
  await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
282
325
  }
283
- await fs.writeFile(path.join(target, BUNDLE_FILES[0].dest), skillMd, "utf8");
284
- for (const { src, dest } of BUNDLE_FILES.slice(1)) {
326
+ await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
327
+ for (const { src, dest } of manifest.files.slice(1)) {
285
328
  const body = await fetchText(`${base}/${src}`);
286
329
  await fs.writeFile(path.join(target, dest), body, "utf8");
287
330
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanv89/azure-arch-skill",
3
- "version": "0.2.2",
3
+ "version": "0.3.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"