@hanv89/azure-arch-skill 0.7.0 → 0.9.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/__test_fixtures__/synthetic-bundle.js +137 -0
- package/dist/adapters/_shared.js +228 -16
- package/dist/adapters/claude-code.js +5 -125
- package/dist/adapters/codex.js +5 -119
- package/dist/adapters/cursor.js +14 -6
- package/dist/all.js +1 -0
- package/dist/index.js +9 -12
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
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.silenceStderr = exports.rmTmpdir = exports.mkTmpdir = exports.failOnNthHeadFetchMock = exports.installFetchMock = exports.SYNTHETIC_MANIFEST = exports.SYNTHETIC_EXAMPLE = exports.SYNTHETIC_SKILL_MD = exports.SYNTHETIC_ICONS_VERSION = exports.SYNTHETIC_REQUIRES_ICONS = exports.SYNTHETIC_VERSION = void 0;
|
|
27
|
+
const fs = __importStar(require("node:fs"));
|
|
28
|
+
const path = __importStar(require("node:path"));
|
|
29
|
+
const os = __importStar(require("node:os"));
|
|
30
|
+
// Synthetic skill bundle used by every test suite that mocks the network.
|
|
31
|
+
// Keeping all three suites (adapters-roundtrip, all, version) on the same
|
|
32
|
+
// fixture eliminates per-file drift (e.g. SKILL.md version: 0.5.0 vs 0.6.0
|
|
33
|
+
// landed in earlier test files because the fixture was copy-pasted).
|
|
34
|
+
exports.SYNTHETIC_VERSION = "0.5.0";
|
|
35
|
+
exports.SYNTHETIC_REQUIRES_ICONS = ">=0.2.2";
|
|
36
|
+
exports.SYNTHETIC_ICONS_VERSION = "0.2.2";
|
|
37
|
+
exports.SYNTHETIC_SKILL_MD = [
|
|
38
|
+
"---",
|
|
39
|
+
"name: azure-architecture-diagram",
|
|
40
|
+
"description: test fixture",
|
|
41
|
+
`version: ${exports.SYNTHETIC_VERSION}`,
|
|
42
|
+
`requires_icons: "${exports.SYNTHETIC_REQUIRES_ICONS}"`,
|
|
43
|
+
"---",
|
|
44
|
+
"# Test skill body",
|
|
45
|
+
"",
|
|
46
|
+
"This is a synthetic SKILL.md used only by the test fixtures.",
|
|
47
|
+
].join("\n");
|
|
48
|
+
exports.SYNTHETIC_EXAMPLE = "@startuml\ntitle Test\n@enduml\n";
|
|
49
|
+
exports.SYNTHETIC_MANIFEST = {
|
|
50
|
+
$schema: "./manifest.schema.json",
|
|
51
|
+
name: "azure-architecture-diagram",
|
|
52
|
+
version: exports.SYNTHETIC_VERSION,
|
|
53
|
+
requires_icons: exports.SYNTHETIC_REQUIRES_ICONS,
|
|
54
|
+
icons_version: exports.SYNTHETIC_ICONS_VERSION,
|
|
55
|
+
files: [
|
|
56
|
+
{ src: "dist/skill/SKILL.md", dest: "SKILL.md", role: "skill" },
|
|
57
|
+
{ src: "dist/skill/examples/01-context.puml", dest: "examples/01-context.puml", role: "example" },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const realFetch = globalThis.fetch;
|
|
61
|
+
/**
|
|
62
|
+
* Returns 200 for the manifest, SKILL.md, and the one bundled example;
|
|
63
|
+
* 200 for any HEAD; 404 otherwise.
|
|
64
|
+
*/
|
|
65
|
+
function installFetchMock() {
|
|
66
|
+
globalThis.fetch = (async (url, init) => {
|
|
67
|
+
const u = url.toString();
|
|
68
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
69
|
+
if (method === "HEAD") {
|
|
70
|
+
return new Response(null, { status: 200 });
|
|
71
|
+
}
|
|
72
|
+
if (u.endsWith("/dist/skill/manifest.json")) {
|
|
73
|
+
return new Response(JSON.stringify(exports.SYNTHETIC_MANIFEST), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
74
|
+
}
|
|
75
|
+
if (u.endsWith("/dist/skill/SKILL.md")) {
|
|
76
|
+
return new Response(exports.SYNTHETIC_SKILL_MD, { status: 200 });
|
|
77
|
+
}
|
|
78
|
+
if (u.endsWith("/dist/skill/examples/01-context.puml")) {
|
|
79
|
+
return new Response(exports.SYNTHETIC_EXAMPLE, { status: 200 });
|
|
80
|
+
}
|
|
81
|
+
return new Response("not found", { status: 404 });
|
|
82
|
+
});
|
|
83
|
+
return { restore: () => { globalThis.fetch = realFetch; } };
|
|
84
|
+
}
|
|
85
|
+
exports.installFetchMock = installFetchMock;
|
|
86
|
+
/**
|
|
87
|
+
* Like `installFetchMock` but the Nth HEAD request returns 404. Used by
|
|
88
|
+
* the rollback test to fail the third adapter's canary while the first
|
|
89
|
+
* two have already installed.
|
|
90
|
+
*/
|
|
91
|
+
function failOnNthHeadFetchMock(failOnHeadIndex) {
|
|
92
|
+
let headCount = 0;
|
|
93
|
+
globalThis.fetch = (async (url, init) => {
|
|
94
|
+
const u = url.toString();
|
|
95
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
96
|
+
if (method === "HEAD") {
|
|
97
|
+
headCount++;
|
|
98
|
+
if (headCount === failOnHeadIndex) {
|
|
99
|
+
return new Response(null, { status: 404 });
|
|
100
|
+
}
|
|
101
|
+
return new Response(null, { status: 200 });
|
|
102
|
+
}
|
|
103
|
+
if (u.endsWith("/dist/skill/manifest.json")) {
|
|
104
|
+
return new Response(JSON.stringify(exports.SYNTHETIC_MANIFEST), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
105
|
+
}
|
|
106
|
+
if (u.endsWith("/dist/skill/SKILL.md")) {
|
|
107
|
+
return new Response(exports.SYNTHETIC_SKILL_MD, { status: 200 });
|
|
108
|
+
}
|
|
109
|
+
if (u.endsWith("/dist/skill/examples/01-context.puml")) {
|
|
110
|
+
return new Response(exports.SYNTHETIC_EXAMPLE, { status: 200 });
|
|
111
|
+
}
|
|
112
|
+
return new Response("not found", { status: 404 });
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
restore: () => { globalThis.fetch = realFetch; },
|
|
116
|
+
headCount: () => headCount,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
exports.failOnNthHeadFetchMock = failOnNthHeadFetchMock;
|
|
120
|
+
function mkTmpdir() {
|
|
121
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "azure-arch-skill-test-"));
|
|
122
|
+
}
|
|
123
|
+
exports.mkTmpdir = mkTmpdir;
|
|
124
|
+
function rmTmpdir(dir) {
|
|
125
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
exports.rmTmpdir = rmTmpdir;
|
|
128
|
+
// Silence stderr summary lines so the test reporter's output stays readable.
|
|
129
|
+
// IMPORTANT: do NOT silence stdout. Hijacking process.stdout.write inside a
|
|
130
|
+
// node:test test confuses the runner's buffered reporter — other tests' ✔
|
|
131
|
+
// lines get eaten by the capture buffer and silently drop from the count.
|
|
132
|
+
function silenceStderr() {
|
|
133
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
134
|
+
process.stderr.write = (_chunk) => true;
|
|
135
|
+
return { restore: () => { process.stderr.write = orig; } };
|
|
136
|
+
}
|
|
137
|
+
exports.silenceStderr = silenceStderr;
|
package/dist/adapters/_shared.js
CHANGED
|
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.withFatalReturn = exports.verifyIconsAvailability = exports.satisfiesRequiresIcons = 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;
|
|
29
|
+
exports.makeFolderInstallAdapter = exports.withFatalReturn = exports.verifyIconsAvailability = exports.satisfiesRequiresIcons = 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
30
|
const fs = __importStar(require("node:fs/promises"));
|
|
31
31
|
const path = __importStar(require("node:path"));
|
|
32
32
|
const package_json_1 = __importDefault(require("../../package.json"));
|
|
@@ -228,6 +228,11 @@ async function fetchManifest(base) {
|
|
|
228
228
|
throw new Error(`manifest ${url} missing required field: ${key}`);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
if (m.icons_version !== undefined) {
|
|
232
|
+
if (typeof m.icons_version !== "string" || !/^\d+\.\d+\.\d+$/.test(m.icons_version)) {
|
|
233
|
+
throw new Error(`manifest ${url} icons_version malformed (must match X.Y.Z): ${String(m.icons_version)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
231
236
|
if (!Array.isArray(m.files) || m.files.length === 0) {
|
|
232
237
|
throw new Error(`manifest ${url} files[] missing or empty`);
|
|
233
238
|
}
|
|
@@ -310,14 +315,20 @@ exports.stripFrontmatter = stripFrontmatter;
|
|
|
310
315
|
* `>=X.Y.Z` today; the other 3 forms exist for future-proofing.
|
|
311
316
|
*
|
|
312
317
|
* Numeric encoding `maj * 1e6 + min * 1e3 + pat` rules out individual segments
|
|
313
|
-
* >= 1000
|
|
318
|
+
* >= 1000 — if a future icons release ever bumps any segment to 4 digits the
|
|
319
|
+
* encoding silently collides (e.g. 1.0.1000 vs 1.1.0). We hard-fail on that
|
|
320
|
+
* input rather than mis-compare.
|
|
314
321
|
*/
|
|
322
|
+
const SEMVER_SEGMENT_MAX = 999;
|
|
315
323
|
function satisfiesRequiresIcons(constraint, iconsSemver) {
|
|
316
324
|
const tagParts = iconsSemver.split(".").map(Number);
|
|
317
325
|
if (tagParts.length !== 3 || tagParts.some(n => isNaN(n))) {
|
|
318
326
|
throw new Error(`icons semver malformed: ${iconsSemver}`);
|
|
319
327
|
}
|
|
320
328
|
const [tagMaj, tagMin, tagPat] = tagParts;
|
|
329
|
+
if (tagMaj > SEMVER_SEGMENT_MAX || tagMin > SEMVER_SEGMENT_MAX || tagPat > SEMVER_SEGMENT_MAX) {
|
|
330
|
+
throw new Error(`icons semver segment exceeds matcher capacity (${SEMVER_SEGMENT_MAX}): ${iconsSemver}`);
|
|
331
|
+
}
|
|
321
332
|
const trimmed = constraint.trim().replace(/^["']|["']$/g, "");
|
|
322
333
|
const m = trimmed.match(/^(>=|\^|~|)(\d+)\.(\d+)\.(\d+)$/);
|
|
323
334
|
if (!m) {
|
|
@@ -327,6 +338,9 @@ function satisfiesRequiresIcons(constraint, iconsSemver) {
|
|
|
327
338
|
const maj = Number(majS);
|
|
328
339
|
const min = Number(minS);
|
|
329
340
|
const pat = Number(patS);
|
|
341
|
+
if (maj > SEMVER_SEGMENT_MAX || min > SEMVER_SEGMENT_MAX || pat > SEMVER_SEGMENT_MAX) {
|
|
342
|
+
throw new Error(`requires_icons segment exceeds matcher capacity (${SEMVER_SEGMENT_MAX}): ${constraint}`);
|
|
343
|
+
}
|
|
330
344
|
const tag = tagMaj * 1e6 + tagMin * 1e3 + tagPat;
|
|
331
345
|
const ref = maj * 1e6 + min * 1e3 + pat;
|
|
332
346
|
if (op === "")
|
|
@@ -344,32 +358,43 @@ exports.satisfiesRequiresIcons = satisfiesRequiresIcons;
|
|
|
344
358
|
* Verify the icon set the skill bundle references is reachable AND its
|
|
345
359
|
* semver satisfies SKILL.md's requires_icons.
|
|
346
360
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* -
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
361
|
+
* Source of the icons-tag, in priority order:
|
|
362
|
+
* 1. `manifest.icons_version` — exact tag (preferred).
|
|
363
|
+
* 2. Lower-bound parse of `manifest.requires_icons` — fallback for
|
|
364
|
+
* bundles published before the field landed (cannot be edited
|
|
365
|
+
* retroactively on a tag).
|
|
366
|
+
*
|
|
367
|
+
* The fallback path makes the gate trivially pass by construction (a
|
|
368
|
+
* lower bound always satisfies its own constraint), preserving install
|
|
369
|
+
* behaviour for older tags. New bundles ship the field, so the gate
|
|
370
|
+
* becomes a real cross-track compatibility check going forward.
|
|
355
371
|
*/
|
|
356
372
|
async function verifyIconsAvailability(base, manifest, requestedVersion) {
|
|
357
373
|
const requires = manifest.requires_icons;
|
|
358
374
|
const canaryUrl = `${base}/${exports.CANARY_ICON_PATH}`;
|
|
359
375
|
const reachable = await headOk(canaryUrl);
|
|
360
376
|
if (!reachable) {
|
|
361
|
-
throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${requires}
|
|
377
|
+
throw new Error(`icon-set unreachable - HEAD ${canaryUrl} failed (skill declares requires_icons=${requires})`);
|
|
362
378
|
}
|
|
363
379
|
if (!requestedVersion) {
|
|
364
380
|
return;
|
|
365
381
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
382
|
+
let iconsTagSemver;
|
|
383
|
+
let source;
|
|
384
|
+
if (manifest.icons_version) {
|
|
385
|
+
iconsTagSemver = manifest.icons_version;
|
|
386
|
+
source = `manifest icons_version`;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const lowerMatch = requires.match(/(\d+\.\d+\.\d+)/);
|
|
390
|
+
if (!lowerMatch) {
|
|
391
|
+
throw new Error(`SKILL.md requires_icons has no parseable lower bound: ${requires}`);
|
|
392
|
+
}
|
|
393
|
+
iconsTagSemver = lowerMatch[1];
|
|
394
|
+
source = `requires_icons lower-bound (bundle has no icons_version field)`;
|
|
369
395
|
}
|
|
370
|
-
const iconsTagSemver = lowerMatch[1];
|
|
371
396
|
if (!satisfiesRequiresIcons(requires, iconsTagSemver)) {
|
|
372
|
-
throw new Error(`requires_icons constraint ${requires} not satisfied by
|
|
397
|
+
throw new Error(`requires_icons constraint ${requires} not satisfied by ${source} ${iconsTagSemver}`);
|
|
373
398
|
}
|
|
374
399
|
}
|
|
375
400
|
exports.verifyIconsAvailability = verifyIconsAvailability;
|
|
@@ -383,3 +408,190 @@ async function withFatalReturn(fn) {
|
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
exports.withFatalReturn = withFatalReturn;
|
|
411
|
+
// Persisted at install time so uninstall can iterate the file list without
|
|
412
|
+
// re-fetching the manifest over the network. Hidden filename so it doesn't
|
|
413
|
+
// clutter the user-visible skill folder.
|
|
414
|
+
const PERSISTED_MANIFEST_BASENAME = ".azure-arch-skill-manifest.json";
|
|
415
|
+
function makeFolderInstallAdapter(cfg) {
|
|
416
|
+
const defaultTarget = () => path.join(cfg.rootDir(), "skills", exports.SKILL_NAME);
|
|
417
|
+
const defaultSkillsRoot = () => path.join(cfg.rootDir(), "skills");
|
|
418
|
+
const resolve = (target) => safeResolveTarget(target, cfg.rootDir(), cfg.rootDisplay());
|
|
419
|
+
const isOurSkillDir = async (dir) => {
|
|
420
|
+
try {
|
|
421
|
+
const skillMd = await fs.readFile(path.join(dir, "SKILL.md"), "utf8");
|
|
422
|
+
return parseFrontmatter(skillMd).name === exports.SKILL_NAME;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
async function install(opts) {
|
|
429
|
+
return withFatalReturn(async () => {
|
|
430
|
+
const target = await resolve(opts.target ?? defaultTarget());
|
|
431
|
+
const base = baseUrl(opts.version);
|
|
432
|
+
if (!process.env.AZURE_ARCH_SKILL_TARGET_ROOT && path.basename(target) !== exports.SKILL_NAME) {
|
|
433
|
+
throw new Error(`refusing to install at ${target} - target basename must be '${exports.SKILL_NAME}' (default ${cfg.rootDisplay()}/skills/${exports.SKILL_NAME}/). Set AZURE_ARCH_SKILL_TARGET_ROOT to install into a custom test root.`);
|
|
434
|
+
}
|
|
435
|
+
const manifest = await fetchManifest(base);
|
|
436
|
+
if (manifest.name !== exports.SKILL_NAME) {
|
|
437
|
+
throw new Error(`manifest name mismatch: expected '${exports.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
438
|
+
}
|
|
439
|
+
const presence = await Promise.all(manifest.files.map(async ({ dest }) => ({
|
|
440
|
+
dest,
|
|
441
|
+
exists: await fs.stat(path.join(target, dest)).then(() => true).catch(() => false),
|
|
442
|
+
})));
|
|
443
|
+
const someExist = presence.some(p => p.exists);
|
|
444
|
+
const allExist = presence.every(p => p.exists);
|
|
445
|
+
if (someExist && !opts.overwrite) {
|
|
446
|
+
throw new Error(allExist
|
|
447
|
+
? `${target} already contains an install. Run 'azure-arch-skill update --agent=${cfg.agentFlag}' to refresh.`
|
|
448
|
+
: `${target} contains a partial install (${presence.filter(p => !p.exists).map(p => p.dest).join(", ")} missing). Run 'azure-arch-skill update --agent=${cfg.agentFlag}' to repair.`);
|
|
449
|
+
}
|
|
450
|
+
const skillUrl = `${base}/${manifest.files[0].src}`;
|
|
451
|
+
const skillMd = await fetchText(skillUrl);
|
|
452
|
+
const fm = parseFrontmatter(skillMd);
|
|
453
|
+
if (!fm.requires_icons) {
|
|
454
|
+
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
455
|
+
}
|
|
456
|
+
await verifyIconsAvailability(base, manifest, opts.version);
|
|
457
|
+
for (const { dest } of manifest.files) {
|
|
458
|
+
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
459
|
+
}
|
|
460
|
+
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
461
|
+
for (const { src, dest } of manifest.files.slice(1)) {
|
|
462
|
+
const body = await fetchText(`${base}/${src}`);
|
|
463
|
+
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
464
|
+
}
|
|
465
|
+
// Persist the manifest so uninstall can iterate the file list without
|
|
466
|
+
// re-fetching from the network.
|
|
467
|
+
await fs.writeFile(path.join(target, PERSISTED_MANIFEST_BASENAME), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
468
|
+
process.stdout.write(`installed ${exports.SKILL_NAME} to ${target}\n`);
|
|
469
|
+
return 0;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async function uninstall(opts) {
|
|
473
|
+
return withFatalReturn(async () => {
|
|
474
|
+
const target = await resolve(opts.target ?? defaultTarget());
|
|
475
|
+
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
476
|
+
if (!exists) {
|
|
477
|
+
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
const ours = await isOurSkillDir(target);
|
|
481
|
+
if (!ours) {
|
|
482
|
+
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.`);
|
|
483
|
+
}
|
|
484
|
+
// Manifest-scoped removal: read the persisted manifest at install time
|
|
485
|
+
// and remove only its files + the manifest itself. Leaves any
|
|
486
|
+
// user-authored content under the same folder in place (with a note).
|
|
487
|
+
// Fallback: bundles installed before 0.9.0 have no persisted manifest;
|
|
488
|
+
// legacy whole-folder rm preserves the pre-0.9.0 behaviour.
|
|
489
|
+
const persistedPath = path.join(target, PERSISTED_MANIFEST_BASENAME);
|
|
490
|
+
const manifestBody = await fs.readFile(persistedPath, "utf8").catch(() => null);
|
|
491
|
+
if (!manifestBody) {
|
|
492
|
+
// Legacy uninstall: rm -rf whole folder.
|
|
493
|
+
try {
|
|
494
|
+
await fs.rm(target, { recursive: true, force: false });
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
const stillExists = await fs.stat(target).then(() => true).catch(() => false);
|
|
498
|
+
if (stillExists) {
|
|
499
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
500
|
+
throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
let persistedManifest;
|
|
507
|
+
try {
|
|
508
|
+
persistedManifest = JSON.parse(manifestBody);
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
512
|
+
throw new Error(`persisted manifest at ${persistedPath} is not valid JSON: ${msg}. Remove the file manually then retry.`);
|
|
513
|
+
}
|
|
514
|
+
for (const f of persistedManifest.files) {
|
|
515
|
+
await fs.unlink(path.join(target, f.dest)).catch(() => null);
|
|
516
|
+
}
|
|
517
|
+
await fs.unlink(persistedPath).catch(() => null);
|
|
518
|
+
// Recursively prune empty directories under target. Stop at target
|
|
519
|
+
// itself — only rmdir target if no user-authored files remain.
|
|
520
|
+
const pruneEmptyDirs = async (dir) => {
|
|
521
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
522
|
+
for (const e of entries) {
|
|
523
|
+
if (e.isDirectory()) {
|
|
524
|
+
await pruneEmptyDirs(path.join(dir, e.name));
|
|
525
|
+
const subEntries = await fs.readdir(path.join(dir, e.name)).catch(() => []);
|
|
526
|
+
if (subEntries.length === 0) {
|
|
527
|
+
await fs.rmdir(path.join(dir, e.name)).catch(() => null);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
await pruneEmptyDirs(target);
|
|
533
|
+
const remaining = await fs.readdir(target).catch(() => []);
|
|
534
|
+
if (remaining.length === 0) {
|
|
535
|
+
await fs.rmdir(target).catch(() => null);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
process.stdout.write(`note: ${target} contains files outside the skill manifest; left in place. Remove manually if intentional.\n`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
process.stdout.write(`uninstalled ${exports.SKILL_NAME} from ${target}\n`);
|
|
542
|
+
return 0;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
async function update(opts) {
|
|
546
|
+
return withFatalReturn(async () => {
|
|
547
|
+
const target = await resolve(opts.target ?? defaultTarget());
|
|
548
|
+
const base = baseUrl(opts.version);
|
|
549
|
+
const manifest = await fetchManifest(base);
|
|
550
|
+
if (manifest.name !== exports.SKILL_NAME) {
|
|
551
|
+
throw new Error(`manifest name mismatch: expected '${exports.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
552
|
+
}
|
|
553
|
+
// Already-at-version short-circuit: read the on-disk SKILL.md
|
|
554
|
+
// frontmatter version and compare to manifest. Equal -> no-op.
|
|
555
|
+
const installedSkillMdPath = path.join(target, "SKILL.md");
|
|
556
|
+
const installedBody = await fs.readFile(installedSkillMdPath, "utf8").catch(() => null);
|
|
557
|
+
if (installedBody) {
|
|
558
|
+
const fmInstalled = parseFrontmatter(installedBody);
|
|
559
|
+
if (fmInstalled.version && fmInstalled.version === manifest.version) {
|
|
560
|
+
process.stdout.write(`${exports.SKILL_NAME} already at version ${manifest.version} (no-op)\n`);
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Otherwise proceed with overwriting install (the existing path).
|
|
565
|
+
return install({ ...opts, overwrite: true });
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async function list(opts) {
|
|
569
|
+
return withFatalReturn(async () => {
|
|
570
|
+
const root = await resolve(opts.target ?? defaultSkillsRoot());
|
|
571
|
+
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
572
|
+
if (!exists) {
|
|
573
|
+
process.stdout.write("(no skills installed)\n");
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
577
|
+
const rows = [];
|
|
578
|
+
for (const e of entries) {
|
|
579
|
+
if (!e.isDirectory())
|
|
580
|
+
continue;
|
|
581
|
+
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
582
|
+
try {
|
|
583
|
+
const md = await fs.readFile(skillMdPath, "utf8");
|
|
584
|
+
const fm = parseFrontmatter(md);
|
|
585
|
+
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// not a skill folder; skip silently
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
process.stdout.write(rows.length ? rows.join("\n") + "\n" : "(no skills installed)\n");
|
|
592
|
+
return 0;
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return { install, uninstall, update, list };
|
|
596
|
+
}
|
|
597
|
+
exports.makeFolderInstallAdapter = makeFolderInstallAdapter;
|
|
@@ -24,7 +24,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.claudeCodeAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
27
|
-
const fs = __importStar(require("node:fs/promises"));
|
|
28
27
|
const path = __importStar(require("node:path"));
|
|
29
28
|
const os = __importStar(require("node:os"));
|
|
30
29
|
const _shared_1 = require("./_shared");
|
|
@@ -33,127 +32,8 @@ Object.defineProperty(exports, "parseFrontmatter", { enumerable: true, get: func
|
|
|
33
32
|
function claudeRootDir() {
|
|
34
33
|
return path.join(os.homedir(), ".claude");
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
const CLAUDE_ROOT_DISPLAY = "~/.claude";
|
|
43
|
-
async function resolveTarget(target) {
|
|
44
|
-
return (0, _shared_1.safeResolveTarget)(target, claudeRootDir(), CLAUDE_ROOT_DISPLAY);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Returns true iff `dir` contains a SKILL.md whose frontmatter `name` field
|
|
48
|
-
* matches our skill. Used by uninstall to refuse deleting paths that aren't
|
|
49
|
-
* our skill folder.
|
|
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)(opts.version);
|
|
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.`);
|
|
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=claude-code' 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=claude-code' 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
|
-
await (0, _shared_1.verifyIconsAvailability)(base, manifest, opts.version);
|
|
90
|
-
for (const { dest } of manifest.files) {
|
|
91
|
-
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
94
|
-
for (const { src, dest } of manifest.files.slice(1)) {
|
|
95
|
-
const body = await (0, _shared_1.fetchText)(`${base}/${src}`);
|
|
96
|
-
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
97
|
-
}
|
|
98
|
-
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${target}\n`);
|
|
99
|
-
return 0;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async function uninstall(opts) {
|
|
103
|
-
return (0, _shared_1.withFatalReturn)(async () => {
|
|
104
|
-
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
105
|
-
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
106
|
-
if (!exists) {
|
|
107
|
-
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
const ours = await isOurSkillDir(target);
|
|
111
|
-
if (!ours) {
|
|
112
|
-
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.`);
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
await fs.rm(target, { recursive: true, force: false });
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
const stillExists = await fs.stat(target).then(() => true).catch(() => false);
|
|
119
|
-
if (stillExists) {
|
|
120
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
-
throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
|
|
122
|
-
}
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
|
-
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${target}\n`);
|
|
126
|
-
return 0;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async function update(opts) {
|
|
130
|
-
return install({ ...opts, overwrite: true });
|
|
131
|
-
}
|
|
132
|
-
async function list(opts) {
|
|
133
|
-
return (0, _shared_1.withFatalReturn)(async () => {
|
|
134
|
-
const root = await resolveTarget(opts.target ?? defaultSkillsRoot());
|
|
135
|
-
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
136
|
-
if (!exists) {
|
|
137
|
-
process.stdout.write("(no skills installed)\n");
|
|
138
|
-
return 0;
|
|
139
|
-
}
|
|
140
|
-
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
141
|
-
const rows = [];
|
|
142
|
-
for (const e of entries) {
|
|
143
|
-
if (!e.isDirectory())
|
|
144
|
-
continue;
|
|
145
|
-
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
146
|
-
try {
|
|
147
|
-
const md = await fs.readFile(skillMdPath, "utf8");
|
|
148
|
-
const fm = (0, _shared_1.parseFrontmatter)(md);
|
|
149
|
-
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// not a skill folder; skip silently
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
process.stdout.write(rows.length ? rows.join("\n") + "\n" : "(no skills installed)\n");
|
|
156
|
-
return 0;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
exports.claudeCodeAdapter = { install, uninstall, update, list };
|
|
35
|
+
exports.claudeCodeAdapter = (0, _shared_1.makeFolderInstallAdapter)({
|
|
36
|
+
rootDir: claudeRootDir,
|
|
37
|
+
rootDisplay: () => "~/.claude",
|
|
38
|
+
agentFlag: "claude-code",
|
|
39
|
+
});
|
package/dist/adapters/codex.js
CHANGED
|
@@ -24,7 +24,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.codexAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
27
|
-
const fs = __importStar(require("node:fs/promises"));
|
|
28
27
|
const path = __importStar(require("node:path"));
|
|
29
28
|
const os = __importStar(require("node:os"));
|
|
30
29
|
const _shared_1 = require("./_shared");
|
|
@@ -36,124 +35,11 @@ function codexRootDir() {
|
|
|
36
35
|
return path.resolve(explicit);
|
|
37
36
|
return path.join(os.homedir(), ".codex");
|
|
38
37
|
}
|
|
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
38
|
function codexRootDisplay() {
|
|
46
39
|
return process.env.CODEX_HOME ? `$CODEX_HOME=${process.env.CODEX_HOME}` : "~/.codex";
|
|
47
40
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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)(opts.version);
|
|
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
|
-
await (0, _shared_1.verifyIconsAvailability)(base, manifest, opts.version);
|
|
90
|
-
for (const { dest } of manifest.files) {
|
|
91
|
-
await fs.mkdir(path.dirname(path.join(target, dest)), { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
await fs.writeFile(path.join(target, manifest.files[0].dest), skillMd, "utf8");
|
|
94
|
-
for (const { src, dest } of manifest.files.slice(1)) {
|
|
95
|
-
const body = await (0, _shared_1.fetchText)(`${base}/${src}`);
|
|
96
|
-
await fs.writeFile(path.join(target, dest), body, "utf8");
|
|
97
|
-
}
|
|
98
|
-
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${target}\n`);
|
|
99
|
-
return 0;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async function uninstall(opts) {
|
|
103
|
-
return (0, _shared_1.withFatalReturn)(async () => {
|
|
104
|
-
const target = await resolveTarget(opts.target ?? defaultTarget());
|
|
105
|
-
const exists = await fs.stat(target).then(() => true).catch(() => false);
|
|
106
|
-
if (!exists) {
|
|
107
|
-
process.stdout.write(`(nothing to uninstall at ${target})\n`);
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
const ours = await isOurSkillDir(target);
|
|
111
|
-
if (!ours) {
|
|
112
|
-
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.`);
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
await fs.rm(target, { recursive: true, force: false });
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
const stillExists = await fs.stat(target).then(() => true).catch(() => false);
|
|
119
|
-
if (stillExists) {
|
|
120
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
-
throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
|
|
122
|
-
}
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
|
-
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${target}\n`);
|
|
126
|
-
return 0;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async function update(opts) {
|
|
130
|
-
return install({ ...opts, overwrite: true });
|
|
131
|
-
}
|
|
132
|
-
async function list(opts) {
|
|
133
|
-
return (0, _shared_1.withFatalReturn)(async () => {
|
|
134
|
-
const root = await resolveTarget(opts.target ?? defaultSkillsRoot());
|
|
135
|
-
const exists = await fs.stat(root).then(() => true).catch(() => false);
|
|
136
|
-
if (!exists) {
|
|
137
|
-
process.stdout.write("(no skills installed)\n");
|
|
138
|
-
return 0;
|
|
139
|
-
}
|
|
140
|
-
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
141
|
-
const rows = [];
|
|
142
|
-
for (const e of entries) {
|
|
143
|
-
if (!e.isDirectory())
|
|
144
|
-
continue;
|
|
145
|
-
const skillMdPath = path.join(root, e.name, "SKILL.md");
|
|
146
|
-
try {
|
|
147
|
-
const md = await fs.readFile(skillMdPath, "utf8");
|
|
148
|
-
const fm = (0, _shared_1.parseFrontmatter)(md);
|
|
149
|
-
rows.push(`${fm.name ?? e.name}\t${fm.version ?? "?"}`);
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// not a skill folder; skip silently
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
process.stdout.write(rows.length ? rows.join("\n") + "\n" : "(no skills installed)\n");
|
|
156
|
-
return 0;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
exports.codexAdapter = { install, uninstall, update, list };
|
|
41
|
+
exports.codexAdapter = (0, _shared_1.makeFolderInstallAdapter)({
|
|
42
|
+
rootDir: codexRootDir,
|
|
43
|
+
rootDisplay: codexRootDisplay,
|
|
44
|
+
agentFlag: "codex",
|
|
45
|
+
});
|
package/dist/adapters/cursor.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.cursorAdapter = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
26
|
+
exports.cursorAdapter = exports.PROVENANCE_RE = exports.provenanceMarker = exports.parseFrontmatter = exports.fetchWithTimeout = void 0;
|
|
27
27
|
const fs = __importStar(require("node:fs/promises"));
|
|
28
28
|
const path = __importStar(require("node:path"));
|
|
29
29
|
const _shared_1 = require("./_shared");
|
|
@@ -33,6 +33,11 @@ const RULE_BASENAME = "azure-arch-skill.mdc";
|
|
|
33
33
|
const RULE_DESCRIPTION = "Use this rule when drawing Microsoft Azure or Microsoft Fabric architecture diagrams using PlantUML. " +
|
|
34
34
|
"Triggers on \"draw Azure architecture\", \"create deployment diagram\", \"Lakehouse + Notebook + Warehouse diagram\", " +
|
|
35
35
|
"\"vẽ Azure\", \"PlantUML diagram for [project]\".";
|
|
36
|
+
// Cursor's rule discovery is per-project: <cwd>/.cursor/rules/*.mdc is the
|
|
37
|
+
// canonical install location, so the team picks up the rule via the project's
|
|
38
|
+
// git repo. A HOME-relative install would only help the local developer. The
|
|
39
|
+
// trade-off — running install from an unintended cwd — is mitigated by a
|
|
40
|
+
// runtime warn-line (see `install` below) and the `--target=<path>` override.
|
|
36
41
|
function defaultTarget() {
|
|
37
42
|
return path.join(process.cwd(), ".cursor", "rules");
|
|
38
43
|
}
|
|
@@ -40,15 +45,15 @@ const CWD_DISPLAY = "<cwd>/.cursor/rules";
|
|
|
40
45
|
async function resolveTarget(target) {
|
|
41
46
|
return (0, _shared_1.safeResolveTarget)(target, process.cwd(), CWD_DISPLAY);
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* non-frontmatter line after the closing `---`.
|
|
47
|
-
*/
|
|
48
|
+
// `provenanceMarker` and `PROVENANCE_RE` are paired: the regex MUST parse what
|
|
49
|
+
// the marker writes. Keep them in lockstep — a unit test in version.test.ts
|
|
50
|
+
// asserts the round-trip.
|
|
48
51
|
function provenanceMarker(version, requiresIcons) {
|
|
49
52
|
return `<!-- ${_shared_1.SKILL_NAME} v${version} (requires_icons: ${requiresIcons}) -->`;
|
|
50
53
|
}
|
|
54
|
+
exports.provenanceMarker = provenanceMarker;
|
|
51
55
|
const PROVENANCE_RE = new RegExp(`<!--\\s*${_shared_1.SKILL_NAME}\\s+v([^\\s]+)\\s+\\(requires_icons:\\s*([^)]+)\\)\\s*-->`);
|
|
56
|
+
exports.PROVENANCE_RE = PROVENANCE_RE;
|
|
52
57
|
function renderRule(skillBody, version, requiresIcons) {
|
|
53
58
|
return [
|
|
54
59
|
"---",
|
|
@@ -75,6 +80,9 @@ async function isOurRuleFile(file) {
|
|
|
75
80
|
}
|
|
76
81
|
async function install(opts) {
|
|
77
82
|
return (0, _shared_1.withFatalReturn)(async () => {
|
|
83
|
+
if (opts.target === undefined) {
|
|
84
|
+
process.stderr.write(`note: Cursor target resolved to ${defaultTarget()} (per-project install). Pass --target=<path> to override.\n`);
|
|
85
|
+
}
|
|
78
86
|
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
79
87
|
const base = (0, _shared_1.baseUrl)(opts.version);
|
|
80
88
|
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
package/dist/all.js
CHANGED
|
@@ -50,6 +50,7 @@ async function runOverAll(sub, opts) {
|
|
|
50
50
|
exports.runOverAll = runOverAll;
|
|
51
51
|
async function runOne(adapter, agent, sub, opts) {
|
|
52
52
|
try {
|
|
53
|
+
// All four Adapter methods now accept the same AdapterOpts shape (see types.ts).
|
|
53
54
|
const exit = await adapter[sub](opts);
|
|
54
55
|
return { agent, exit };
|
|
55
56
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,6 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
9
9
|
const registry_1 = require("./adapters/registry");
|
|
10
10
|
const all_1 = require("./all");
|
|
11
|
-
function pickAdapter(agent) {
|
|
12
|
-
if (!(agent in registry_1.ADAPTERS)) {
|
|
13
|
-
throw new Error(`unknown agent: ${agent} (supported: ${registry_1.SUPPORTED_TARGETS.join(", ")})`);
|
|
14
|
-
}
|
|
15
|
-
return registry_1.ADAPTERS[agent];
|
|
16
|
-
}
|
|
17
11
|
const program = new commander_1.Command()
|
|
18
12
|
.name("azure-arch-skill")
|
|
19
13
|
.description("Install the Azure architecture diagram skill into your AI coding agent.")
|
|
@@ -27,10 +21,8 @@ function defineSubcommand(name, description) {
|
|
|
27
21
|
.option("--target <dir>", "override target directory (validation use)")
|
|
28
22
|
.option("--version <semver>", "pin to a specific skill version (X.Y.Z); default = latest from main")
|
|
29
23
|
.action(async (opts) => {
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// Both this top-level path and adapter-internal failures emit a single
|
|
33
|
-
// '^fatal: ' prefix line on stderr — log-parsers can rely on the prefix.
|
|
24
|
+
// Validate --version pre-dispatch as defense-in-depth; baseUrl() in
|
|
25
|
+
// _shared.ts also rejects malformed values when it builds the URL.
|
|
34
26
|
if (opts.version !== undefined && !VERSION_RE.test(opts.version)) {
|
|
35
27
|
throw new Error(`--version must match X.Y.Z (got: ${opts.version})`);
|
|
36
28
|
}
|
|
@@ -40,15 +32,20 @@ function defineSubcommand(name, description) {
|
|
|
40
32
|
return;
|
|
41
33
|
}
|
|
42
34
|
if (!registry_1.SUPPORTED_AGENTS.includes(opts.agent)) {
|
|
43
|
-
throw new Error(`unknown agent: ${opts.agent} (supported: ${registry_1.
|
|
35
|
+
throw new Error(`unknown agent: ${opts.agent} (supported: ${registry_1.SUPPORTED_AGENTS.join(", ")})`);
|
|
44
36
|
}
|
|
45
|
-
process.exitCode = await
|
|
37
|
+
process.exitCode = await registry_1.ADAPTERS[opts.agent][name](optsForAdapter);
|
|
46
38
|
});
|
|
47
39
|
}
|
|
48
40
|
defineSubcommand("install", "Install the skill into an AI agent's skill folder.");
|
|
49
41
|
defineSubcommand("uninstall", "Remove a previously installed skill.");
|
|
50
42
|
defineSubcommand("update", "Update an installed skill to the latest version.");
|
|
51
43
|
defineSubcommand("list", "List installed skills and their versions.");
|
|
44
|
+
// Top-level catch: setting process.exitCode (instead of process.exit(1)) lets
|
|
45
|
+
// any pending async cleanup (file handles, the override-warning stderr write)
|
|
46
|
+
// drain before the event loop empties. Both this path and adapter-internal
|
|
47
|
+
// failures emit a single '^fatal: ' prefix line on stderr — log-parsers can
|
|
48
|
+
// rely on the prefix.
|
|
52
49
|
program.parseAsync(process.argv).catch(err => {
|
|
53
50
|
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
54
51
|
process.exit(1);
|
package/package.json
CHANGED