@hanv89/arch-skill 0.1.0 → 0.2.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 +147 -0
- package/dist/adapters/_shared.js +1 -0
- package/dist/adapters/codex.js +56 -0
- package/dist/adapters/cursor.js +198 -0
- package/dist/adapters/registry.js +10 -6
- package/dist/all.js +66 -0
- package/dist/index.js +5 -0
- package/package.json +3 -3
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SYNTHETIC_MANIFEST = exports.SYNTHETIC_EXAMPLE = exports.SYNTHETIC_SKILL_MD = exports.SYNTHETIC_ICONS_VERSION = exports.SYNTHETIC_REQUIRES_ICONS = exports.SYNTHETIC_VERSION = void 0;
|
|
37
|
+
exports.installFetchMock = installFetchMock;
|
|
38
|
+
exports.failOnNthHeadFetchMock = failOnNthHeadFetchMock;
|
|
39
|
+
exports.mkTmpdir = mkTmpdir;
|
|
40
|
+
exports.rmTmpdir = rmTmpdir;
|
|
41
|
+
exports.silenceStderr = silenceStderr;
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const os = __importStar(require("node:os"));
|
|
45
|
+
// Synthetic skill bundle used by every test suite that mocks the network.
|
|
46
|
+
// Keeping all three suites (adapters-roundtrip, all, version) on the same
|
|
47
|
+
// fixture eliminates per-file drift (e.g. SKILL.md version: 0.5.0 vs 0.6.0
|
|
48
|
+
// landed in earlier test files because the fixture was copy-pasted).
|
|
49
|
+
exports.SYNTHETIC_VERSION = "0.5.0";
|
|
50
|
+
exports.SYNTHETIC_REQUIRES_ICONS = ">=0.2.2";
|
|
51
|
+
exports.SYNTHETIC_ICONS_VERSION = "0.2.2";
|
|
52
|
+
exports.SYNTHETIC_SKILL_MD = [
|
|
53
|
+
"---",
|
|
54
|
+
"name: architecture-diagram",
|
|
55
|
+
"description: test fixture",
|
|
56
|
+
`version: ${exports.SYNTHETIC_VERSION}`,
|
|
57
|
+
`requires_icons: "${exports.SYNTHETIC_REQUIRES_ICONS}"`,
|
|
58
|
+
"---",
|
|
59
|
+
"# Test skill body",
|
|
60
|
+
"",
|
|
61
|
+
"This is a synthetic SKILL.md used only by the test fixtures.",
|
|
62
|
+
].join("\n");
|
|
63
|
+
exports.SYNTHETIC_EXAMPLE = "@startuml\ntitle Test\n@enduml\n";
|
|
64
|
+
exports.SYNTHETIC_MANIFEST = {
|
|
65
|
+
$schema: "./manifest.schema.json",
|
|
66
|
+
name: "architecture-diagram",
|
|
67
|
+
version: exports.SYNTHETIC_VERSION,
|
|
68
|
+
requires_icons: exports.SYNTHETIC_REQUIRES_ICONS,
|
|
69
|
+
icons_version: exports.SYNTHETIC_ICONS_VERSION,
|
|
70
|
+
files: [
|
|
71
|
+
{ src: "dist/skill/SKILL.md", dest: "SKILL.md", role: "skill" },
|
|
72
|
+
{ src: "dist/skill/examples/01-context.puml", dest: "examples/01-context.puml", role: "example" },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const realFetch = globalThis.fetch;
|
|
76
|
+
/**
|
|
77
|
+
* Returns 200 for the manifest, SKILL.md, and the one bundled example;
|
|
78
|
+
* 200 for any HEAD; 404 otherwise.
|
|
79
|
+
*/
|
|
80
|
+
function installFetchMock() {
|
|
81
|
+
globalThis.fetch = (async (url, init) => {
|
|
82
|
+
const u = url.toString();
|
|
83
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
84
|
+
if (method === "HEAD") {
|
|
85
|
+
return new Response(null, { status: 200 });
|
|
86
|
+
}
|
|
87
|
+
if (u.endsWith("/dist/skill/manifest.json")) {
|
|
88
|
+
return new Response(JSON.stringify(exports.SYNTHETIC_MANIFEST), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
89
|
+
}
|
|
90
|
+
if (u.endsWith("/dist/skill/SKILL.md")) {
|
|
91
|
+
return new Response(exports.SYNTHETIC_SKILL_MD, { status: 200 });
|
|
92
|
+
}
|
|
93
|
+
if (u.endsWith("/dist/skill/examples/01-context.puml")) {
|
|
94
|
+
return new Response(exports.SYNTHETIC_EXAMPLE, { status: 200 });
|
|
95
|
+
}
|
|
96
|
+
return new Response("not found", { status: 404 });
|
|
97
|
+
});
|
|
98
|
+
return { restore: () => { globalThis.fetch = realFetch; } };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Like `installFetchMock` but the Nth HEAD request returns 404. Used by
|
|
102
|
+
* the rollback test to fail the third adapter's canary while the first
|
|
103
|
+
* two have already installed.
|
|
104
|
+
*/
|
|
105
|
+
function failOnNthHeadFetchMock(failOnHeadIndex) {
|
|
106
|
+
let headCount = 0;
|
|
107
|
+
globalThis.fetch = (async (url, init) => {
|
|
108
|
+
const u = url.toString();
|
|
109
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
110
|
+
if (method === "HEAD") {
|
|
111
|
+
headCount++;
|
|
112
|
+
if (headCount === failOnHeadIndex) {
|
|
113
|
+
return new Response(null, { status: 404 });
|
|
114
|
+
}
|
|
115
|
+
return new Response(null, { status: 200 });
|
|
116
|
+
}
|
|
117
|
+
if (u.endsWith("/dist/skill/manifest.json")) {
|
|
118
|
+
return new Response(JSON.stringify(exports.SYNTHETIC_MANIFEST), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
119
|
+
}
|
|
120
|
+
if (u.endsWith("/dist/skill/SKILL.md")) {
|
|
121
|
+
return new Response(exports.SYNTHETIC_SKILL_MD, { status: 200 });
|
|
122
|
+
}
|
|
123
|
+
if (u.endsWith("/dist/skill/examples/01-context.puml")) {
|
|
124
|
+
return new Response(exports.SYNTHETIC_EXAMPLE, { status: 200 });
|
|
125
|
+
}
|
|
126
|
+
return new Response("not found", { status: 404 });
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
restore: () => { globalThis.fetch = realFetch; },
|
|
130
|
+
headCount: () => headCount,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function mkTmpdir() {
|
|
134
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "arch-skill-test-"));
|
|
135
|
+
}
|
|
136
|
+
function rmTmpdir(dir) {
|
|
137
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
// Silence stderr summary lines so the test reporter's output stays readable.
|
|
140
|
+
// IMPORTANT: do NOT silence stdout. Hijacking process.stdout.write inside a
|
|
141
|
+
// node:test test confuses the runner's buffered reporter — other tests' ✔
|
|
142
|
+
// lines get eaten by the capture buffer and silently drop from the count.
|
|
143
|
+
function silenceStderr() {
|
|
144
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
145
|
+
process.stderr.write = (_chunk) => true;
|
|
146
|
+
return { restore: () => { process.stderr.write = orig; } };
|
|
147
|
+
}
|
package/dist/adapters/_shared.js
CHANGED
|
@@ -51,6 +51,7 @@ exports.satisfiesRequiresIcons = satisfiesRequiresIcons;
|
|
|
51
51
|
exports.verifyIconsAvailability = verifyIconsAvailability;
|
|
52
52
|
exports.withFatalReturn = withFatalReturn;
|
|
53
53
|
exports.makeFolderInstallAdapter = makeFolderInstallAdapter;
|
|
54
|
+
exports.verifyBundleFile = verifyBundleFile;
|
|
54
55
|
const fs = __importStar(require("node:fs/promises"));
|
|
55
56
|
const path = __importStar(require("node:path"));
|
|
56
57
|
const crypto = __importStar(require("node:crypto"));
|
|
@@ -0,0 +1,56 @@
|
|
|
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 () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.codexAdapter = void 0;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const os = __importStar(require("node:os"));
|
|
39
|
+
const _shared_1 = require("./_shared");
|
|
40
|
+
// Codex CLI discovers user-installed skills at $CODEX_HOME/skills/<name>/SKILL.md,
|
|
41
|
+
// defaulting to ~/.codex/skills/ when CODEX_HOME is unset. Verified against the
|
|
42
|
+
// Codex Rust binary's bundled prompt strings. See https://github.com/openai/codex.
|
|
43
|
+
function codexRootDir() {
|
|
44
|
+
const explicit = process.env.CODEX_HOME;
|
|
45
|
+
if (explicit)
|
|
46
|
+
return path.resolve(explicit);
|
|
47
|
+
return path.join(os.homedir(), ".codex");
|
|
48
|
+
}
|
|
49
|
+
function codexRootDisplay() {
|
|
50
|
+
return process.env.CODEX_HOME ? `$CODEX_HOME=${process.env.CODEX_HOME}` : "~/.codex";
|
|
51
|
+
}
|
|
52
|
+
exports.codexAdapter = (0, _shared_1.makeFolderInstallAdapter)({
|
|
53
|
+
rootDir: codexRootDir,
|
|
54
|
+
rootDisplay: codexRootDisplay,
|
|
55
|
+
agentFlag: "codex",
|
|
56
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
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 () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.cursorAdapter = exports.PROVENANCE_RE = void 0;
|
|
37
|
+
exports.provenanceMarker = provenanceMarker;
|
|
38
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const _shared_1 = require("./_shared");
|
|
41
|
+
// Cursor's project-scoped rules live at <cwd>/.cursor/rules/*.mdc with a
|
|
42
|
+
// distinct frontmatter shape (description / globs / alwaysApply). User Rules
|
|
43
|
+
// are settings-only and not programmatically writable from an external CLI,
|
|
44
|
+
// so this adapter targets Project Rules only.
|
|
45
|
+
//
|
|
46
|
+
// Unlike Claude Code + Codex (per-user folder containing SKILL.md + examples/),
|
|
47
|
+
// Cursor installs a SINGLE .mdc file. The skill body is concatenated under a
|
|
48
|
+
// Cursor-shaped frontmatter; examples are NOT bundled locally — the skill body
|
|
49
|
+
// references their public raw GitHub URLs and the agent fetches them at
|
|
50
|
+
// diagram-authoring time.
|
|
51
|
+
//
|
|
52
|
+
// See https://cursor.com/docs/context/rules for the format reference.
|
|
53
|
+
const RULE_BASENAME = "arch-skill.mdc";
|
|
54
|
+
const RULE_DESCRIPTION = "Use this rule when drawing Microsoft Azure or Microsoft Fabric architecture diagrams using PlantUML. " +
|
|
55
|
+
"Triggers on \"draw Azure architecture\", \"draw Azure diagram\", \"create deployment diagram\", " +
|
|
56
|
+
"\"Lakehouse + Notebook + Warehouse diagram\", \"PlantUML diagram for [project]\". " +
|
|
57
|
+
"Also triggers on the Vietnamese phrase \"vẽ Azure\".";
|
|
58
|
+
// Cursor's rule discovery is per-project: <cwd>/.cursor/rules/*.mdc is the
|
|
59
|
+
// canonical install location, so the team picks up the rule via the project's
|
|
60
|
+
// git repo. A HOME-relative install would only help the local developer. The
|
|
61
|
+
// trade-off — running install from an unintended cwd — is mitigated by a
|
|
62
|
+
// runtime warn-line (see `install` below) and the `--target=<path>` override.
|
|
63
|
+
function defaultTarget() {
|
|
64
|
+
return path.join(process.cwd(), ".cursor", "rules");
|
|
65
|
+
}
|
|
66
|
+
const CWD_DISPLAY = "<cwd>/.cursor/rules";
|
|
67
|
+
async function resolveTarget(target) {
|
|
68
|
+
return (0, _shared_1.safeResolveTarget)(target, process.cwd(), CWD_DISPLAY);
|
|
69
|
+
}
|
|
70
|
+
// `provenanceMarker` and `PROVENANCE_RE` are paired: the regex MUST parse what
|
|
71
|
+
// the marker writes. Keep them in lockstep — a unit test in version.test.ts
|
|
72
|
+
// asserts the round-trip.
|
|
73
|
+
function provenanceMarker(version, requiresIcons) {
|
|
74
|
+
return `<!-- ${_shared_1.SKILL_NAME} v${version} (requires_icons: ${requiresIcons}) -->`;
|
|
75
|
+
}
|
|
76
|
+
const PROVENANCE_RE = new RegExp(`<!--\\s*${_shared_1.SKILL_NAME}\\s+v([^\\s]+)\\s+\\(requires_icons:\\s*([^)]+)\\)\\s*-->`);
|
|
77
|
+
exports.PROVENANCE_RE = PROVENANCE_RE;
|
|
78
|
+
function renderRule(skillBody, version, requiresIcons) {
|
|
79
|
+
return [
|
|
80
|
+
"---",
|
|
81
|
+
`description: ${RULE_DESCRIPTION}`,
|
|
82
|
+
"alwaysApply: false",
|
|
83
|
+
"---",
|
|
84
|
+
"",
|
|
85
|
+
provenanceMarker(version, requiresIcons),
|
|
86
|
+
"",
|
|
87
|
+
skillBody.trimStart(),
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
async function isOurRuleFile(file) {
|
|
91
|
+
try {
|
|
92
|
+
const body = await fs.readFile(file, "utf8");
|
|
93
|
+
const match = body.match(PROVENANCE_RE);
|
|
94
|
+
if (!match)
|
|
95
|
+
return { ours: false };
|
|
96
|
+
return { ours: true, version: match[1] };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { ours: false };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function install(opts) {
|
|
103
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
104
|
+
if (opts.target === undefined) {
|
|
105
|
+
process.stderr.write(`note: Cursor target resolved to ${defaultTarget()} (per-project install). Pass --target=<path> to override.\n`);
|
|
106
|
+
}
|
|
107
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
108
|
+
const base = (0, _shared_1.baseUrl)(opts.version);
|
|
109
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
110
|
+
// Fetch manifest first so we know which file is the canonical SKILL.md
|
|
111
|
+
// and what version / requires_icons to embed in the provenance marker.
|
|
112
|
+
const manifest = await (0, _shared_1.fetchManifest)(base);
|
|
113
|
+
if (manifest.name !== _shared_1.SKILL_NAME) {
|
|
114
|
+
throw new Error(`manifest name mismatch: expected '${_shared_1.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
115
|
+
}
|
|
116
|
+
const skillFile = manifest.files[0];
|
|
117
|
+
const skillUrl = `${base}/${skillFile.src}`;
|
|
118
|
+
const skillMd = await (0, _shared_1.fetchText)(skillUrl);
|
|
119
|
+
// Integrity gate (verify-before-write): the Cursor adapter doesn't use
|
|
120
|
+
// makeFolderInstallAdapter, so it must apply the same sha256 check the
|
|
121
|
+
// folder adapters get for free. Keeps the manifest-checksum guarantee
|
|
122
|
+
// uniform across all three adapters.
|
|
123
|
+
(0, _shared_1.verifyBundleFile)(skillFile, skillMd);
|
|
124
|
+
const fm = (0, _shared_1.parseFrontmatter)(skillMd);
|
|
125
|
+
if (!fm.requires_icons) {
|
|
126
|
+
throw new Error("SKILL.md missing requires_icons frontmatter");
|
|
127
|
+
}
|
|
128
|
+
await (0, _shared_1.verifyIconsAvailability)(base, manifest, opts.version);
|
|
129
|
+
const exists = await fs.stat(ruleFile).then(() => true).catch(() => false);
|
|
130
|
+
if (exists && !opts.overwrite) {
|
|
131
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
132
|
+
throw new Error(probe.ours
|
|
133
|
+
? `${ruleFile} already contains an install. Run 'arch-skill update --agent=cursor' to refresh.`
|
|
134
|
+
: `${ruleFile} exists but is not one of ours (no '${_shared_1.SKILL_NAME}' provenance marker). Move/rename the file or remove it manually if intentional.`);
|
|
135
|
+
}
|
|
136
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
137
|
+
const body = (0, _shared_1.stripFrontmatter)(skillMd);
|
|
138
|
+
const rendered = renderRule(body, fm.version ?? manifest.version, fm.requires_icons);
|
|
139
|
+
await fs.writeFile(ruleFile, rendered, "utf8");
|
|
140
|
+
process.stdout.write(`installed ${_shared_1.SKILL_NAME} to ${ruleFile}\n`);
|
|
141
|
+
return 0;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function uninstall(opts) {
|
|
145
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
146
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
147
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
148
|
+
const exists = await fs.stat(ruleFile).then(() => true).catch(() => false);
|
|
149
|
+
if (!exists) {
|
|
150
|
+
process.stdout.write(`(nothing to uninstall at ${ruleFile})\n`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
154
|
+
if (!probe.ours) {
|
|
155
|
+
throw new Error(`refusing to remove ${ruleFile} - missing '${_shared_1.SKILL_NAME}' provenance marker. Move/rename the file or remove it manually if intentional.`);
|
|
156
|
+
}
|
|
157
|
+
await fs.unlink(ruleFile);
|
|
158
|
+
process.stdout.write(`uninstalled ${_shared_1.SKILL_NAME} from ${ruleFile}\n`);
|
|
159
|
+
return 0;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function update(opts) {
|
|
163
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
164
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
165
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
166
|
+
const base = (0, _shared_1.baseUrl)(opts.version);
|
|
167
|
+
// Already-at-version short-circuit (parity with the folder-install
|
|
168
|
+
// adapters). Read the installed .mdc, extract version from the
|
|
169
|
+
// provenance marker, compare to the upstream manifest's version.
|
|
170
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
171
|
+
if (probe.ours && probe.version) {
|
|
172
|
+
const manifest = await (0, _shared_1.fetchManifest)(base);
|
|
173
|
+
if (manifest.name !== _shared_1.SKILL_NAME) {
|
|
174
|
+
throw new Error(`manifest name mismatch: expected '${_shared_1.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
|
|
175
|
+
}
|
|
176
|
+
if (probe.version === manifest.version) {
|
|
177
|
+
process.stdout.write(`${_shared_1.SKILL_NAME} already at version ${manifest.version} (no-op)\n`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Otherwise, overwriting install.
|
|
182
|
+
return install({ ...opts, overwrite: true });
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function list(opts) {
|
|
186
|
+
return (0, _shared_1.withFatalReturn)(async () => {
|
|
187
|
+
const targetDir = await resolveTarget(opts.target ?? defaultTarget());
|
|
188
|
+
const ruleFile = path.join(targetDir, RULE_BASENAME);
|
|
189
|
+
const probe = await isOurRuleFile(ruleFile);
|
|
190
|
+
if (!probe.ours) {
|
|
191
|
+
process.stdout.write("(no skills installed)\n");
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(`${_shared_1.SKILL_NAME}\t${probe.version ?? "?"}\n`);
|
|
195
|
+
return 0;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
exports.cursorAdapter = { install, uninstall, update, list };
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_TARGETS = exports.SUPPORTED_AGENTS = exports.ADAPTERS = void 0;
|
|
3
|
+
exports.SUPPORTED_TARGETS = exports.ALL_TARGET = exports.SUPPORTED_AGENTS = exports.ADAPTERS = void 0;
|
|
4
4
|
const claude_code_1 = require("./claude-code");
|
|
5
|
+
const codex_1 = require("./codex");
|
|
6
|
+
const cursor_1 = require("./cursor");
|
|
5
7
|
// Single source of truth for the supported-agent set. Add a new adapter by
|
|
6
8
|
// importing it here and adding one entry below; both `src/index.ts` (CLI
|
|
7
|
-
// dispatch + --agent help text)
|
|
8
|
-
//
|
|
9
|
-
// Only the Claude Code adapter ships in this phase; Codex / Cursor / the
|
|
10
|
-
// `--agent=all` fan-out land in a later port.
|
|
9
|
+
// dispatch + --agent help text) and `adapters-roundtrip.test.ts` (test loop)
|
|
10
|
+
// read from this map directly.
|
|
11
11
|
exports.ADAPTERS = {
|
|
12
12
|
"claude-code": claude_code_1.claudeCodeAdapter,
|
|
13
|
+
"codex": codex_1.codexAdapter,
|
|
14
|
+
"cursor": cursor_1.cursorAdapter,
|
|
13
15
|
};
|
|
14
16
|
exports.SUPPORTED_AGENTS = Object.keys(exports.ADAPTERS);
|
|
17
|
+
/** Sentinel value for `--agent=all` — iterate every adapter under one command. */
|
|
18
|
+
exports.ALL_TARGET = "all";
|
|
15
19
|
/** Full target set the CLI's `--agent` flag accepts. */
|
|
16
|
-
exports.SUPPORTED_TARGETS = [...exports.SUPPORTED_AGENTS];
|
|
20
|
+
exports.SUPPORTED_TARGETS = [...exports.SUPPORTED_AGENTS, exports.ALL_TARGET];
|
package/dist/all.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runOverAll = runOverAll;
|
|
4
|
+
const registry_1 = require("./adapters/registry");
|
|
5
|
+
async function runOverAll(sub, opts) {
|
|
6
|
+
if (registry_1.SUPPORTED_AGENTS.length === 0) {
|
|
7
|
+
throw new Error("no adapters loaded in registry (registry.ts ADAPTERS is empty)");
|
|
8
|
+
}
|
|
9
|
+
const completed = [];
|
|
10
|
+
const successes = [];
|
|
11
|
+
const failures = [];
|
|
12
|
+
const isTransactional = sub === "install" || sub === "update";
|
|
13
|
+
for (const agent of registry_1.SUPPORTED_AGENTS) {
|
|
14
|
+
const adapter = registry_1.ADAPTERS[agent];
|
|
15
|
+
const outcome = await runOne(adapter, agent, sub, opts);
|
|
16
|
+
if (outcome.exit === 0) {
|
|
17
|
+
successes.push(agent);
|
|
18
|
+
if (isTransactional)
|
|
19
|
+
completed.push(agent);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
failures.push(outcome);
|
|
23
|
+
if (isTransactional)
|
|
24
|
+
break; // halt + roll back below
|
|
25
|
+
// uninstall / list: best-effort, keep going
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const rolledBack = [];
|
|
29
|
+
if (isTransactional && failures.length > 0) {
|
|
30
|
+
for (const agent of completed.slice().reverse()) {
|
|
31
|
+
try {
|
|
32
|
+
const exit = await registry_1.ADAPTERS[agent].uninstall(opts);
|
|
33
|
+
if (exit === 0) {
|
|
34
|
+
rolledBack.push(agent);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
process.stderr.write(`fatal: rollback failed for ${agent}: uninstall exited ${exit}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
process.stderr.write(`fatal: rollback failed for ${agent}: ${msg}\n`);
|
|
43
|
+
// continue with the rest (best-effort cleanup)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
process.stderr.write(formatSummary(sub, successes, failures, rolledBack, isTransactional));
|
|
48
|
+
return failures.length > 0 ? 1 : 0;
|
|
49
|
+
}
|
|
50
|
+
async function runOne(adapter, agent, sub, opts) {
|
|
51
|
+
try {
|
|
52
|
+
// All four Adapter methods now accept the same AdapterOpts shape (see types.ts).
|
|
53
|
+
const exit = await adapter[sub](opts);
|
|
54
|
+
return { agent, exit };
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return { agent, exit: 1, error: err instanceof Error ? err : new Error(String(err)) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function formatSummary(sub, successes, failures, rolledBack, isTransactional) {
|
|
61
|
+
const fmt = (list, fallback) => `[${list.length > 0 ? list.join(", ") : fallback}]`;
|
|
62
|
+
const succLabel = fmt(successes, "(none)");
|
|
63
|
+
const failLabel = fmt(failures.map(f => f.agent), "(none)");
|
|
64
|
+
const rbLabel = isTransactional ? fmt(rolledBack, "(none)") : "[(n/a)]";
|
|
65
|
+
return `--agent=all (${sub}): succeeded=${succLabel} failed=${failLabel} rolled-back=${rbLabel}\n`;
|
|
66
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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 registry_1 = require("./adapters/registry");
|
|
10
|
+
const all_1 = require("./all");
|
|
10
11
|
const program = new commander_1.Command()
|
|
11
12
|
.name("arch-skill")
|
|
12
13
|
.description("Install the architecture diagram skill into your AI coding agent.")
|
|
@@ -35,6 +36,10 @@ function defineSubcommand(name, description) {
|
|
|
35
36
|
throw new Error(`--version must match X.Y.Z (got: ${opts.version})`);
|
|
36
37
|
}
|
|
37
38
|
const optsForAdapter = { target: opts.target, version: opts.version };
|
|
39
|
+
if (opts.agent === registry_1.ALL_TARGET) {
|
|
40
|
+
process.exitCode = await (0, all_1.runOverAll)(name, optsForAdapter);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
38
43
|
if (!registry_1.SUPPORTED_AGENTS.includes(opts.agent)) {
|
|
39
44
|
throw new Error(`unknown agent: ${opts.agent} (supported: ${registry_1.SUPPORTED_AGENTS.join(", ")})`);
|
|
40
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanv89/arch-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Install the architecture-diagram skill into your AI coding agent (Claude Code, Codex CLI, Cursor).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"arch-skill": "dist/index.js"
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
|
-
"test": "tsc && node --test dist/**/*.test.js",
|
|
19
|
-
"test:coverage": "tsc && node --test --experimental-test-coverage dist/**/*.test.js",
|
|
18
|
+
"test": "tsc && node --test dist/**/*.test.js dist/*.test.js",
|
|
19
|
+
"test:coverage": "tsc && node --test --experimental-test-coverage dist/**/*.test.js dist/*.test.js",
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|