harness-alchemist 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/.claude-plugin/plugin.json +14 -0
- package/.codex-plugin/plugin.json +13 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/harness-alchemist.mjs +43 -0
- package/cordis.patch.yml +3 -0
- package/dist/deepseek.d.ts +4 -0
- package/dist/deepseek.d.ts.map +1 -0
- package/dist/deepseek.js +5 -0
- package/dist/deepseek.js.map +1 -0
- package/dist/opencode.d.ts +3 -0
- package/dist/opencode.d.ts.map +1 -0
- package/dist/opencode.js +5 -0
- package/dist/opencode.js.map +1 -0
- package/lib/create.mjs +319 -0
- package/lib/validate.mjs +433 -0
- package/package.json +86 -0
- package/plugin.json +5 -0
- package/skills/harness-alchemist/SKILL.md +58 -0
- package/skills/harness-alchemist/references/antigravity.md +37 -0
- package/skills/harness-alchemist/references/claude-code.md +51 -0
- package/skills/harness-alchemist/references/codex.md +37 -0
- package/skills/harness-alchemist/references/compatibility.md +53 -0
- package/skills/harness-alchemist/references/deepseek-harness.md +56 -0
- package/skills/harness-alchemist/references/opencode.md +51 -0
- package/skills/harness-alchemist/references/publishing.md +66 -0
- package/templates/README.md +9 -0
- package/templates/v0.1.0/licenses/Apache-2.0.txt +201 -0
- package/templates/v0.1.0/template.json +13 -0
- package/templates/v0.1.0/universal-typescript/.agents/plugins/marketplace.json.tpl +20 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/SKILL.md.tpl +33 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/references/compatibility.md.tpl +23 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/check-package.mjs.tpl +69 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/sync-metadata.mjs.tpl +124 -0
- package/templates/v0.1.0/universal-typescript/.claude-plugin/marketplace.json.tpl +19 -0
- package/templates/v0.1.0/universal-typescript/.claude-plugin/plugin.json.tpl +14 -0
- package/templates/v0.1.0/universal-typescript/.codex-plugin/plugin.json.tpl +13 -0
- package/templates/v0.1.0/universal-typescript/.github/workflows/npm-publish.yml.tpl +43 -0
- package/templates/v0.1.0/universal-typescript/.gitignore.tpl +9 -0
- package/templates/v0.1.0/universal-typescript/AGENTS.md.tpl +16 -0
- package/templates/v0.1.0/universal-typescript/README.md.tpl +131 -0
- package/templates/v0.1.0/universal-typescript/cordis.patch.yml.tpl +3 -0
- package/templates/v0.1.0/universal-typescript/package.json.tpl +70 -0
- package/templates/v0.1.0/universal-typescript/plugin.json.tpl +5 -0
- package/templates/v0.1.0/universal-typescript/skills/shared-skill/SKILL.md.tpl +18 -0
- package/templates/v0.1.0/universal-typescript/src/deepseek.ts.tpl +7 -0
- package/templates/v0.1.0/universal-typescript/src/opencode.ts.tpl +7 -0
- package/templates/v0.1.0/universal-typescript/tests/runtimes.test.mjs.tpl +15 -0
- package/templates/v0.1.0/universal-typescript/tsconfig.json.tpl +17 -0
package/lib/validate.mjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { readdir, readFile, realpath } from "node:fs/promises"
|
|
5
|
+
import { basename, dirname, join, resolve, sep } from "node:path"
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
import { spawnSync } from "node:child_process"
|
|
8
|
+
|
|
9
|
+
const REQUIRED_FILES = [
|
|
10
|
+
".agents/plugins/marketplace.json",
|
|
11
|
+
".claude-plugin/marketplace.json",
|
|
12
|
+
".claude-plugin/plugin.json",
|
|
13
|
+
".codex-plugin/plugin.json",
|
|
14
|
+
".github/workflows/npm-publish.yml",
|
|
15
|
+
".gitignore",
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"package.json",
|
|
21
|
+
"plugin.json",
|
|
22
|
+
"src/deepseek.ts",
|
|
23
|
+
"src/opencode.ts",
|
|
24
|
+
"tsconfig.json",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
function usage() {
|
|
28
|
+
return `Usage: harness-alchemist validate [project-directory] [--external] [--json]
|
|
29
|
+
|
|
30
|
+
Validates a universal Claude, Codex, OpenCode, Antigravity, and DeepSeek
|
|
31
|
+
plugin scaffold. By default the project is resolved from the current working
|
|
32
|
+
directory or from the script's containing generated project.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--external Run installed platform validators, currently Claude Code.
|
|
36
|
+
--json Print a machine-readable result.
|
|
37
|
+
--help Show this help.`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findProjectRoot(start) {
|
|
41
|
+
let current = resolve(start)
|
|
42
|
+
while (true) {
|
|
43
|
+
if (
|
|
44
|
+
existsSync(join(current, "package.json")) &&
|
|
45
|
+
existsSync(join(current, ".claude-plugin")) &&
|
|
46
|
+
existsSync(join(current, ".codex-plugin"))
|
|
47
|
+
) {
|
|
48
|
+
return current
|
|
49
|
+
}
|
|
50
|
+
const parent = dirname(current)
|
|
51
|
+
if (parent === current) return undefined
|
|
52
|
+
current = parent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
let project
|
|
58
|
+
let external = false
|
|
59
|
+
let json = false
|
|
60
|
+
|
|
61
|
+
for (const arg of argv) {
|
|
62
|
+
if (arg === "--help" || arg === "-h") return { help: true }
|
|
63
|
+
if (arg === "--external") external = true
|
|
64
|
+
else if (arg === "--json") json = true
|
|
65
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`)
|
|
66
|
+
else if (project) throw new Error("Only one project directory may be supplied")
|
|
67
|
+
else project = arg
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { project, external, json, help: false }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readJson(path, errors) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(await readFile(path, "utf8"))
|
|
76
|
+
} catch (error) {
|
|
77
|
+
errors.push(`${path}: invalid JSON (${error.message})`)
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unquote(value) {
|
|
83
|
+
const trimmed = value.trim()
|
|
84
|
+
if (
|
|
85
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
86
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
87
|
+
) {
|
|
88
|
+
return trimmed.slice(1, -1)
|
|
89
|
+
}
|
|
90
|
+
return trimmed
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseFrontmatter(content) {
|
|
94
|
+
if (!content.startsWith("---\n")) return undefined
|
|
95
|
+
const end = content.indexOf("\n---", 4)
|
|
96
|
+
if (end < 0) return undefined
|
|
97
|
+
const block = content.slice(4, end)
|
|
98
|
+
const values = {}
|
|
99
|
+
for (const line of block.split("\n")) {
|
|
100
|
+
const match = line.match(/^([a-zA-Z0-9-]+):\s*(.*)$/)
|
|
101
|
+
if (match) values[match[1]] = unquote(match[2])
|
|
102
|
+
}
|
|
103
|
+
return values
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseYamlScalar(value) {
|
|
107
|
+
const scalar = value.trim()
|
|
108
|
+
if (scalar.startsWith("'") && scalar.endsWith("'")) {
|
|
109
|
+
return scalar.slice(1, -1).replaceAll("''", "'")
|
|
110
|
+
}
|
|
111
|
+
if (scalar.startsWith('"') && scalar.endsWith('"')) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(scalar)
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return /^[A-Za-z0-9@/_.-]+$/.test(scalar) ? scalar : undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseCordisInsertEntries(content) {
|
|
122
|
+
if (content.includes("\t")) return []
|
|
123
|
+
const lines = content.split("\n")
|
|
124
|
+
const entries = []
|
|
125
|
+
|
|
126
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
127
|
+
const insertMatch = lines[index].match(/^(\s*)-\s+insert:\s*(?:#.*)?$/)
|
|
128
|
+
if (!insertMatch || insertMatch[1].length !== 0) continue
|
|
129
|
+
|
|
130
|
+
let entryIndent
|
|
131
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
132
|
+
const line = lines[next]
|
|
133
|
+
if (line.trim() && line.search(/\S/) === 0) break
|
|
134
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue
|
|
135
|
+
const idMatch = line.match(/^(\s*)-\s+id:\s*(.+?)\s*$/)
|
|
136
|
+
if (entryIndent === undefined) {
|
|
137
|
+
if (!idMatch || idMatch[1].length === 0) break
|
|
138
|
+
entryIndent = idMatch[1].length
|
|
139
|
+
}
|
|
140
|
+
if (!idMatch || idMatch[1].length !== entryIndent) continue
|
|
141
|
+
const id = parseYamlScalar(idMatch[2])
|
|
142
|
+
if (!id) continue
|
|
143
|
+
|
|
144
|
+
const idIndent = idMatch[1].length
|
|
145
|
+
let name
|
|
146
|
+
let nameFields = 0
|
|
147
|
+
for (let field = next + 1; field < lines.length; field += 1) {
|
|
148
|
+
const fieldLine = lines[field]
|
|
149
|
+
const fieldIndent = fieldLine.search(/\S/)
|
|
150
|
+
if (fieldLine.trim() && fieldIndent <= idIndent) break
|
|
151
|
+
const nameMatch = fieldLine.match(/^(\s*)name:\s*(.+?)\s*$/)
|
|
152
|
+
if (nameMatch && nameMatch[1].length === idIndent + 2) {
|
|
153
|
+
nameFields += 1
|
|
154
|
+
name = parseYamlScalar(nameMatch[2])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
entries.push({ id, name, valid: name !== undefined && nameFields === 1 })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return entries
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function collectSkillFiles(root, relativeRoot) {
|
|
165
|
+
const start = join(root, relativeRoot)
|
|
166
|
+
if (!existsSync(start)) return []
|
|
167
|
+
const results = []
|
|
168
|
+
|
|
169
|
+
async function walk(directory, depth) {
|
|
170
|
+
if (depth > 5) return
|
|
171
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
172
|
+
const path = join(directory, entry.name)
|
|
173
|
+
if (entry.isDirectory()) await walk(path, depth + 1)
|
|
174
|
+
else if (entry.isFile() && entry.name === "SKILL.md") results.push(path)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await walk(start, 0)
|
|
179
|
+
return results
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function scanForTokens(root) {
|
|
183
|
+
const matches = []
|
|
184
|
+
const ignored = new Set([".git", "dist", "node_modules", "coverage", "templates"])
|
|
185
|
+
|
|
186
|
+
async function walk(directory, depth) {
|
|
187
|
+
if (depth > 8) return
|
|
188
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
189
|
+
if (ignored.has(entry.name)) continue
|
|
190
|
+
const path = join(directory, entry.name)
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
await walk(path, depth + 1)
|
|
193
|
+
} else if (entry.isFile()) {
|
|
194
|
+
const content = await readFile(path, "utf8").catch(() => "")
|
|
195
|
+
if (/\{\{[A-Z0-9_]+\}\}/.test(content)) matches.push(path)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await walk(root, 0)
|
|
201
|
+
return matches
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function requirePath(root, relativePath, errors) {
|
|
205
|
+
if (!existsSync(join(root, relativePath))) errors.push(`Missing required file: ${relativePath}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function requireManifestPath(root, value, label, errors) {
|
|
209
|
+
if (typeof value !== "string" || !value.startsWith("./")) {
|
|
210
|
+
errors.push(`${label} must be a ./-relative path`)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
const target = resolve(root, value)
|
|
214
|
+
if (!(target === root || target.startsWith(`${root}${sep}`))) {
|
|
215
|
+
errors.push(`${label} escapes the plugin root`)
|
|
216
|
+
} else if (!existsSync(target)) {
|
|
217
|
+
errors.push(`${label} points to missing path ${value}`)
|
|
218
|
+
} else {
|
|
219
|
+
const [canonicalRoot, canonicalTarget] = await Promise.all([realpath(root), realpath(target)])
|
|
220
|
+
if (!(canonicalTarget === canonicalRoot || canonicalTarget.startsWith(`${canonicalRoot}${sep}`))) {
|
|
221
|
+
errors.push(`${label} resolves through a symlink outside the plugin root`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function exportTarget(exports, key, field = "import") {
|
|
227
|
+
const value = exports?.[key]
|
|
228
|
+
if (typeof value === "string") return value
|
|
229
|
+
if (value && typeof value === "object") return value[field]
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function validateProject(projectRoot, options = {}) {
|
|
234
|
+
const root = resolve(projectRoot)
|
|
235
|
+
const errors = []
|
|
236
|
+
const warnings = []
|
|
237
|
+
|
|
238
|
+
for (const file of REQUIRED_FILES) requirePath(root, file, errors)
|
|
239
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
240
|
+
|
|
241
|
+
const packageJson = await readJson(join(root, "package.json"), errors)
|
|
242
|
+
const claudePlugin = await readJson(join(root, ".claude-plugin/plugin.json"), errors)
|
|
243
|
+
const claudeMarketplace = await readJson(
|
|
244
|
+
join(root, ".claude-plugin/marketplace.json"),
|
|
245
|
+
errors,
|
|
246
|
+
)
|
|
247
|
+
const codexPlugin = await readJson(join(root, ".codex-plugin/plugin.json"), errors)
|
|
248
|
+
const codexMarketplace = await readJson(
|
|
249
|
+
join(root, ".agents/plugins/marketplace.json"),
|
|
250
|
+
errors,
|
|
251
|
+
)
|
|
252
|
+
const antigravityPlugin = await readJson(join(root, "plugin.json"), errors)
|
|
253
|
+
|
|
254
|
+
if (errors.length > 0) return { root, errors, warnings }
|
|
255
|
+
|
|
256
|
+
const packageBase = packageJson.name?.split("/").at(-1)
|
|
257
|
+
const pluginName = claudePlugin.name
|
|
258
|
+
const validName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
259
|
+
|
|
260
|
+
if (!validName.test(pluginName ?? "")) {
|
|
261
|
+
errors.push("Plugin name must contain lowercase letters, digits, and single hyphens")
|
|
262
|
+
}
|
|
263
|
+
if ((pluginName?.length ?? 0) > 56) {
|
|
264
|
+
errors.push("Plugin name must be at most 56 characters")
|
|
265
|
+
}
|
|
266
|
+
if (packageBase !== pluginName) {
|
|
267
|
+
errors.push(`npm package basename '${packageBase}' must match plugin name '${pluginName}'`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const [label, value] of [
|
|
271
|
+
["Codex plugin", codexPlugin.name],
|
|
272
|
+
["Antigravity plugin", antigravityPlugin.name],
|
|
273
|
+
]) {
|
|
274
|
+
if (value !== pluginName) errors.push(`${label} name '${value}' does not match '${pluginName}'`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const [label, manifest] of [
|
|
278
|
+
["Claude plugin", claudePlugin],
|
|
279
|
+
["Codex plugin", codexPlugin],
|
|
280
|
+
]) {
|
|
281
|
+
if (manifest.version !== packageJson.version) {
|
|
282
|
+
errors.push(`${label} version does not match package.json`)
|
|
283
|
+
}
|
|
284
|
+
if (manifest.description !== packageJson.description) {
|
|
285
|
+
errors.push(`${label} description does not match package.json`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (antigravityPlugin.description !== packageJson.description) {
|
|
289
|
+
errors.push("Antigravity description does not match package.json")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (packageJson.type !== "module") errors.push("package.json type must be 'module'")
|
|
293
|
+
if (packageJson.engines?.node !== ">=22.20.0") {
|
|
294
|
+
errors.push("package.json engines.node must be '>=22.20.0'")
|
|
295
|
+
}
|
|
296
|
+
const requiredExports = [
|
|
297
|
+
[".", "import", "./dist/opencode.js"],
|
|
298
|
+
[".", "types", "./dist/opencode.d.ts"],
|
|
299
|
+
["./deepseek", "import", "./dist/deepseek.js"],
|
|
300
|
+
["./deepseek", "types", "./dist/deepseek.d.ts"],
|
|
301
|
+
["./cordis.patch.yml", "import", "./cordis.patch.yml"],
|
|
302
|
+
]
|
|
303
|
+
for (const [key, field, expected] of requiredExports) {
|
|
304
|
+
const target = exportTarget(packageJson.exports, key, field)
|
|
305
|
+
if (target !== expected) {
|
|
306
|
+
errors.push(`package.json export '${key}' ${field} target must be '${expected}'`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const entry of [
|
|
310
|
+
"dist",
|
|
311
|
+
"skills",
|
|
312
|
+
"cordis.patch.yml",
|
|
313
|
+
".claude-plugin/plugin.json",
|
|
314
|
+
".codex-plugin/plugin.json",
|
|
315
|
+
"plugin.json",
|
|
316
|
+
]) {
|
|
317
|
+
if (!packageJson.files?.includes(entry)) errors.push(`package.json files is missing '${entry}'`)
|
|
318
|
+
}
|
|
319
|
+
if (packageJson.dsh?.bundle?.patch !== "./cordis.patch.yml") {
|
|
320
|
+
errors.push("package.json dsh.bundle.patch must be './cordis.patch.yml'")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await requireManifestPath(root, claudePlugin.skills, "Claude skills", errors)
|
|
324
|
+
await requireManifestPath(root, codexPlugin.skills, "Codex skills", errors)
|
|
325
|
+
|
|
326
|
+
const claudeEntry = claudeMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
327
|
+
if (!claudeEntry) errors.push("Claude marketplace is missing the plugin entry")
|
|
328
|
+
else if (claudeEntry.source !== "./") errors.push("Claude marketplace source must be './'")
|
|
329
|
+
|
|
330
|
+
const codexEntry = codexMarketplace.plugins?.find((entry) => entry.name === pluginName)
|
|
331
|
+
if (!codexEntry) errors.push("Codex marketplace is missing the plugin entry")
|
|
332
|
+
else {
|
|
333
|
+
if (codexEntry.source?.source !== "local" || codexEntry.source?.path !== "./") {
|
|
334
|
+
errors.push("Codex marketplace source must be a local './' path")
|
|
335
|
+
}
|
|
336
|
+
if (!codexEntry.policy?.installation || !codexEntry.policy?.authentication) {
|
|
337
|
+
errors.push("Codex marketplace entry requires installation and authentication policies")
|
|
338
|
+
}
|
|
339
|
+
if (!codexEntry.category) errors.push("Codex marketplace entry requires a category")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const patch = await readFile(join(root, "cordis.patch.yml"), "utf8")
|
|
343
|
+
const expectedModule = `${packageJson.name}/deepseek`
|
|
344
|
+
const cordisEntries = parseCordisInsertEntries(patch)
|
|
345
|
+
.filter((entry) => entry.id === pluginName)
|
|
346
|
+
const cordisEntry = cordisEntries[0]
|
|
347
|
+
if (cordisEntries.length !== 1 || !cordisEntry?.valid) {
|
|
348
|
+
errors.push(`cordis.patch.yml must contain a valid insert entry for '${pluginName}'`)
|
|
349
|
+
} else if (cordisEntry.name !== expectedModule) {
|
|
350
|
+
errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const skillFiles = [
|
|
354
|
+
...(await collectSkillFiles(root, "skills")),
|
|
355
|
+
...(await collectSkillFiles(root, ".agents/skills")),
|
|
356
|
+
]
|
|
357
|
+
if (skillFiles.length < 2) errors.push("Expected a shared skill and a project development skill")
|
|
358
|
+
|
|
359
|
+
for (const skillFile of skillFiles) {
|
|
360
|
+
const frontmatter = parseFrontmatter(await readFile(skillFile, "utf8"))
|
|
361
|
+
const directoryName = basename(dirname(skillFile))
|
|
362
|
+
if (!frontmatter) errors.push(`${skillFile}: missing YAML frontmatter`)
|
|
363
|
+
else {
|
|
364
|
+
if (frontmatter.name !== directoryName) {
|
|
365
|
+
errors.push(`${skillFile}: frontmatter name must match directory '${directoryName}'`)
|
|
366
|
+
}
|
|
367
|
+
if (!frontmatter.description) errors.push(`${skillFile}: description is required`)
|
|
368
|
+
if (!validName.test(frontmatter.name ?? "") || (frontmatter.name?.length ?? 0) > 64) {
|
|
369
|
+
errors.push(`${skillFile}: invalid Agent Skill name`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const path of await scanForTokens(root)) {
|
|
375
|
+
errors.push(`${path}: unresolved scaffold token`)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (options.external) {
|
|
379
|
+
const result = spawnSync("claude", ["plugin", "validate", root, "--strict"], {
|
|
380
|
+
encoding: "utf8",
|
|
381
|
+
})
|
|
382
|
+
if (result.error?.code === "ENOENT") warnings.push("Claude CLI not found; skipped external validation")
|
|
383
|
+
else if (result.status !== 0) {
|
|
384
|
+
errors.push(`Claude plugin validation failed: ${(result.stderr || result.stdout).trim()}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { root, errors, warnings }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function runValidate(argv) {
|
|
392
|
+
let args
|
|
393
|
+
try {
|
|
394
|
+
args = parseArgs(argv)
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error(error.message)
|
|
397
|
+
console.error(usage())
|
|
398
|
+
return 2
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (args.help) {
|
|
402
|
+
console.log(usage())
|
|
403
|
+
return 0
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
407
|
+
const root = args.project
|
|
408
|
+
? resolve(args.project)
|
|
409
|
+
: findProjectRoot(process.cwd()) ?? findProjectRoot(scriptDirectory)
|
|
410
|
+
|
|
411
|
+
if (!root) {
|
|
412
|
+
console.error("Could not find a universal plugin project. Pass its directory explicitly.")
|
|
413
|
+
return 2
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const result = await validateProject(root, { external: args.external })
|
|
417
|
+
if (args.json) {
|
|
418
|
+
console.log(JSON.stringify({ valid: result.errors.length === 0, ...result }, null, 2))
|
|
419
|
+
} else if (result.errors.length === 0) {
|
|
420
|
+
console.log(`Validated universal plugin scaffold at ${result.root}`)
|
|
421
|
+
for (const warning of result.warnings) console.warn(`Warning: ${warning}`)
|
|
422
|
+
} else {
|
|
423
|
+
console.error(`Validation failed for ${result.root}:`)
|
|
424
|
+
for (const error of result.errors) console.error(`- ${error}`)
|
|
425
|
+
for (const warning of result.warnings) console.warn(`Warning: ${warning}`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return result.errors.length > 0 ? 1 : 0
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
|
|
432
|
+
process.exitCode = await runValidate(process.argv.slice(2))
|
|
433
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "harness-alchemist",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"harness-alchemist": "./bin/harness-alchemist.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/opencode.js",
|
|
10
|
+
"types": "./dist/opencode.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/opencode.d.ts",
|
|
14
|
+
"import": "./dist/opencode.js"
|
|
15
|
+
},
|
|
16
|
+
"./deepseek": {
|
|
17
|
+
"types": "./dist/deepseek.d.ts",
|
|
18
|
+
"import": "./dist/deepseek.js"
|
|
19
|
+
},
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"bin",
|
|
25
|
+
"lib",
|
|
26
|
+
"templates",
|
|
27
|
+
"skills",
|
|
28
|
+
"cordis.patch.yml",
|
|
29
|
+
".claude-plugin/plugin.json",
|
|
30
|
+
".codex-plugin/plugin.json",
|
|
31
|
+
"plugin.json",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
39
|
+
"sync": "node .agents/skills/develop-harness-alchemist/scripts/sync-metadata.mjs",
|
|
40
|
+
"validate": "node .agents/skills/develop-harness-alchemist/scripts/validate.mjs",
|
|
41
|
+
"pack:check": "node .agents/skills/develop-harness-alchemist/scripts/check-package.mjs",
|
|
42
|
+
"verify": "npm run check && npm test && npm run validate && npm run pack:check",
|
|
43
|
+
"prepack": "npm run verify"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.20.0",
|
|
47
|
+
"bun": ">=1.2.0"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"agent-skill",
|
|
51
|
+
"antigravity",
|
|
52
|
+
"claude-code",
|
|
53
|
+
"codex",
|
|
54
|
+
"deepseek",
|
|
55
|
+
"opencode",
|
|
56
|
+
"scaffolding"
|
|
57
|
+
],
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
+
"@opencode-ai/plugin": "^1.18.21"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"@deepseek-ai/cordis": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@opencode-ai/plugin": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
72
|
+
"@opencode-ai/plugin": "^1.18.21",
|
|
73
|
+
"typescript": "^5.9.3"
|
|
74
|
+
},
|
|
75
|
+
"author": "Haochuan Zhang",
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"repository": {
|
|
78
|
+
"type": "git",
|
|
79
|
+
"url": "https://github.com/lunarmoon26/harness-alchemist"
|
|
80
|
+
},
|
|
81
|
+
"dsh": {
|
|
82
|
+
"bundle": {
|
|
83
|
+
"patch": "./cordis.patch.yml"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-alchemist
|
|
3
|
+
description: "Use the Harness Alchemist CLI to create or validate universal coding-agent plugins for Claude Code, Codex/ChatGPT, OpenCode, Google Antigravity, and DeepSeek Harness/Cordis."
|
|
4
|
+
compatibility: Requires Node.js 22.20+ or Bun 1.2+ for the CLI. Platform CLIs are optional and only needed for live installation checks.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Harness Alchemist
|
|
8
|
+
|
|
9
|
+
Use the CLI to create or validate a universal plugin repository. This is the
|
|
10
|
+
tier-2 end-user workflow: it does not maintain Harness Alchemist itself or
|
|
11
|
+
replace the generated repository's local maintenance skill.
|
|
12
|
+
|
|
13
|
+
## Start Here
|
|
14
|
+
|
|
15
|
+
1. Inspect the target directory before changing it.
|
|
16
|
+
2. Read [references/compatibility.md](references/compatibility.md) before choosing paths or manifests.
|
|
17
|
+
3. For a new project, run `harness-alchemist create --help` and use the CLI instead of recreating the structure manually.
|
|
18
|
+
4. After creation, load `.agents/skills/develop-<name>/` in the generated project before maintaining its files.
|
|
19
|
+
5. Run `harness-alchemist validate` and the generated package's `verify` script before reporting completion.
|
|
20
|
+
|
|
21
|
+
## Create A Project
|
|
22
|
+
|
|
23
|
+
Gather a lowercase kebab-case plugin name, description, npm package name, author, repository, and license. Then run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
harness-alchemist create /absolute/path/to/project \
|
|
27
|
+
--name my-plugin \
|
|
28
|
+
--description "What the plugin does" \
|
|
29
|
+
--package @scope/my-plugin \
|
|
30
|
+
--author "Example Team" \
|
|
31
|
+
--repository example/my-plugin
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `bunx harness-alchemist create ...` or `npx harness-alchemist create ...` when the CLI is not installed. The CLI only writes to a missing or empty destination.
|
|
35
|
+
|
|
36
|
+
## After Generation
|
|
37
|
+
|
|
38
|
+
The generated `skills/<name>/SKILL.md` is the plugin's end-user workflow
|
|
39
|
+
starter. The generated `.agents/skills/develop-<name>/` is its maintenance
|
|
40
|
+
guide; use that local skill for file ownership, metadata, and harness changes.
|
|
41
|
+
|
|
42
|
+
List supported template versions with:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
harness-alchemist templates
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Verify
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
harness-alchemist validate /path/to/project
|
|
52
|
+
cd /path/to/project
|
|
53
|
+
npm install
|
|
54
|
+
npm run verify
|
|
55
|
+
npm pack --dry-run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Static validation is structural evidence, not proof that remote services or host-specific lifecycle events work.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Google Antigravity Plugins
|
|
2
|
+
|
|
3
|
+
## Plugin Shape
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
plugin.json
|
|
7
|
+
skills/<name>/SKILL.md
|
|
8
|
+
rules/<name>.md
|
|
9
|
+
hooks.json
|
|
10
|
+
mcp_config.json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Only `plugin.json` is required. The CLI manifest schema accepts a machine-readable name and optional description. Keep the root manifest separate from Claude and Codex manifests.
|
|
14
|
+
|
|
15
|
+
Current Antigravity Agent Skills use the open Agent Skills directory format with nested `<name>/SKILL.md`. This is also the documented plugin skill shape.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Antigravity 2 scans workspace plugins under `.agents/plugins/` or `_agents/plugins/`, and global plugins under `~/.gemini/config/plugins/`.
|
|
20
|
+
|
|
21
|
+
Antigravity CLI stages installed plugins under `~/.gemini/antigravity-cli/plugins/<name>/` and supports:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agy plugin install /path/to/plugin
|
|
25
|
+
agy plugin list
|
|
26
|
+
agy plugin disable plugin-name
|
|
27
|
+
agy plugin enable plugin-name
|
|
28
|
+
agy plugin uninstall plugin-name
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The CLI documentation also shows a legacy flat Markdown workspace-skill example. Prefer the nested Agent Skills shape because the current Antigravity Skills and plugin documentation both specify it.
|
|
32
|
+
|
|
33
|
+
Official references:
|
|
34
|
+
|
|
35
|
+
- https://antigravity.google/docs/plugins/
|
|
36
|
+
- https://antigravity.google/docs/skills/
|
|
37
|
+
- https://antigravity.google/docs/cli/plugins/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Claude Code Plugins
|
|
2
|
+
|
|
3
|
+
## Required Shape
|
|
4
|
+
|
|
5
|
+
For a manifest-backed plugin:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
.claude-plugin/plugin.json
|
|
9
|
+
skills/<name>/SKILL.md
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For repository installation, add `.claude-plugin/marketplace.json`. A one-plugin repository may use `"source": "./"`.
|
|
13
|
+
|
|
14
|
+
Only `plugin.json` belongs inside `.claude-plugin/`. Components remain at the plugin root.
|
|
15
|
+
|
|
16
|
+
## Supported Components
|
|
17
|
+
|
|
18
|
+
- `skills/<name>/SKILL.md`; prefer this over legacy `commands/*.md`.
|
|
19
|
+
- `agents/*.md`.
|
|
20
|
+
- `hooks/hooks.json`.
|
|
21
|
+
- `.mcp.json`.
|
|
22
|
+
- `.lsp.json`.
|
|
23
|
+
- `monitors/monitors.json`.
|
|
24
|
+
- `bin/` for executables added to the Bash tool path.
|
|
25
|
+
- Root `settings.json` for supported plugin defaults.
|
|
26
|
+
|
|
27
|
+
Use `${CLAUDE_PLUGIN_ROOT}` for files within the copied plugin. Do not reference `../` paths outside the repository payload.
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude --plugin-dir /absolute/path/to/plugin
|
|
33
|
+
claude plugin validate /absolute/path/to/plugin --strict
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `/reload-plugins` after component changes when supported.
|
|
37
|
+
|
|
38
|
+
## Distribution
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude plugin marketplace add owner/repo
|
|
42
|
+
claude plugin install plugin-name@marketplace-name
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Marketplace installation copies plugins into a cache. Versioned manifests require a version bump for updates.
|
|
46
|
+
|
|
47
|
+
Official references:
|
|
48
|
+
|
|
49
|
+
- https://code.claude.com/docs/en/plugins
|
|
50
|
+
- https://code.claude.com/docs/en/plugins-reference
|
|
51
|
+
- https://code.claude.com/docs/en/plugin-marketplaces
|