@ai-plugin-marketplace/core 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/core.d.ts +291 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/build.d.ts +90 -0
- package/dist/pipeline/build.d.ts.map +1 -0
- package/dist/pipeline/build.js +224 -0
- package/dist/pipeline/build.js.map +1 -0
- package/dist/pipeline/discover.d.ts +37 -0
- package/dist/pipeline/discover.d.ts.map +1 -0
- package/dist/pipeline/discover.js +71 -0
- package/dist/pipeline/discover.js.map +1 -0
- package/dist/pipeline/init-template.d.ts +33 -0
- package/dist/pipeline/init-template.d.ts.map +1 -0
- package/dist/pipeline/init-template.js +142 -0
- package/dist/pipeline/init-template.js.map +1 -0
- package/dist/pipeline/init.d.ts +39 -0
- package/dist/pipeline/init.d.ts.map +1 -0
- package/dist/pipeline/init.js +84 -0
- package/dist/pipeline/init.js.map +1 -0
- package/dist/pipeline/load-config.d.ts +47 -0
- package/dist/pipeline/load-config.d.ts.map +1 -0
- package/dist/pipeline/load-config.js +106 -0
- package/dist/pipeline/load-config.js.map +1 -0
- package/dist/pipeline/operations.d.ts +70 -0
- package/dist/pipeline/operations.d.ts.map +1 -0
- package/dist/pipeline/operations.js +100 -0
- package/dist/pipeline/operations.js.map +1 -0
- package/dist/pipeline/scaffold.d.ts +105 -0
- package/dist/pipeline/scaffold.d.ts.map +1 -0
- package/dist/pipeline/scaffold.js +422 -0
- package/dist/pipeline/scaffold.js.map +1 -0
- package/dist/pipeline/sentinel.d.ts +127 -0
- package/dist/pipeline/sentinel.d.ts.map +1 -0
- package/dist/pipeline/sentinel.js +263 -0
- package/dist/pipeline/sentinel.js.map +1 -0
- package/dist/pipeline/types.d.ts +178 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +26 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/pipeline/validate.d.ts +90 -0
- package/dist/pipeline/validate.d.ts.map +1 -0
- package/dist/pipeline/validate.js +617 -0
- package/dist/pipeline/validate.js.map +1 -0
- package/dist/targets/claude/scaffold.d.ts +32 -0
- package/dist/targets/claude/scaffold.d.ts.map +1 -0
- package/dist/targets/claude/scaffold.js +48 -0
- package/dist/targets/claude/scaffold.js.map +1 -0
- package/dist/targets/claude/schemas.d.ts +119 -0
- package/dist/targets/claude/schemas.d.ts.map +1 -0
- package/dist/targets/claude/schemas.js +204 -0
- package/dist/targets/claude/schemas.js.map +1 -0
- package/dist/targets/claude/transform.d.ts +40 -0
- package/dist/targets/claude/transform.d.ts.map +1 -0
- package/dist/targets/claude/transform.js +48 -0
- package/dist/targets/claude/transform.js.map +1 -0
- package/dist/targets/claude/validate.d.ts +25 -0
- package/dist/targets/claude/validate.d.ts.map +1 -0
- package/dist/targets/claude/validate.js +263 -0
- package/dist/targets/claude/validate.js.map +1 -0
- package/dist/targets/cursor/scaffold.d.ts +29 -0
- package/dist/targets/cursor/scaffold.d.ts.map +1 -0
- package/dist/targets/cursor/scaffold.js +45 -0
- package/dist/targets/cursor/scaffold.js.map +1 -0
- package/dist/targets/cursor/schemas.d.ts +49 -0
- package/dist/targets/cursor/schemas.d.ts.map +1 -0
- package/dist/targets/cursor/schemas.js +125 -0
- package/dist/targets/cursor/schemas.js.map +1 -0
- package/dist/targets/cursor/validate.d.ts +28 -0
- package/dist/targets/cursor/validate.d.ts.map +1 -0
- package/dist/targets/cursor/validate.js +181 -0
- package/dist/targets/cursor/validate.js.map +1 -0
- package/dist/targets/gemini/bundle.d.ts +25 -0
- package/dist/targets/gemini/bundle.d.ts.map +1 -0
- package/dist/targets/gemini/bundle.js +149 -0
- package/dist/targets/gemini/bundle.js.map +1 -0
- package/dist/targets/gemini/scaffold.d.ts +28 -0
- package/dist/targets/gemini/scaffold.d.ts.map +1 -0
- package/dist/targets/gemini/scaffold.js +57 -0
- package/dist/targets/gemini/scaffold.js.map +1 -0
- package/dist/targets/gemini/schemas.d.ts +53 -0
- package/dist/targets/gemini/schemas.d.ts.map +1 -0
- package/dist/targets/gemini/schemas.js +72 -0
- package/dist/targets/gemini/schemas.js.map +1 -0
- package/dist/targets/gemini/transform.d.ts +106 -0
- package/dist/targets/gemini/transform.d.ts.map +1 -0
- package/dist/targets/gemini/transform.js +137 -0
- package/dist/targets/gemini/transform.js.map +1 -0
- package/dist/targets/gemini/validate.d.ts +26 -0
- package/dist/targets/gemini/validate.d.ts.map +1 -0
- package/dist/targets/gemini/validate.js +146 -0
- package/dist/targets/gemini/validate.js.map +1 -0
- package/dist/targets/kiro/bundle.d.ts +32 -0
- package/dist/targets/kiro/bundle.d.ts.map +1 -0
- package/dist/targets/kiro/bundle.js +106 -0
- package/dist/targets/kiro/bundle.js.map +1 -0
- package/dist/targets/kiro/scaffold.d.ts +28 -0
- package/dist/targets/kiro/scaffold.d.ts.map +1 -0
- package/dist/targets/kiro/scaffold.js +55 -0
- package/dist/targets/kiro/scaffold.js.map +1 -0
- package/dist/targets/kiro/schemas.d.ts +100 -0
- package/dist/targets/kiro/schemas.d.ts.map +1 -0
- package/dist/targets/kiro/schemas.js +147 -0
- package/dist/targets/kiro/schemas.js.map +1 -0
- package/dist/targets/kiro/transform.d.ts +53 -0
- package/dist/targets/kiro/transform.d.ts.map +1 -0
- package/dist/targets/kiro/transform.js +113 -0
- package/dist/targets/kiro/transform.js.map +1 -0
- package/dist/targets/kiro/validate.d.ts +36 -0
- package/dist/targets/kiro/validate.d.ts.map +1 -0
- package/dist/targets/kiro/validate.js +232 -0
- package/dist/targets/kiro/validate.js.map +1 -0
- package/dist/targets/scaffold-kit.d.ts +56 -0
- package/dist/targets/scaffold-kit.d.ts.map +1 -0
- package/dist/targets/scaffold-kit.js +33 -0
- package/dist/targets/scaffold-kit.js.map +1 -0
- package/dist/targets/vercel/scaffold.d.ts +34 -0
- package/dist/targets/vercel/scaffold.d.ts.map +1 -0
- package/dist/targets/vercel/scaffold.js +58 -0
- package/dist/targets/vercel/scaffold.js.map +1 -0
- package/dist/targets/vercel/schemas.d.ts +42 -0
- package/dist/targets/vercel/schemas.d.ts.map +1 -0
- package/dist/targets/vercel/schemas.js +69 -0
- package/dist/targets/vercel/schemas.js.map +1 -0
- package/dist/targets/vercel/validate.d.ts +28 -0
- package/dist/targets/vercel/validate.d.ts.map +1 -0
- package/dist/targets/vercel/validate.js +180 -0
- package/dist/targets/vercel/validate.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline orchestration for scaffolding and compatibility-assist (§6.4).
|
|
3
|
+
*
|
|
4
|
+
* Three orchestrators perform filesystem I/O around the per-target *pure* scaffold functions:
|
|
5
|
+
*
|
|
6
|
+
* - `runScaffold` — create a brand-new plugin (the `aipm scaffold` surface).
|
|
7
|
+
* - `runAddTarget` — add one target's skeleton to an existing plugin (`aipm add-target`).
|
|
8
|
+
* - `runCheckSupport` — report declared-but-incomplete and plausibly-addable targets
|
|
9
|
+
* (`aipm check-support`).
|
|
10
|
+
*
|
|
11
|
+
* Per §3.4/§12.4 the pipeline layer is the orchestration boundary and MAY import each target's
|
|
12
|
+
* `scaffold.ts`; the per-target scaffold modules themselves never import one another.
|
|
13
|
+
*
|
|
14
|
+
* Envelope reading (§6.1): loading and executing a TypeScript `aipm.config.ts` from disk is the
|
|
15
|
+
* build orchestrator's job and is not yet built. `runCheckSupport`/`runAddTarget` therefore read
|
|
16
|
+
* the declared targets *pragmatically* — by extracting the `targets: [...]` array literal out of
|
|
17
|
+
* the config source text (see `parseDeclaredTargets`). This tolerates the canonical
|
|
18
|
+
* `defineConfig({ ... })` form this module emits and common hand-authored variants, but it is a
|
|
19
|
+
* lexical heuristic, not a TS evaluation. Documented limitation: a config that computes `targets`
|
|
20
|
+
* dynamically (spread, variable reference, conditional) is not understood and yields an error.
|
|
21
|
+
*
|
|
22
|
+
* @see docs/specs/architecture.md §6 (support envelope)
|
|
23
|
+
* @see docs/specs/architecture.md §6.4 (compatibility-assist tooling)
|
|
24
|
+
* @see docs/specs/architecture.md §10.1 (validation contract — artifact tables reused here)
|
|
25
|
+
* @see docs/specs/architecture.md §12.5 (per-target scaffold.ts)
|
|
26
|
+
*/
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { scaffoldClaudeFiles } from '../targets/claude/scaffold.js';
|
|
30
|
+
import { scaffoldCursorFiles } from '../targets/cursor/scaffold.js';
|
|
31
|
+
import { scaffoldGeminiFiles } from '../targets/gemini/scaffold.js';
|
|
32
|
+
import { scaffoldKiroFiles } from '../targets/kiro/scaffold.js';
|
|
33
|
+
import { scaffoldVercelFiles } from '../targets/vercel/scaffold.js';
|
|
34
|
+
import { SCHEMA_VERSION } from '../targets/scaffold-kit.js';
|
|
35
|
+
import { TARGET_IDS } from './types.js';
|
|
36
|
+
import { TARGET_MIN_REQUIRED } from './validate.js';
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Per-target scaffold dispatch
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/** Map a target ID to its pure scaffold function. */
|
|
41
|
+
const TARGET_SCAFFOLDERS = {
|
|
42
|
+
claude: scaffoldClaudeFiles,
|
|
43
|
+
cursor: scaffoldCursorFiles,
|
|
44
|
+
gemini: scaffoldGeminiFiles,
|
|
45
|
+
kiro: scaffoldKiroFiles,
|
|
46
|
+
vercel: scaffoldVercelFiles,
|
|
47
|
+
};
|
|
48
|
+
const md = String.raw;
|
|
49
|
+
const ts = String.raw;
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Name validation (mirrors the slug rules enforced by the manifest schemas)
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Validate a plugin name against the canonical slug rules shared by every manifest schema:
|
|
55
|
+
* lowercase, starts with a letter, `[a-z0-9-]` only, no consecutive hyphens, no trailing hyphen.
|
|
56
|
+
*
|
|
57
|
+
* @throws Error with a clear message when the name is invalid.
|
|
58
|
+
*/
|
|
59
|
+
export function validatePluginName(name) {
|
|
60
|
+
if (name === '') {
|
|
61
|
+
throw new Error('Plugin name is required.');
|
|
62
|
+
}
|
|
63
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
64
|
+
throw new Error(`Invalid plugin name: "${name}". Names must be lowercase, start with a letter, and contain only letters, digits, and hyphens.`);
|
|
65
|
+
}
|
|
66
|
+
if (name.includes('--')) {
|
|
67
|
+
throw new Error(`Invalid plugin name: "${name}". Names must not contain consecutive hyphens.`);
|
|
68
|
+
}
|
|
69
|
+
if (name.endsWith('-')) {
|
|
70
|
+
throw new Error(`Invalid plugin name: "${name}". Names must not end with a hyphen.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// aipm.config.ts generation + parsing
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Render the canonical `aipm.config.ts` source for a plugin with the given targets.
|
|
78
|
+
*
|
|
79
|
+
* Emits a `defineConfig({ version, targets })` literal importing from the public package root
|
|
80
|
+
* (§8.1 — the only public subpath). Deterministic: stable target ordering, no timestamps.
|
|
81
|
+
*/
|
|
82
|
+
export function renderAipmConfig(targets) {
|
|
83
|
+
const ordered = orderTargets(targets);
|
|
84
|
+
const targetList = ordered.map((t) => `'${t}'`).join(', ');
|
|
85
|
+
return ts `import { defineConfig } from '@ai-plugin-marketplace/core';
|
|
86
|
+
|
|
87
|
+
export default defineConfig({
|
|
88
|
+
version: '${SCHEMA_VERSION}',
|
|
89
|
+
targets: [${targetList}],
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Extract the declared targets from `aipm.config.ts` source text.
|
|
95
|
+
*
|
|
96
|
+
* Lexical heuristic (see module doc): finds the first `targets:` key and parses the immediately
|
|
97
|
+
* following `[...]` array of single/double-quoted string literals. Only IDs in `TARGET_IDS` are
|
|
98
|
+
* returned; unknown IDs are ignored (the shape validator surfaces those separately).
|
|
99
|
+
*
|
|
100
|
+
* @returns Declared target IDs in canonical order, deduplicated.
|
|
101
|
+
* @throws Error when no `targets: [...]` array literal can be located.
|
|
102
|
+
*/
|
|
103
|
+
export function parseDeclaredTargets(configSource) {
|
|
104
|
+
// Match `targets` followed by optional whitespace, a colon, then a bracketed list.
|
|
105
|
+
const match = /targets\s*:\s*\[([^\]]*)\]/.exec(configSource);
|
|
106
|
+
if (!match) {
|
|
107
|
+
throw new Error('Could not locate a `targets: [...]` array literal in aipm.config.ts. ' +
|
|
108
|
+
'Dynamically-computed targets are not supported by the lexical envelope reader.');
|
|
109
|
+
}
|
|
110
|
+
const inner = match[1] ?? '';
|
|
111
|
+
const ids = [...inner.matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1] ?? '');
|
|
112
|
+
const known = new Set(TARGET_IDS);
|
|
113
|
+
const declared = ids.filter((id) => known.has(id));
|
|
114
|
+
return orderTargets([...new Set(declared)]);
|
|
115
|
+
}
|
|
116
|
+
/** Order a target set by the canonical `TARGET_IDS` ordering. Deterministic output. */
|
|
117
|
+
function orderTargets(targets) {
|
|
118
|
+
const present = new Set(targets);
|
|
119
|
+
return TARGET_IDS.filter((t) => present.has(t));
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Marketplace registration (§4.4, §10.1.4)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* The template-level marketplace registries that a plugin must be registered in, keyed by the
|
|
126
|
+
* target whose presence in the envelope requires registration (§4.4). Both registries live at the
|
|
127
|
+
* **repo root**, not inside any plugin: `<repoRoot>/.claude-plugin/marketplace.json` (Claude) and
|
|
128
|
+
* `<repoRoot>/.cursor-plugin/marketplace.json` (Cursor).
|
|
129
|
+
*
|
|
130
|
+
* Mirrors `validateMarketplaceRegistration` in `validate.ts`: those two targets, those two paths.
|
|
131
|
+
*/
|
|
132
|
+
const MARKETPLACE_REGISTRIES = [
|
|
133
|
+
{ target: 'claude', dir: '.claude-plugin' },
|
|
134
|
+
{ target: 'cursor', dir: '.cursor-plugin' },
|
|
135
|
+
];
|
|
136
|
+
/** The canonical `source` value for a plugin's registry entry (§4.4 — `./plugins/<name>`). */
|
|
137
|
+
function expectedSource(pluginName) {
|
|
138
|
+
return `./plugins/${pluginName}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Register `pluginName` in the registry file at `registryPath`, creating it if absent.
|
|
142
|
+
*
|
|
143
|
+
* Idempotent: if an entry whose `name` already matches `pluginName` exists, the file is left
|
|
144
|
+
* untouched (no duplicate, no source rewrite — repairing a wrong source is the validator's job to
|
|
145
|
+
* report, not the scaffolder's to silently mutate). Output is 2-space JSON with a trailing newline.
|
|
146
|
+
*
|
|
147
|
+
* Existing entries and any extra top-level keys are preserved.
|
|
148
|
+
*/
|
|
149
|
+
function registerInMarketplace(registryPath, pluginName) {
|
|
150
|
+
const entry = { name: pluginName, source: expectedSource(pluginName) };
|
|
151
|
+
let registry;
|
|
152
|
+
if (fs.existsSync(registryPath)) {
|
|
153
|
+
const raw = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
|
|
154
|
+
// Tolerate a missing/!object/!plugins file by normalizing to a registry with a plugins array.
|
|
155
|
+
registry =
|
|
156
|
+
typeof raw === 'object' && raw !== null ? raw : { plugins: [] };
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
registry = { plugins: [] };
|
|
160
|
+
}
|
|
161
|
+
const plugins = Array.isArray(registry.plugins) ? registry.plugins : [];
|
|
162
|
+
if (plugins.some((p) => p.name === pluginName)) {
|
|
163
|
+
return; // already registered — idempotent no-op
|
|
164
|
+
}
|
|
165
|
+
registry.plugins = [...plugins, entry];
|
|
166
|
+
writeFileEnsuringDir(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Register a plugin in every marketplace registry implied by its envelope (§4.4).
|
|
170
|
+
*
|
|
171
|
+
* For each of Claude/Cursor present in `targets`, create-or-append the plugin's entry in the
|
|
172
|
+
* corresponding `<repoRoot>/<dir>/marketplace.json`. Targets not in the envelope get no entry, so
|
|
173
|
+
* the scaffold→validate happy path stays clean (the validator forbids registration for
|
|
174
|
+
* undeclared targets).
|
|
175
|
+
*/
|
|
176
|
+
function registerPluginInMarketplaces(repoRoot, pluginName, targets) {
|
|
177
|
+
const envelope = new Set(targets);
|
|
178
|
+
for (const { target, dir } of MARKETPLACE_REGISTRIES) {
|
|
179
|
+
if (!envelope.has(target))
|
|
180
|
+
continue;
|
|
181
|
+
registerInMarketplace(path.join(repoRoot, dir, 'marketplace.json'), pluginName);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Filesystem helpers
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
/** Write `content` to `filePath`, creating parent directories. Does not check for existence. */
|
|
188
|
+
function writeFileEnsuringDir(filePath, content) {
|
|
189
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
190
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Canonical top-level files every scaffolded plugin gets, independent of targets (§4 layout).
|
|
194
|
+
* Deterministic: no clock/env reads — the LICENSE year is intentionally omitted.
|
|
195
|
+
*/
|
|
196
|
+
function canonicalRootFiles(pluginName, description) {
|
|
197
|
+
const readme = md `# ${pluginName}
|
|
198
|
+
|
|
199
|
+
${description}
|
|
200
|
+
|
|
201
|
+
## Installation
|
|
202
|
+
|
|
203
|
+
Install this plugin by copying it into your AI assistant's plugin directory.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
ISC
|
|
208
|
+
`;
|
|
209
|
+
const license = `ISC License
|
|
210
|
+
|
|
211
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
212
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
213
|
+
copyright notice and this permission notice appear in all copies.
|
|
214
|
+
|
|
215
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
216
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
217
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
218
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
219
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
220
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
221
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
222
|
+
`;
|
|
223
|
+
return [
|
|
224
|
+
{ path: 'README.md', content: readme },
|
|
225
|
+
{ path: 'LICENSE', content: license },
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// runScaffold
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
/**
|
|
232
|
+
* Create a brand-new plugin under `pluginsDir/<name>/`.
|
|
233
|
+
*
|
|
234
|
+
* Writes `aipm.config.ts` (via the `defineConfig` literal), each declared target's skeleton
|
|
235
|
+
* files, and the canonical `README.md` / `LICENSE`. Default targets are all known IDs when
|
|
236
|
+
* `opts.targets` is absent.
|
|
237
|
+
*
|
|
238
|
+
* Also registers the plugin in the template-level marketplace registries (§4.4) for each of
|
|
239
|
+
* Claude/Cursor in the envelope (`repoRoot = dirname(pluginsDir)`), so the scaffolded plugin
|
|
240
|
+
* passes `validate`'s `marketplace-registration` check (§10.1.4) out of the box.
|
|
241
|
+
*
|
|
242
|
+
* @throws Error when `name` is invalid or the plugin directory already exists.
|
|
243
|
+
*/
|
|
244
|
+
export async function runScaffold(name, pluginsDir, opts = {}) {
|
|
245
|
+
validatePluginName(name);
|
|
246
|
+
const pluginDir = path.join(pluginsDir, name);
|
|
247
|
+
if (fs.existsSync(pluginDir)) {
|
|
248
|
+
throw new Error(`Plugin directory already exists: ${pluginDir}`);
|
|
249
|
+
}
|
|
250
|
+
const targets = orderTargets(opts.targets ?? TARGET_IDS);
|
|
251
|
+
const description = opts.description ?? `A plugin for ${name}`;
|
|
252
|
+
const targetOpts = { description };
|
|
253
|
+
const files = [
|
|
254
|
+
{ path: 'aipm.config.ts', content: renderAipmConfig(targets) },
|
|
255
|
+
...canonicalRootFiles(name, description),
|
|
256
|
+
];
|
|
257
|
+
for (const target of targets) {
|
|
258
|
+
files.push(...TARGET_SCAFFOLDERS[target](name, targetOpts));
|
|
259
|
+
}
|
|
260
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
writeFileEnsuringDir(path.join(pluginDir, file.path), file.content);
|
|
263
|
+
}
|
|
264
|
+
// Register in the repo-root marketplace registries (§4.4). repoRoot is the parent of pluginsDir.
|
|
265
|
+
registerPluginInMarketplaces(path.dirname(pluginsDir), name, targets);
|
|
266
|
+
return Promise.resolve();
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// runAddTarget
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
/**
|
|
272
|
+
* Scaffold one target's skeleton files into an existing plugin (§6.4 `aipm add-target`).
|
|
273
|
+
*
|
|
274
|
+
* Manifest fields are emitted as placeholders for the author to complete. Existing files are
|
|
275
|
+
* NEVER clobbered: if any file this target would write already exists, the function refuses and
|
|
276
|
+
* throws — the author resolves the conflict deliberately.
|
|
277
|
+
*
|
|
278
|
+
* The plugin's `aipm.config.ts` is updated to include `target` in its `targets` array when the
|
|
279
|
+
* lexical reader can locate the array (see module doc). Limitation: if the array cannot be parsed
|
|
280
|
+
* (dynamic/computed targets), the config is left untouched and the error message instructs the
|
|
281
|
+
* author to add the target manually — the function never leaves the envelope silently
|
|
282
|
+
* inconsistent.
|
|
283
|
+
*
|
|
284
|
+
* When `target` is Claude or Cursor, the plugin is also registered in the corresponding
|
|
285
|
+
* repo-root marketplace registry (§4.4) so adding the target keeps `validate` green. repoRoot is
|
|
286
|
+
* the plugin's grandparent (`<repoRoot>/plugins/<name>`), matching `discoverPlugins`.
|
|
287
|
+
*
|
|
288
|
+
* @throws Error when `pluginDir` does not exist, a target file already exists, or the config's
|
|
289
|
+
* targets array cannot be located for the update.
|
|
290
|
+
*/
|
|
291
|
+
export async function runAddTarget(pluginDir, target) {
|
|
292
|
+
if (!fs.existsSync(pluginDir)) {
|
|
293
|
+
throw new Error(`Plugin directory does not exist: ${pluginDir}`);
|
|
294
|
+
}
|
|
295
|
+
const pluginName = path.basename(pluginDir);
|
|
296
|
+
const files = TARGET_SCAFFOLDERS[target](pluginName, { placeholder: true });
|
|
297
|
+
// Refuse to overwrite: collect conflicts before writing anything.
|
|
298
|
+
const conflicts = files
|
|
299
|
+
.map((f) => f.path)
|
|
300
|
+
.filter((rel) => fs.existsSync(path.join(pluginDir, rel)));
|
|
301
|
+
if (conflicts.length > 0) {
|
|
302
|
+
throw new Error(`Refusing to overwrite existing files while adding target '${target}': ${conflicts.join(', ')}. ` +
|
|
303
|
+
'Remove or move them first, then re-run.');
|
|
304
|
+
}
|
|
305
|
+
// Update the envelope BEFORE writing skeleton files, so a config we cannot parse aborts the
|
|
306
|
+
// whole operation rather than leaving orphan files with an unchanged envelope.
|
|
307
|
+
updateConfigTargets(pluginDir, target);
|
|
308
|
+
for (const file of files) {
|
|
309
|
+
writeFileEnsuringDir(path.join(pluginDir, file.path), file.content);
|
|
310
|
+
}
|
|
311
|
+
// Register in the repo-root marketplace registry when adding a registry-backed target (§4.4).
|
|
312
|
+
// repoRoot = dirname(dirname(pluginDir)) — the `<repoRoot>/plugins/<name>` grandparent.
|
|
313
|
+
registerPluginInMarketplaces(path.dirname(path.dirname(pluginDir)), pluginName, [target]);
|
|
314
|
+
return Promise.resolve();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Add `target` to the `targets` array in `pluginDir/aipm.config.ts`, rewriting the file.
|
|
318
|
+
*
|
|
319
|
+
* Idempotent: if `target` is already declared, the file is left byte-for-byte unchanged.
|
|
320
|
+
* Always re-renders the array in canonical order. Throws (rather than silently skipping) when the
|
|
321
|
+
* config is missing or the array cannot be located, so the caller never proceeds with an
|
|
322
|
+
* inconsistent envelope.
|
|
323
|
+
*/
|
|
324
|
+
function updateConfigTargets(pluginDir, target) {
|
|
325
|
+
const configPath = path.join(pluginDir, 'aipm.config.ts');
|
|
326
|
+
if (!fs.existsSync(configPath)) {
|
|
327
|
+
throw new Error(`Cannot add target '${target}': no aipm.config.ts found at ${configPath}. ` +
|
|
328
|
+
'Create one declaring the support envelope first.');
|
|
329
|
+
}
|
|
330
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
331
|
+
const declared = parseDeclaredTargets(source); // throws if no array literal located
|
|
332
|
+
if (declared.includes(target)) {
|
|
333
|
+
return; // already declared — nothing to do
|
|
334
|
+
}
|
|
335
|
+
const next = orderTargets([...declared, target]);
|
|
336
|
+
const targetList = next.map((t) => `'${t}'`).join(', ');
|
|
337
|
+
const rewritten = source.replace(/targets\s*:\s*\[[^\]]*\]/, `targets: [${targetList}]`);
|
|
338
|
+
fs.writeFileSync(configPath, rewritten, 'utf-8');
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// runCheckSupport
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
/**
|
|
344
|
+
* Report a plugin's support posture (§6.4 `aipm check-support`).
|
|
345
|
+
*
|
|
346
|
+
* Reads the declared envelope from `aipm.config.ts` (lexically — see module doc), then:
|
|
347
|
+
* - `missingArtifacts`: for each declared target, the `TARGET_MIN_REQUIRED` files absent on disk
|
|
348
|
+
* (Vercel's "at least one SKILL.md under a skills subdirectory" rule is applied specially,
|
|
349
|
+
* mirroring the
|
|
350
|
+
* validator). Only targets with at least one missing artifact appear.
|
|
351
|
+
* - `suggestions`: each undeclared known target, with `wouldNeed` = that target's min-required
|
|
352
|
+
* files (the concrete list of files the author would write to add it).
|
|
353
|
+
*
|
|
354
|
+
* Reuses the validator's `TARGET_MIN_REQUIRED` table so "missing" stays consistent with
|
|
355
|
+
* `aipm validate`.
|
|
356
|
+
*
|
|
357
|
+
* @throws Error when `pluginDir` or its `aipm.config.ts` is missing, or the envelope is unreadable.
|
|
358
|
+
*/
|
|
359
|
+
export async function runCheckSupport(pluginDir) {
|
|
360
|
+
if (!fs.existsSync(pluginDir)) {
|
|
361
|
+
throw new Error(`Plugin directory does not exist: ${pluginDir}`);
|
|
362
|
+
}
|
|
363
|
+
const configPath = path.join(pluginDir, 'aipm.config.ts');
|
|
364
|
+
if (!fs.existsSync(configPath)) {
|
|
365
|
+
throw new Error(`No aipm.config.ts found at ${configPath}.`);
|
|
366
|
+
}
|
|
367
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
368
|
+
const declared = parseDeclaredTargets(source);
|
|
369
|
+
const declaredSet = new Set(declared);
|
|
370
|
+
const missingArtifacts = [];
|
|
371
|
+
for (const target of declared) {
|
|
372
|
+
const missing = missingMinRequired(pluginDir, target);
|
|
373
|
+
if (missing.length > 0) {
|
|
374
|
+
missingArtifacts.push({ target, missing });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const suggestions = TARGET_IDS.filter((t) => !declaredSet.has(t)).map((target) => ({ target, wouldNeed: wouldNeedArtifacts(target) }));
|
|
378
|
+
return Promise.resolve({
|
|
379
|
+
plugin: path.basename(pluginDir),
|
|
380
|
+
declared,
|
|
381
|
+
missingArtifacts,
|
|
382
|
+
suggestions,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The min-required artifacts for `target` that are absent under `pluginDir`.
|
|
387
|
+
*
|
|
388
|
+
* Vercel has no entry in `TARGET_MIN_REQUIRED` (its requirement is "at least one
|
|
389
|
+
* a SKILL.md under a skills subdirectory", a directory-scan rule), so it is handled specially to
|
|
390
|
+
* match the validator.
|
|
391
|
+
*/
|
|
392
|
+
function missingMinRequired(pluginDir, target) {
|
|
393
|
+
if (target === 'vercel') {
|
|
394
|
+
return hasAnyVercelSkill(pluginDir) ? [] : ['skills/<skill-name>/SKILL.md'];
|
|
395
|
+
}
|
|
396
|
+
return TARGET_MIN_REQUIRED[target].filter((rel) => !fs.existsSync(path.join(pluginDir, rel)));
|
|
397
|
+
}
|
|
398
|
+
/** The concrete files an author would write to add `target` (mirrors `missingMinRequired`). */
|
|
399
|
+
function wouldNeedArtifacts(target) {
|
|
400
|
+
if (target === 'vercel') {
|
|
401
|
+
return ['skills/<skill-name>/SKILL.md'];
|
|
402
|
+
}
|
|
403
|
+
return [...TARGET_MIN_REQUIRED[target]];
|
|
404
|
+
}
|
|
405
|
+
/** True iff at least one `skills/<dir>/SKILL.md` exists one level under `pluginDir/skills`. */
|
|
406
|
+
function hasAnyVercelSkill(pluginDir) {
|
|
407
|
+
const skillsDir = path.join(pluginDir, 'skills');
|
|
408
|
+
if (!fs.existsSync(skillsDir))
|
|
409
|
+
return false;
|
|
410
|
+
try {
|
|
411
|
+
for (const dirent of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
412
|
+
if (dirent.isDirectory() && fs.existsSync(path.join(skillsDir, dirent.name, 'SKILL.md'))) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/pipeline/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAE9E,qDAAqD;AACrD,MAAM,kBAAkB,GAGpB;IACF,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,mBAAmB;IAC3B,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,mBAAmB;CAC5B,CAAC;AAEF,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC;AACtB,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC;AAEtB,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,iGAAiG,CAC/H,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,gDAAgD,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,sCAAsC,CAAC,CAAC;IACvF,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,sCAAsC;AACtC,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA4B;IAC3D,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,OAAO,EAAE,CAAA;;;cAGG,cAAc;cACd,UAAU;;CAEvB,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,YAAoB;IACvD,mFAAmF;IACnF,MAAM,KAAK,GAAG,4BAA4B,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC9D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,gFAAgF,CACnF,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,UAAU,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAkB,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACnE,OAAO,YAAY,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED,uFAAuF;AACvF,SAAS,YAAY,CAAC,OAA4B;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,2CAA2C;AAC3C,8EAA8E;AAE9E;;;;;;;GAOG;AACH,MAAM,sBAAsB,GAAwC;IAClE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,gBAAgB,EAAE;IAC3C,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,gBAAgB,EAAE;CAC5C,CAAC;AAcF,8FAA8F;AAC9F,SAAS,cAAc,CAAC,UAAkB;IACxC,OAAO,aAAa,UAAU,EAAE,CAAC;AACnC,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,qBAAqB,CAAC,YAAoB,EAAE,UAAkB;IACrE,MAAM,KAAK,GAAqB,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;IAEzF,IAAI,QAA6B,CAAC;IAClC,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAChC,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;QACxE,8FAA8F;QAC9F,QAAQ;YACN,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAE,GAA2B,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC7F,CAAC;SAAM,CAAC;QACN,QAAQ,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,EAAE,CAAC;QAC/C,OAAO,CAAC,wCAAwC;IAClD,CAAC;IAED,QAAQ,CAAC,OAAO,GAAG,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,CAAC;IAEvC,oBAAoB,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,4BAA4B,CACnC,QAAgB,EAChB,UAAkB,EAClB,OAA4B;IAE5B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,KAAK,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,sBAAsB,EAAE,CAAC;QACrD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,SAAS;QACpC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,kBAAkB,CAAC,EAAE,UAAU,CAAC,CAAC;IAClF,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,gGAAgG;AAChG,SAAS,oBAAoB,CAAC,QAAgB,EAAE,OAAe;IAC7D,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,UAAkB,EAAE,WAAmB;IACjE,MAAM,MAAM,GAAG,EAAE,CAAA,KAAK,UAAU;;EAEhC,WAAW;;;;;;;;;CASZ,CAAC;IAEA,MAAM,OAAO,GAAG;;;;;;;;;;;;;CAajB,CAAC;IAEA,OAAO;QACL,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE;QACtC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE;KACtC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAY,EACZ,UAAkB,EAClB,OAAwB,EAAE;IAE1B,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAEzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,oCAAoC,SAAS,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,IAAI,UAAU,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,gBAAgB,IAAI,EAAE,CAAC;IAC/D,MAAM,UAAU,GAA0B,EAAE,WAAW,EAAE,CAAC;IAE1D,MAAM,KAAK,GAAqB;QAC9B,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC9D,GAAG,kBAAkB,CAAC,IAAI,EAAE,WAAW,CAAC;KACzC,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtE,CAAC;IAED,iGAAiG;IACjG,4BAA4B,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAEtE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,SAAiB,EAAE,MAAgB;IACpE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,oCAAoC,SAAS,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5E,kEAAkE;IAClE,MAAM,SAAS,GAAG,KAAK;SACpB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,6DAA6D,MAAM,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC/F,yCAAyC,CAC5C,CAAC;IACJ,CAAC;IAED,4FAA4F;IAC5F,+EAA+E;IAC/E,mBAAmB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAEvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtE,CAAC;IAED,8FAA8F;IAC9F,wFAAwF;IACxF,4BAA4B,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAE1F,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,mBAAmB,CAAC,SAAiB,EAAE,MAAgB;IAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAC1D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,sBAAsB,MAAM,iCAAiC,UAAU,IAAI;YACzE,kDAAkD,CACrD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,qCAAqC;IACpF,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,mCAAmC;IAC7C,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,0BAA0B,EAAE,aAAa,UAAU,GAAG,CAAC,CAAC;IACzF,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,oCAAoC,SAAS,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAC1D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,GAAG,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtC,MAAM,gBAAgB,GAAsC,EAAE,CAAC;IAC/D,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAiC,UAAU,CAAC,MAAM,CACjE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAC3B,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAEvE,OAAO,OAAO,CAAC,OAAO,CAAC;QACrB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAChC,QAAQ;QACR,gBAAgB;QAChB,WAAW;KACZ,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,SAAiB,EAAE,MAAgB;IAC7D,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AAChG,CAAC;AAED,+FAA+F;AAC/F,SAAS,kBAAkB,CAAC,MAAgB;IAC1C,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,CAAC,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,+FAA+F;AAC/F,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACxE,IAAI,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;gBACzF,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated-file sentinels.
|
|
3
|
+
*
|
|
4
|
+
* Per §4.3 of the architecture spec, every toolkit-generated file carries a sentinel so
|
|
5
|
+
* hand-edits are detectable by the freshness check (§10.5). There are three carriers:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Inline text** — a comment block prepended to plain-text generated files.
|
|
8
|
+
* 2. **JSON `_generated` field** — a top-level object on JSON outputs that tolerate extra keys.
|
|
9
|
+
* 3. **Sidecar `<artifact>.generated`** — a companion file holding the inline-text body, used
|
|
10
|
+
* for strict-schema hosts that reject unknown top-level fields (Gemini's
|
|
11
|
+
* `gemini-extension.json`, Kiro's `.kiro/agents/*.json`).
|
|
12
|
+
*
|
|
13
|
+
* This module is **pure**: string/object in, string out. It performs no filesystem I/O — the
|
|
14
|
+
* build (§5.2) and freshness (§10.5) layers own all reads and writes. It is internal to
|
|
15
|
+
* `@ai-plugin-marketplace/core` and intentionally NOT re-exported from the package root.
|
|
16
|
+
*
|
|
17
|
+
* Round-trip invariants hold for every carrier:
|
|
18
|
+
* - `stripSentinel(applyXSentinel(x, src), mode) === x`
|
|
19
|
+
* - `readSentinelSource(applyXSentinel(x, src), mode) === src`
|
|
20
|
+
*
|
|
21
|
+
* @see docs/specs/architecture.md §4.3 (author-authored vs toolkit-generated — governing section)
|
|
22
|
+
* @see docs/specs/architecture.md §10.5 (freshness check — consumer of the detect/strip helpers)
|
|
23
|
+
* @see docs/specs/architecture.md §5.2 (build phase — consumer of the apply helpers)
|
|
24
|
+
*/
|
|
25
|
+
import type { ClaudeHooksFile } from '../targets/claude/schemas.js';
|
|
26
|
+
/**
|
|
27
|
+
* Canonical generator identity string embedded in every sentinel. Centralized here so the
|
|
28
|
+
* inline comment block, the JSON `_generated.by` field, and any detector all agree on one
|
|
29
|
+
* literal — there is no second copy to drift.
|
|
30
|
+
*/
|
|
31
|
+
export declare const GENERATOR_ID = "@ai-plugin-marketplace/cli";
|
|
32
|
+
/**
|
|
33
|
+
* Which sentinel carrier a given generated file uses. Selected by the build step per the
|
|
34
|
+
* artifact's format and host tolerance (§4.3):
|
|
35
|
+
* - `'inline'` — plain-text outputs (comment block prepended).
|
|
36
|
+
* - `'json-field'` — JSON outputs that tolerate an extra top-level `_generated` key.
|
|
37
|
+
* - `'sidecar'` — strict-schema JSON whose host rejects unknown fields; sentinel lives in
|
|
38
|
+
* a companion `<artifact>.generated` file and the artifact itself is left untouched.
|
|
39
|
+
*/
|
|
40
|
+
export type SentinelMode = 'inline' | 'json-field' | 'sidecar';
|
|
41
|
+
/**
|
|
42
|
+
* Prepend the inline-text sentinel (§4.3) to a plain-text generated file's content.
|
|
43
|
+
*
|
|
44
|
+
* @param content - The generated file body, sans sentinel.
|
|
45
|
+
* @param source - Author-authored source path, relative to the plugin directory.
|
|
46
|
+
* @returns Content with the three-line sentinel block prepended.
|
|
47
|
+
*/
|
|
48
|
+
export declare function applyInlineSentinel(content: string, source: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The body of a `<artifact>.generated` sidecar file (§4.3). Identical to the inline-text
|
|
51
|
+
* sentinel block — the sidecar carries no artifact content, only the sentinel.
|
|
52
|
+
*
|
|
53
|
+
* @param source - Author-authored source path, relative to the plugin directory.
|
|
54
|
+
* @returns The three-line sentinel block, newline-terminated.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sidecarContent(source: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Derive the sidecar sentinel path for an artifact: `<artifactPath>.generated` (§4.3). The
|
|
59
|
+
* sidecar sits next to the artifact; the artifact itself is left untouched.
|
|
60
|
+
*
|
|
61
|
+
* @param artifactPath - Path to the strict-schema artifact (e.g. `gemini-extension.json`).
|
|
62
|
+
* @returns The sidecar path.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sidecarPath(artifactPath: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Serialize an object as canonical JSON with a top-level `_generated` sentinel (§4.3). The
|
|
67
|
+
* `_generated` key is emitted FIRST, every other own-enumerable key follows in its original
|
|
68
|
+
* order, and the output uses 2-space indentation with a trailing newline — matching the JSON
|
|
69
|
+
* conventions in `claude/transform.ts`'s `serializeClaudeHooksJson`.
|
|
70
|
+
*
|
|
71
|
+
* Any pre-existing `_generated` key on the input is overwritten by the fresh sentinel rather
|
|
72
|
+
* than duplicated, so re-applying is idempotent at the field level.
|
|
73
|
+
*
|
|
74
|
+
* @param obj - The value to serialize. Treated as a string-keyed object; non-object inputs
|
|
75
|
+
* (arrays, primitives) cannot carry a top-level field and are rejected.
|
|
76
|
+
* @param source - Author-authored source path, relative to the plugin directory.
|
|
77
|
+
* @returns Canonical JSON string with `_generated` first and a trailing `\n`.
|
|
78
|
+
* @throws {TypeError} If `obj` is not a plain JSON object (null, array, or primitive).
|
|
79
|
+
*/
|
|
80
|
+
export declare function applyJsonSentinel(obj: unknown, source: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Convenience overload for validated Claude hooks data, matching the call shape of
|
|
83
|
+
* `serializeClaudeHooksJson`. Equivalent to {@link applyJsonSentinel} with the hooks object.
|
|
84
|
+
*
|
|
85
|
+
* @param data - Validated {@link ClaudeHooksFile}.
|
|
86
|
+
* @param source - Author-authored source path, relative to the plugin directory.
|
|
87
|
+
* @returns Canonical JSON string with `_generated` first and a trailing `\n`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function applyJsonSentinelToHooks(data: ClaudeHooksFile, source: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Detect whether `content` carries a sentinel for the given carrier (§4.3). For `'sidecar'`,
|
|
92
|
+
* `content` is the sidecar file body (same shape as inline); the artifact itself is never
|
|
93
|
+
* inspected by this function.
|
|
94
|
+
*
|
|
95
|
+
* @param content - File content to inspect (artifact body, or sidecar body for `'sidecar'`).
|
|
96
|
+
* @param mode - Which carrier to check for.
|
|
97
|
+
* @returns `true` iff a well-formed sentinel of that carrier is present.
|
|
98
|
+
*/
|
|
99
|
+
export declare function hasSentinel(content: string, mode: SentinelMode): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Extract the author-authored source path recorded in a sentinel, or `undefined` if no
|
|
102
|
+
* well-formed sentinel of the given carrier is present (§10.5 uses this to identify the source
|
|
103
|
+
* and to distinguish generated files from hand-authored ones).
|
|
104
|
+
*
|
|
105
|
+
* @param content - File content to inspect (artifact body, or sidecar body for `'sidecar'`).
|
|
106
|
+
* @param mode - Which carrier to read.
|
|
107
|
+
* @returns The recorded source path, or `undefined`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readSentinelSource(content: string, mode: SentinelMode): string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Remove a sentinel from `content`, returning the body sans sentinel. The inverse of the
|
|
112
|
+
* matching `applyXSentinel`, so freshness (§10.5) can compare an on-disk generated file to
|
|
113
|
+
* freshly-built content modulo the sentinel.
|
|
114
|
+
*
|
|
115
|
+
* - `'inline'` — drops the three-line comment block if present; otherwise returns `content`
|
|
116
|
+
* unchanged.
|
|
117
|
+
* - `'sidecar'` — the sidecar carries only the sentinel, so the stripped body is the empty
|
|
118
|
+
* string; if `content` is not a recognized sidecar it is returned unchanged.
|
|
119
|
+
* - `'json-field'` — re-serializes the object without `_generated`, preserving the remaining
|
|
120
|
+
* keys' order, 2-space indent, and trailing newline. Malformed JSON is returned unchanged.
|
|
121
|
+
*
|
|
122
|
+
* @param content - File content to strip.
|
|
123
|
+
* @param mode - Which carrier to strip.
|
|
124
|
+
* @returns The content with its sentinel removed.
|
|
125
|
+
*/
|
|
126
|
+
export declare function stripSentinel(content: string, mode: SentinelMode): string;
|
|
127
|
+
//# sourceMappingURL=sentinel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../src/pipeline/sentinel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,YAAY,+BAA+B,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;AAqC/D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AASD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAExD;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBtE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtF;AAmDD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAExE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAQ1F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAWzE"}
|