harness-alchemist 0.1.9 → 0.2.0-alpha.1
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +39 -3
- package/alchemy.json +1 -1
- package/alchemy.schema.json +1 -1
- package/bin/harness-alchemist.mjs +62 -36
- package/mcp.json +1 -1
- package/package.json +36 -42
- package/plugin.json +1 -1
- package/skills/harness-alchemist/references/publishing.md +11 -7
- package/templates/README.md +1 -1
- package/{lib → templates/v0.1.0/generated}/validate.mjs +7 -7
- package/templates/v0.1.0/universal-typescript/.github/workflows/npm-publish.yml.tpl +45 -20
- package/templates/v0.1.0/universal-typescript/README.md.tpl +8 -6
- package/templates/v0.1.0/universal-typescript/mcp.json.tpl +1 -1
- package/templates/v0.1.0/universal-typescript/package.json.tpl +1 -1
- package/lib/create.mjs +0 -331
- package/lib/install-check.mjs +0 -363
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "harness-alchemist",
|
|
4
4
|
"displayName": "Harness Alchemist",
|
|
5
|
-
"version": "0.1
|
|
5
|
+
"version": "0.2.0-alpha.1",
|
|
6
6
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Haochuan Zhang"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-alchemist",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
4
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Haochuan Zhang",
|
package/README.md
CHANGED
|
@@ -38,7 +38,11 @@ npm install -g harness-alchemist
|
|
|
38
38
|
npx harness-alchemist@latest create my-plugin --help
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The CLI implementation is a native Rust binary. npm and Bun installations use a
|
|
42
|
+
small launcher to select a prebuilt macOS, Linux, or Windows binary for x64 or
|
|
43
|
+
arm64. No Rust toolchain or install script is required by CLI users. Repository
|
|
44
|
+
development requires the pinned Rust toolchain in `rust-toolchain.toml`. The npm
|
|
45
|
+
launcher requires Node.js 22.20+; invoking it with Bun 1.2+ is also supported.
|
|
42
46
|
|
|
43
47
|
## Quick start
|
|
44
48
|
|
|
@@ -157,14 +161,46 @@ and the DeepSeek Cordis check require npm mode with built adapters
|
|
|
157
161
|
|
|
158
162
|
## Validation tiers
|
|
159
163
|
|
|
160
|
-
`npm run validate` always enforces Agent Skills frontmatter compliance, SKILL.md
|
|
164
|
+
`npm run validate` always enforces Agent Skills frontmatter compliance, SKILL.md
|
|
165
|
+
reference resolution, validation of every present runtime manifest, fixed
|
|
166
|
+
contained entrypoints, npm-mode twin parity, and Python syntax through the native
|
|
167
|
+
Rust parser. Generated npm plugins require a runtime manifest for every product
|
|
168
|
+
skill; adapted skills-only repositories may also contain additional prose-only
|
|
169
|
+
skills while retaining a manifest for the primary MCP-targeted skill. The validator
|
|
170
|
+
does not execute project Python during structural validation; JavaScript/Python
|
|
171
|
+
behavioral parity is exercised by the runtime tests.
|
|
161
172
|
|
|
162
173
|
## Release automation
|
|
163
174
|
|
|
175
|
+
The main package and six platform packages use GitHub Actions workflow
|
|
176
|
+
`npm-publish.yml` in `lunarmoon26/harness-alchemist` as their Trusted Publisher.
|
|
177
|
+
The workflow builds each native target on its matching hosted runner, publishes
|
|
178
|
+
platform packages first, then publishes the main package. It uses OIDC instead
|
|
179
|
+
of an `NPM_TOKEN`, requires the tag to match both `package.json` and
|
|
180
|
+
`Cargo.toml`, and accepts only commits contained in `main`.
|
|
181
|
+
|
|
182
|
+
Trusted Publisher configuration is package-scoped. Before the first native
|
|
183
|
+
release, create each new platform package once using an interactive npm session
|
|
184
|
+
with 2FA (a reserved `0.0.0` bootstrap version keeps real releases on OIDC), then
|
|
185
|
+
configure its publisher with npm 11.5.1 or newer:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm trust github @lunarmoon26/harness-alchemist-darwin-arm64 \
|
|
189
|
+
--repo lunarmoon26/harness-alchemist --file npm-publish.yml \
|
|
190
|
+
--allow-publish --yes
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Repeat that command for all six platform package names, then set each package's
|
|
194
|
+
publishing access to **Require two-factor authentication and disallow tokens**.
|
|
195
|
+
The existing main package must retain the same workflow identity and policy.
|
|
196
|
+
Prerelease versions publish under the `next` dist-tag; stable versions publish
|
|
197
|
+
under `latest`.
|
|
198
|
+
|
|
164
199
|
```bash
|
|
165
200
|
# bump package.json version, then:
|
|
166
201
|
npm run sync && npm run verify && git commit -am "Release vX.Y.Z" && git push
|
|
167
|
-
|
|
202
|
+
# after the release commit is merged to main:
|
|
203
|
+
git tag vX.Y.Z && git push origin vX.Y.Z # publishes through npm Trusted Publishing
|
|
168
204
|
```
|
|
169
205
|
|
|
170
206
|
## License
|
package/alchemy.json
CHANGED
package/alchemy.schema.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"runtime": {
|
|
14
14
|
"enum": ["npm", "skills"],
|
|
15
15
|
"default": "npm",
|
|
16
|
-
"description": "'npm' requires the full generated contract (npm package, portable runtime
|
|
16
|
+
"description": "'npm' requires the full generated contract (npm package, a portable runtime manifest for every product skill, OpenCode and Cordis adapters, and Cordis patch). 'skills' requires portable skills and harness manifests, validates runtime manifests when present, and allows additional prose-only skills and single-language scripts while retaining a manifest for the primary MCP-targeted skill."
|
|
17
17
|
},
|
|
18
18
|
"pluginRoot": {
|
|
19
19
|
"type": "string",
|
|
@@ -1,48 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import { createRequire } from "node:module"
|
|
7
|
+
import { spawnSync } from "node:child_process"
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
11
|
+
const extension = process.platform === "win32" ? ".exe" : ""
|
|
8
12
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const platformPackages = {
|
|
14
|
+
darwin: {
|
|
15
|
+
arm64: "@lunarmoon26/harness-alchemist-darwin-arm64",
|
|
16
|
+
x64: "@lunarmoon26/harness-alchemist-darwin-x64",
|
|
17
|
+
},
|
|
18
|
+
linux: {
|
|
19
|
+
arm64: "@lunarmoon26/harness-alchemist-linux-arm64",
|
|
20
|
+
x64: "@lunarmoon26/harness-alchemist-linux-x64",
|
|
21
|
+
},
|
|
22
|
+
win32: {
|
|
23
|
+
arm64: "@lunarmoon26/harness-alchemist-win32-arm64",
|
|
24
|
+
x64: "@lunarmoon26/harness-alchemist-win32-x64",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
12
27
|
|
|
13
|
-
function
|
|
14
|
-
|
|
28
|
+
function installedBinary() {
|
|
29
|
+
const packageName = platformPackages[process.platform]?.[process.arch]
|
|
30
|
+
if (!packageName) return undefined
|
|
31
|
+
try {
|
|
32
|
+
return join(dirname(require.resolve(`${packageName}/package.json`)), `harness-alchemist${extension}`)
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
|
|
16
|
-
|
|
38
|
+
function localBinary() {
|
|
39
|
+
for (const profile of ["release", "debug"]) {
|
|
40
|
+
const candidate = join(packageRoot, "target", profile, `harness-alchemist${extension}`)
|
|
41
|
+
if (existsSync(candidate)) return candidate
|
|
42
|
+
}
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
17
45
|
|
|
18
|
-
|
|
19
|
-
create <directory> Create a universal coding-agent plugin repository.
|
|
20
|
-
validate [directory] Validate a generated repository.
|
|
21
|
-
install-check Install the plugin into local harness CLIs and verify
|
|
22
|
-
discovery (claude, codex, agy, opencode, dsh).
|
|
23
|
-
templates List bundled canonical templates.
|
|
24
|
-
version Print the CLI version.
|
|
25
|
-
help Show this help.
|
|
46
|
+
const binary = process.env.HARNESS_ALCHEMIST_BINARY || installedBinary() || localBinary()
|
|
26
47
|
|
|
27
|
-
|
|
48
|
+
if (!binary) {
|
|
49
|
+
const supported = platformPackages[process.platform]?.[process.arch]
|
|
50
|
+
const reason = supported
|
|
51
|
+
? `The optional package '${supported}' is not installed.`
|
|
52
|
+
: `The platform '${process.platform}-${process.arch}' is not supported.`
|
|
53
|
+
console.error(`Harness Alchemist could not locate its native binary. ${reason}`)
|
|
54
|
+
console.error("Reinstall without --no-optional, or install a supported binary from GitHub Releases.")
|
|
55
|
+
process.exit(1)
|
|
28
56
|
}
|
|
29
57
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
process.
|
|
58
|
+
const result = spawnSync(binary, process.argv.slice(2), {
|
|
59
|
+
stdio: "inherit",
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
HARNESS_ALCHEMIST_PACKAGE_ROOT: packageRoot,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (result.error) {
|
|
67
|
+
console.error(`Failed to start Harness Alchemist: ${result.error.message}`)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
if (result.signal) {
|
|
71
|
+
process.kill(process.pid, result.signal)
|
|
44
72
|
} else {
|
|
45
|
-
|
|
46
|
-
console.error(usage())
|
|
47
|
-
process.exitCode = 2
|
|
73
|
+
process.exit(result.status ?? 1)
|
|
48
74
|
}
|
package/mcp.json
CHANGED
package/package.json
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"version": "0.1.9",
|
|
4
|
-
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
|
-
"type": "module",
|
|
2
|
+
"author": "Haochuan Zhang",
|
|
6
3
|
"bin": {
|
|
7
4
|
"harness-alchemist": "bin/harness-alchemist.mjs"
|
|
8
5
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@lunarmoon26/agent-skill-runtime": "0.1.1"
|
|
8
|
+
},
|
|
9
|
+
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
10
|
+
"dsh": {
|
|
11
|
+
"bundle": {
|
|
12
|
+
"patch": "./cordis.patch.yml"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"bun": ">=1.2.0",
|
|
17
|
+
"node": ">=22.20.0"
|
|
18
|
+
},
|
|
11
19
|
"exports": {
|
|
12
20
|
".": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
21
|
+
"import": "./dist/opencode.js",
|
|
22
|
+
"types": "./dist/opencode.d.ts"
|
|
15
23
|
},
|
|
24
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
16
25
|
"./deepseek": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
}
|
|
20
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
26
|
+
"import": "./dist/deepseek.js",
|
|
27
|
+
"types": "./dist/deepseek.d.ts"
|
|
28
|
+
}
|
|
21
29
|
},
|
|
22
30
|
"files": [
|
|
23
31
|
"dist",
|
|
24
32
|
"bin",
|
|
25
|
-
"lib",
|
|
26
33
|
"templates",
|
|
27
34
|
"skills",
|
|
28
35
|
"alchemy.json",
|
|
@@ -35,20 +42,6 @@
|
|
|
35
42
|
"README.md",
|
|
36
43
|
"LICENSE"
|
|
37
44
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.json",
|
|
40
|
-
"check": "tsc -p tsconfig.json --noEmit",
|
|
41
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
42
|
-
"sync": "node .agents/skills/develop-harness-alchemist/scripts/sync-metadata.mjs",
|
|
43
|
-
"validate": "node .agents/skills/develop-harness-alchemist/scripts/validate.mjs",
|
|
44
|
-
"pack:check": "node .agents/skills/develop-harness-alchemist/scripts/check-package.mjs",
|
|
45
|
-
"verify": "npm run check && npm test && npm run validate && npm run pack:check",
|
|
46
|
-
"prepack": "npm run verify"
|
|
47
|
-
},
|
|
48
|
-
"engines": {
|
|
49
|
-
"node": ">=22.20.0",
|
|
50
|
-
"bun": ">=1.2.0"
|
|
51
|
-
},
|
|
52
45
|
"keywords": [
|
|
53
46
|
"agent-skill",
|
|
54
47
|
"antigravity",
|
|
@@ -58,8 +51,16 @@
|
|
|
58
51
|
"opencode",
|
|
59
52
|
"scaffolding"
|
|
60
53
|
],
|
|
61
|
-
"
|
|
62
|
-
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"main": "./dist/opencode.js",
|
|
56
|
+
"name": "harness-alchemist",
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"@lunarmoon26/harness-alchemist-darwin-arm64": "0.2.0-alpha.1",
|
|
59
|
+
"@lunarmoon26/harness-alchemist-darwin-x64": "0.2.0-alpha.1",
|
|
60
|
+
"@lunarmoon26/harness-alchemist-linux-arm64": "0.2.0-alpha.1",
|
|
61
|
+
"@lunarmoon26/harness-alchemist-linux-x64": "0.2.0-alpha.1",
|
|
62
|
+
"@lunarmoon26/harness-alchemist-win32-arm64": "0.2.0-alpha.1",
|
|
63
|
+
"@lunarmoon26/harness-alchemist-win32-x64": "0.2.0-alpha.1"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
65
66
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -73,22 +74,15 @@
|
|
|
73
74
|
"optional": true
|
|
74
75
|
}
|
|
75
76
|
},
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
"@types/node": "^26.2.0",
|
|
80
|
-
"pyodide": "^314.0.5",
|
|
81
|
-
"typescript": "^5.9.3"
|
|
77
|
+
"preferUnplugged": true,
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public"
|
|
82
80
|
},
|
|
83
|
-
"author": "Haochuan Zhang",
|
|
84
|
-
"license": "MIT",
|
|
85
81
|
"repository": {
|
|
86
82
|
"type": "git",
|
|
87
83
|
"url": "git+https://github.com/lunarmoon26/harness-alchemist.git"
|
|
88
84
|
},
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
85
|
+
"type": "module",
|
|
86
|
+
"types": "./dist/opencode.d.ts",
|
|
87
|
+
"version": "0.2.0-alpha.1"
|
|
94
88
|
}
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-alchemist",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.0-alpha.1",
|
|
5
5
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Haochuan Zhang"
|
|
@@ -57,13 +57,17 @@ The package root exports OpenCode. The `./deepseek` subpath exports Cordis. Keep
|
|
|
57
57
|
## GitHub Release Publishing
|
|
58
58
|
|
|
59
59
|
`.github/workflows/npm-publish.yml` publishes when a `vX.Y.Z` tag is pushed
|
|
60
|
-
(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
(including a prerelease tag such as `vX.Y.Z-rc.1`). The workflow requires the
|
|
61
|
+
tag to match the committed `package.json` version, checks that the tagged
|
|
62
|
+
commit is contained in `main`, verifies synchronized metadata and the package,
|
|
63
|
+
then publishes through npm Trusted Publishing with provenance.
|
|
64
|
+
Prerelease versions publish under npm's `next` dist-tag; stable versions publish
|
|
65
|
+
under `latest`.
|
|
66
|
+
|
|
67
|
+
Before the first release, configure the npm package's Trusted Publisher for
|
|
68
|
+
GitHub Actions with the repository owner, repository name, workflow filename
|
|
69
|
+
`npm-publish.yml`, and no environment. Do not create an `NPM_TOKEN` secret.
|
|
70
|
+
The workflow never publishes from pull requests or branch pushes.
|
|
67
71
|
|
|
68
72
|
## Public Catalog Metadata
|
|
69
73
|
|
package/templates/README.md
CHANGED
|
@@ -4,6 +4,6 @@ Each published versioned directory is a complete, immutable scaffold contract.
|
|
|
4
4
|
|
|
5
5
|
- `v0.1.0/universal-typescript/` is the unreleased canonical universal plugin layout and may be updated in place.
|
|
6
6
|
- `v0.1.0/licenses/` contains license source text used during rendering.
|
|
7
|
-
-
|
|
7
|
+
- The native Rust generator maps template directory names, renders metadata tokens, injects `generated/validate.mjs` and the selected license, validates in staging, and atomically installs the result.
|
|
8
8
|
|
|
9
9
|
After a version is published, make incompatible changes in a new `v<version>/` directory and require callers to select that version explicitly. Do not silently redirect old template identifiers.
|
|
@@ -11,7 +11,7 @@ import { discoverSkillTools } from "@lunarmoon26/agent-skill-runtime"
|
|
|
11
11
|
const AGENT_PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
|
|
12
12
|
const AGENT_PLUGIN_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"
|
|
13
13
|
const SKILL_RUNTIME_PACKAGE = "@lunarmoon26/agent-skill-runtime"
|
|
14
|
-
const SKILL_RUNTIME_VERSION = "0.1.
|
|
14
|
+
const SKILL_RUNTIME_VERSION = "0.1.1"
|
|
15
15
|
|
|
16
16
|
const PROJECT_REQUIRED_FILES = [
|
|
17
17
|
".agents/plugins/marketplace.json",
|
|
@@ -46,9 +46,9 @@ Validates a universal Claude, Codex, OpenCode, Antigravity, and DeepSeek
|
|
|
46
46
|
plugin scaffold. By default the project is resolved from the current working
|
|
47
47
|
directory or from the script's containing generated project.
|
|
48
48
|
|
|
49
|
-
Product skills are checked against the Agent Skills specification and
|
|
50
|
-
|
|
51
|
-
installed, Python
|
|
49
|
+
Product skills are checked against the Agent Skills specification, and every
|
|
50
|
+
present runtime manifest is validated. When the optional 'pyodide' devDependency
|
|
51
|
+
is installed, Python entrypoints are also syntax-checked and smoke-executed
|
|
52
52
|
in a WebAssembly CPython sandbox.
|
|
53
53
|
|
|
54
54
|
Options:
|
|
@@ -668,9 +668,9 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
668
668
|
}
|
|
669
669
|
if (productSkillFiles.includes(skillFile)) {
|
|
670
670
|
const manifestPath = join(dirname(skillFile), "skill-runtime.json")
|
|
671
|
-
if (!existsSync(manifestPath)) {
|
|
671
|
+
if (!existsSync(manifestPath) && (runtime === "npm" || directoryName === pluginName)) {
|
|
672
672
|
errors.push(`${skillFile}: missing skill-runtime.json`)
|
|
673
|
-
} else {
|
|
673
|
+
} else if (existsSync(manifestPath)) {
|
|
674
674
|
runtimeManifestPaths.push(relative(pluginRoot, manifestPath))
|
|
675
675
|
}
|
|
676
676
|
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts, runtime)
|
|
@@ -678,7 +678,7 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
-
if (runtimeManifestPaths.length
|
|
681
|
+
if (runtimeManifestPaths.length > 0) {
|
|
682
682
|
try {
|
|
683
683
|
const tools = await discoverSkillTools({ pluginRoot, manifestPaths: runtimeManifestPaths })
|
|
684
684
|
for (const tool of tools) {
|
|
@@ -7,43 +7,68 @@ on:
|
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: read
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: npm-publish-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: false
|
|
11
14
|
|
|
12
15
|
jobs:
|
|
13
16
|
publish:
|
|
17
|
+
name: Publish package
|
|
14
18
|
if: github.event.created && !github.event.deleted
|
|
15
|
-
runs-on: ubuntu-
|
|
19
|
+
runs-on: ubuntu-24.04
|
|
16
20
|
timeout-minutes: 15
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
id-token: write # Required for npm Trusted Publishing OIDC.
|
|
17
24
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
19
|
-
|
|
25
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
20
30
|
with:
|
|
21
|
-
node-version:
|
|
31
|
+
node-version: "24.19.0"
|
|
22
32
|
registry-url: https://registry.npmjs.org
|
|
23
|
-
|
|
33
|
+
package-manager-cache: false
|
|
34
|
+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
24
35
|
with:
|
|
25
|
-
bun-version: 1.
|
|
36
|
+
bun-version: 1.4.0
|
|
37
|
+
no-cache: true
|
|
26
38
|
|
|
27
|
-
- name:
|
|
28
|
-
id: version
|
|
39
|
+
- name: Verify release tag
|
|
29
40
|
shell: bash
|
|
30
41
|
run: |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if [[
|
|
34
|
-
echo "::error::Tag '$
|
|
42
|
+
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
|
43
|
+
EXPECTED_TAG="v${PACKAGE_VERSION}"
|
|
44
|
+
if [[ "$GITHUB_REF_NAME" != "$EXPECTED_TAG" ]]; then
|
|
45
|
+
echo "::error::Tag '$GITHUB_REF_NAME' must match package version '$EXPECTED_TAG'"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
git fetch --no-tags origin main:refs/remotes/origin/main
|
|
50
|
+
TAG_COMMIT="$(git rev-list -n 1 "$GITHUB_REF_NAME")"
|
|
51
|
+
if ! git merge-base --is-ancestor "$TAG_COMMIT" refs/remotes/origin/main; then
|
|
52
|
+
echo "::error::Release tag '$GITHUB_REF_NAME' must point to a commit contained in main"
|
|
35
53
|
exit 1
|
|
36
54
|
fi
|
|
37
|
-
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
38
55
|
|
|
39
56
|
- run: npm ci
|
|
40
|
-
- name:
|
|
41
|
-
run:
|
|
42
|
-
|
|
57
|
+
- name: Verify synchronized metadata
|
|
58
|
+
run: |
|
|
59
|
+
npm run sync
|
|
60
|
+
git diff --exit-code
|
|
43
61
|
- run: npm run verify
|
|
44
62
|
- name: Verify npm package contents
|
|
45
63
|
run: npm pack --dry-run
|
|
64
|
+
- name: Select npm dist-tag
|
|
65
|
+
shell: bash
|
|
66
|
+
run: |
|
|
67
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
68
|
+
if [[ "$VERSION" == *-* ]]; then
|
|
69
|
+
echo "NPM_DIST_TAG=next" >> "$GITHUB_ENV"
|
|
70
|
+
else
|
|
71
|
+
echo "NPM_DIST_TAG=latest" >> "$GITHUB_ENV"
|
|
72
|
+
fi
|
|
46
73
|
- name: Publish npm package
|
|
47
|
-
run: npm publish --
|
|
48
|
-
env:
|
|
49
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
74
|
+
run: npm publish --access public --tag "$NPM_DIST_TAG"
|
|
@@ -57,12 +57,14 @@ npm pack --dry-run
|
|
|
57
57
|
## GitHub Release Publishing
|
|
58
58
|
|
|
59
59
|
`.github/workflows/npm-publish.yml` publishes when a `vX.Y.Z` tag is pushed
|
|
60
|
-
(
|
|
61
|
-
the
|
|
62
|
-
verifies
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
(including a prerelease tag such as `vX.Y.Z-rc.1`). The workflow requires the
|
|
61
|
+
tag to match the committed `package.json` version, checks that the tagged
|
|
62
|
+
commit is contained in `main`, verifies synchronized metadata and the package,
|
|
63
|
+
then publishes through npm Trusted Publishing with provenance.
|
|
64
|
+
|
|
65
|
+
Before the first release, configure the npm package's Trusted Publisher for
|
|
66
|
+
GitHub Actions with the repository owner, repository name, workflow filename
|
|
67
|
+
`npm-publish.yml`, and no environment. Do not create an `NPM_TOKEN` secret.
|
|
66
68
|
|
|
67
69
|
## Claude Code
|
|
68
70
|
|
package/lib/create.mjs
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { randomUUID } from "node:crypto"
|
|
4
|
-
import { existsSync } from "node:fs"
|
|
5
|
-
import { chmod, cp, mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises"
|
|
6
|
-
import { basename, dirname, join, resolve } from "node:path"
|
|
7
|
-
import { fileURLToPath } from "node:url"
|
|
8
|
-
import { spawnSync } from "node:child_process"
|
|
9
|
-
|
|
10
|
-
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
11
|
-
const PACKAGE_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
12
|
-
const LICENSES = new Set(["MIT", "Apache-2.0", "UNLICENSED"])
|
|
13
|
-
const CANONICAL_VERSION = "v0.1.0"
|
|
14
|
-
|
|
15
|
-
export function listTemplates() {
|
|
16
|
-
return `${CANONICAL_VERSION} (canonical)`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function usage() {
|
|
20
|
-
return `Usage: harness-alchemist create <output-directory> [options]
|
|
21
|
-
|
|
22
|
-
Create one TypeScript repository for Claude Code, Codex/ChatGPT, OpenCode,
|
|
23
|
-
Google Antigravity, and DeepSeek Harness/Cordis.
|
|
24
|
-
|
|
25
|
-
Required:
|
|
26
|
-
--description <text> One-sentence plugin description.
|
|
27
|
-
--author <name> Author or team name.
|
|
28
|
-
--repository <source> GitHub owner/repo or a full Git URL.
|
|
29
|
-
|
|
30
|
-
Options:
|
|
31
|
-
--name <name> Plugin name; defaults to output directory name.
|
|
32
|
-
--package <name> npm package; defaults to plugin name. Its basename
|
|
33
|
-
must equal the plugin name.
|
|
34
|
-
--display-name <text> Human-readable name; defaults from plugin name.
|
|
35
|
-
--marketplace <name> Claude/Codex marketplace name; defaults to
|
|
36
|
-
<plugin-name>-plugins.
|
|
37
|
-
--template <version> Canonical template version (default: v0.1.0).
|
|
38
|
-
--license <id> MIT (default), Apache-2.0, or UNLICENSED.
|
|
39
|
-
--dry-run Validate inputs and print the files without writing.
|
|
40
|
-
--help Show this help.
|
|
41
|
-
|
|
42
|
-
The destination must be missing or empty. Existing content is never replaced.`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseArgs(argv) {
|
|
46
|
-
const options = { license: "MIT", templateVersion: CANONICAL_VERSION, dryRun: false }
|
|
47
|
-
const valueOptions = new Map([
|
|
48
|
-
["--name", "name"],
|
|
49
|
-
["--description", "description"],
|
|
50
|
-
["--package", "packageName"],
|
|
51
|
-
["--author", "author"],
|
|
52
|
-
["--repository", "repository"],
|
|
53
|
-
["--display-name", "displayName"],
|
|
54
|
-
["--marketplace", "marketplace"],
|
|
55
|
-
["--template", "templateVersion"],
|
|
56
|
-
["--license", "license"],
|
|
57
|
-
])
|
|
58
|
-
|
|
59
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
60
|
-
const arg = argv[index]
|
|
61
|
-
if (arg === "--help" || arg === "-h") return { help: true }
|
|
62
|
-
if (arg === "--dry-run") {
|
|
63
|
-
options.dryRun = true
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
if (valueOptions.has(arg)) {
|
|
67
|
-
const value = argv[index + 1]
|
|
68
|
-
if (!value || value.startsWith("--")) throw new Error(`${arg} requires a value`)
|
|
69
|
-
options[valueOptions.get(arg)] = value
|
|
70
|
-
index += 1
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`)
|
|
74
|
-
if (options.output) throw new Error("Only one output directory may be supplied")
|
|
75
|
-
options.output = arg
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { ...options, help: false }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function titleCase(name) {
|
|
82
|
-
return name
|
|
83
|
-
.split("-")
|
|
84
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
85
|
-
.join(" ")
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function normalizeRepository(source) {
|
|
89
|
-
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source)) {
|
|
90
|
-
return { installSource: source, url: `https://github.com/${source}` }
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
const url = new URL(source)
|
|
94
|
-
const githubMatch = url.hostname === "github.com" && url.pathname.match(/^\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/)
|
|
95
|
-
return {
|
|
96
|
-
installSource: githubMatch ? `${githubMatch[1]}/${githubMatch[2]}` : source,
|
|
97
|
-
url: source.replace(/\.git$/, ""),
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
if (/^git@[^:]+:.+/.test(source)) return { installSource: source, url: source }
|
|
101
|
-
throw new Error("--repository must be GitHub owner/repo or a full Git URL")
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function validateOptions(options) {
|
|
106
|
-
if (!options.output) throw new Error("An output directory is required")
|
|
107
|
-
const name = options.name ?? basename(resolve(options.output))
|
|
108
|
-
if (!NAME_PATTERN.test(name) || name.length > 56) {
|
|
109
|
-
throw new Error("Plugin name must be <=56 characters using lowercase letters, digits, and single hyphens")
|
|
110
|
-
}
|
|
111
|
-
const packageName = options.packageName ?? name
|
|
112
|
-
if (!PACKAGE_PATTERN.test(packageName) || packageName.split("/").at(-1) !== name) {
|
|
113
|
-
throw new Error("npm package must be unscoped or @scoped and have the plugin name as its basename")
|
|
114
|
-
}
|
|
115
|
-
if (!options.description?.trim()) throw new Error("--description is required")
|
|
116
|
-
if (options.description.includes("\n")) throw new Error("--description must be one line")
|
|
117
|
-
if (!options.author?.trim()) throw new Error("--author is required")
|
|
118
|
-
if (!options.repository?.trim()) throw new Error("--repository is required")
|
|
119
|
-
if (!LICENSES.has(options.license)) {
|
|
120
|
-
throw new Error("--license must be MIT, Apache-2.0, or UNLICENSED")
|
|
121
|
-
}
|
|
122
|
-
if (options.templateVersion !== CANONICAL_VERSION) {
|
|
123
|
-
throw new Error(`Unknown template version '${options.templateVersion}'. Available: ${CANONICAL_VERSION}`)
|
|
124
|
-
}
|
|
125
|
-
const marketplace = options.marketplace ?? `${name}-plugins`
|
|
126
|
-
if (!NAME_PATTERN.test(marketplace) || marketplace.length > 64) {
|
|
127
|
-
throw new Error("Marketplace name must be <=64 characters in lowercase kebab-case")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
...options,
|
|
132
|
-
output: resolve(options.output),
|
|
133
|
-
name,
|
|
134
|
-
packageName,
|
|
135
|
-
marketplace,
|
|
136
|
-
displayName: options.displayName?.trim() || titleCase(name),
|
|
137
|
-
description: options.description.trim(),
|
|
138
|
-
author: options.author.trim(),
|
|
139
|
-
repository: normalizeRepository(options.repository.trim()),
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function destinationState(path) {
|
|
144
|
-
if (!existsSync(path)) return "missing"
|
|
145
|
-
const info = await stat(path)
|
|
146
|
-
if (!info.isDirectory()) return "not-directory"
|
|
147
|
-
return (await readdir(path)).length === 0 ? "empty" : "non-empty"
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function replaceTokens(content, tokens) {
|
|
151
|
-
return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, key) => {
|
|
152
|
-
if (!(key in tokens)) throw new Error(`Unknown template token ${match}`)
|
|
153
|
-
return tokens[key]
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function mapSegment(segment, metadata) {
|
|
158
|
-
if (segment === "shared-skill") return metadata.name
|
|
159
|
-
if (segment === "develop-template") return `develop-${metadata.name}`
|
|
160
|
-
return segment.endsWith(".tpl") ? segment.slice(0, -4) : segment
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function renderTemplates(source, destination, metadata, tokens, relative = []) {
|
|
164
|
-
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
165
|
-
const nextRelative = [...relative, mapSegment(entry.name, metadata)]
|
|
166
|
-
const target = join(destination, ...nextRelative)
|
|
167
|
-
const input = join(source, entry.name)
|
|
168
|
-
if (entry.isDirectory()) {
|
|
169
|
-
await mkdir(target, { recursive: true })
|
|
170
|
-
await renderTemplates(input, destination, metadata, tokens, nextRelative)
|
|
171
|
-
} else if (entry.isFile()) {
|
|
172
|
-
const content = replaceTokens(await readFile(input, "utf8"), tokens)
|
|
173
|
-
await mkdir(dirname(target), { recursive: true })
|
|
174
|
-
await writeFile(target, content)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function licenseText(skillRoot, metadata) {
|
|
180
|
-
if (metadata.license === "UNLICENSED") {
|
|
181
|
-
return `Copyright (c) ${new Date().getFullYear()} ${metadata.author}\n\nAll rights reserved. This project is not licensed for redistribution.\n`
|
|
182
|
-
}
|
|
183
|
-
if (metadata.license === "Apache-2.0") {
|
|
184
|
-
return readFile(join(skillRoot, "licenses/Apache-2.0.txt"), "utf8")
|
|
185
|
-
}
|
|
186
|
-
return `MIT License
|
|
187
|
-
|
|
188
|
-
Copyright (c) ${new Date().getFullYear()} ${metadata.author}
|
|
189
|
-
|
|
190
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
191
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
192
|
-
in the Software without restriction, including without limitation the rights
|
|
193
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
194
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
195
|
-
furnished to do so, subject to the following conditions:
|
|
196
|
-
|
|
197
|
-
The above copyright notice and this permission notice shall be included in all
|
|
198
|
-
copies or substantial portions of the Software.
|
|
199
|
-
|
|
200
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
201
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
202
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
203
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
204
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
205
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
206
|
-
SOFTWARE.
|
|
207
|
-
`
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async function listTemplateFiles(root, metadata, relative = []) {
|
|
211
|
-
const files = []
|
|
212
|
-
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
213
|
-
const next = [...relative, mapSegment(entry.name, metadata)]
|
|
214
|
-
if (entry.isDirectory()) files.push(...(await listTemplateFiles(join(root, entry.name), metadata, next)))
|
|
215
|
-
else if (entry.isFile()) files.push(next.join("/"))
|
|
216
|
-
}
|
|
217
|
-
files.push(`.agents/skills/develop-${metadata.name}/scripts/validate.mjs`, "LICENSE")
|
|
218
|
-
return files.sort()
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export async function runCreate(argv) {
|
|
222
|
-
let options
|
|
223
|
-
try {
|
|
224
|
-
const parsed = parseArgs(argv)
|
|
225
|
-
if (parsed.help) {
|
|
226
|
-
console.log(usage())
|
|
227
|
-
return 0
|
|
228
|
-
}
|
|
229
|
-
options = validateOptions(parsed)
|
|
230
|
-
} catch (error) {
|
|
231
|
-
console.error(error.message)
|
|
232
|
-
console.error(usage())
|
|
233
|
-
return 2
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const scriptPath = fileURLToPath(import.meta.url)
|
|
237
|
-
const projectRoot = dirname(dirname(scriptPath))
|
|
238
|
-
const canonicalRoot = join(projectRoot, "templates", options.templateVersion)
|
|
239
|
-
const templateRoot = join(canonicalRoot, "universal-typescript")
|
|
240
|
-
const generatorVersion = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8")).version
|
|
241
|
-
const state = await destinationState(options.output)
|
|
242
|
-
if (state === "not-directory" || state === "non-empty") {
|
|
243
|
-
console.error(`Destination must be missing or empty: ${options.output}`)
|
|
244
|
-
return 1
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const tokens = {
|
|
248
|
-
NAME: options.name,
|
|
249
|
-
NAME_JSON: JSON.stringify(options.name),
|
|
250
|
-
TOOL_NAME: `${options.name.replaceAll("-", "_")}_run`,
|
|
251
|
-
TOOL_NAME_JSON: JSON.stringify(`${options.name.replaceAll("-", "_")}_run`),
|
|
252
|
-
DISPLAY_NAME: options.displayName,
|
|
253
|
-
DISPLAY_NAME_JSON: JSON.stringify(options.displayName),
|
|
254
|
-
DESCRIPTION: options.description,
|
|
255
|
-
DESCRIPTION_JSON: JSON.stringify(options.description),
|
|
256
|
-
TEMPLATE_VERSION: options.templateVersion,
|
|
257
|
-
GENERATOR_VERSION: generatorVersion,
|
|
258
|
-
CREATED_AT: new Date().toISOString(),
|
|
259
|
-
SHARED_SKILL_DESCRIPTION_JSON: JSON.stringify(
|
|
260
|
-
`${options.description} Use when the user requests ${options.displayName} workflows or explicitly asks to use the ${options.name} plugin.`,
|
|
261
|
-
),
|
|
262
|
-
DEVELOPMENT_SKILL_DESCRIPTION_JSON: JSON.stringify(
|
|
263
|
-
`Develop, validate, and publish the ${options.displayName} universal coding-agent plugin. Use when modifying its shared skills, Claude or Codex manifests, OpenCode npm entrypoint, Antigravity bundle, or DeepSeek Cordis integration.`,
|
|
264
|
-
),
|
|
265
|
-
PACKAGE_NAME: options.packageName,
|
|
266
|
-
PACKAGE_NAME_JSON: JSON.stringify(options.packageName),
|
|
267
|
-
AUTHOR: options.author,
|
|
268
|
-
AUTHOR_JSON: JSON.stringify(options.author),
|
|
269
|
-
REPOSITORY_SOURCE: options.repository.installSource,
|
|
270
|
-
REPOSITORY_SOURCE_JSON: JSON.stringify(options.repository.installSource),
|
|
271
|
-
REPOSITORY_URL: options.repository.url,
|
|
272
|
-
REPOSITORY_URL_JSON: JSON.stringify(options.repository.url),
|
|
273
|
-
MARKETPLACE: options.marketplace,
|
|
274
|
-
MARKETPLACE_JSON: JSON.stringify(options.marketplace),
|
|
275
|
-
LICENSE: options.license,
|
|
276
|
-
LICENSE_JSON: JSON.stringify(options.license),
|
|
277
|
-
YEAR: String(new Date().getFullYear()),
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (options.dryRun) {
|
|
281
|
-
console.log(JSON.stringify({
|
|
282
|
-
output: options.output,
|
|
283
|
-
template: options.templateVersion,
|
|
284
|
-
plugin: options.name,
|
|
285
|
-
package: options.packageName,
|
|
286
|
-
files: await listTemplateFiles(templateRoot, options),
|
|
287
|
-
}, null, 2))
|
|
288
|
-
return 0
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
await mkdir(dirname(options.output), { recursive: true })
|
|
292
|
-
const staging = join(dirname(options.output), `.${basename(options.output)}.scaffold-${randomUUID()}`)
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
await mkdir(staging)
|
|
296
|
-
await renderTemplates(templateRoot, staging, options, tokens)
|
|
297
|
-
const localScripts = join(staging, ".agents/skills", `develop-${options.name}`, "scripts")
|
|
298
|
-
await mkdir(localScripts, { recursive: true })
|
|
299
|
-
await cp(join(projectRoot, "lib/validate.mjs"), join(localScripts, "validate.mjs"))
|
|
300
|
-
const skillScripts = join(staging, "skills", options.name, "scripts")
|
|
301
|
-
if (existsSync(skillScripts)) {
|
|
302
|
-
for (const entry of await readdir(skillScripts)) {
|
|
303
|
-
await chmod(join(skillScripts, entry), 0o755)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
await writeFile(join(staging, "LICENSE"), await licenseText(canonicalRoot, options))
|
|
307
|
-
|
|
308
|
-
const validation = spawnSync(process.execPath, [join(projectRoot, "lib/validate.mjs"), staging], {
|
|
309
|
-
encoding: "utf8",
|
|
310
|
-
})
|
|
311
|
-
if (validation.status !== 0) {
|
|
312
|
-
throw new Error(`Generated project failed validation:\n${validation.stderr || validation.stdout}`)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (state === "empty") await rmdir(options.output)
|
|
316
|
-
await rename(staging, options.output)
|
|
317
|
-
|
|
318
|
-
console.log(`Created universal plugin scaffold at ${options.output}`)
|
|
319
|
-
console.log(`Next: cd ${JSON.stringify(options.output)} && npm install && npm run verify`)
|
|
320
|
-
console.log(`Bun: cd ${JSON.stringify(options.output)} && bun install && bun run verify`)
|
|
321
|
-
return 0
|
|
322
|
-
} catch (error) {
|
|
323
|
-
await rm(staging, { recursive: true, force: true })
|
|
324
|
-
console.error(error.message)
|
|
325
|
-
return 1
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
|
|
330
|
-
process.exitCode = await runCreate(process.argv.slice(2))
|
|
331
|
-
}
|
package/lib/install-check.mjs
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, readdirSync } from "node:fs"
|
|
4
|
-
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
5
|
-
import { tmpdir } from "node:os"
|
|
6
|
-
import { dirname, join, resolve } from "node:path"
|
|
7
|
-
import { fileURLToPath } from "node:url"
|
|
8
|
-
import { spawnSync } from "node:child_process"
|
|
9
|
-
|
|
10
|
-
const TIMEOUT_MS = 300_000
|
|
11
|
-
|
|
12
|
-
function findProjectRoot(start) {
|
|
13
|
-
let current = resolve(start)
|
|
14
|
-
let conventionalRoot
|
|
15
|
-
while (true) {
|
|
16
|
-
if (existsSync(join(current, "alchemy.json"))) return current
|
|
17
|
-
if (
|
|
18
|
-
!conventionalRoot &&
|
|
19
|
-
existsSync(join(current, "package.json")) &&
|
|
20
|
-
existsSync(join(current, ".claude-plugin")) &&
|
|
21
|
-
existsSync(join(current, ".codex-plugin"))
|
|
22
|
-
) {
|
|
23
|
-
conventionalRoot = current
|
|
24
|
-
}
|
|
25
|
-
const parent = dirname(current)
|
|
26
|
-
if (parent === current) return conventionalRoot
|
|
27
|
-
current = parent
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function run(command, args, options = {}) {
|
|
32
|
-
const result = spawnSync(command, args, {
|
|
33
|
-
encoding: "utf8",
|
|
34
|
-
timeout: TIMEOUT_MS,
|
|
35
|
-
...options,
|
|
36
|
-
})
|
|
37
|
-
if (result.error?.code === "ENOENT") return { missing: true }
|
|
38
|
-
return {
|
|
39
|
-
ok: result.status === 0,
|
|
40
|
-
status: result.status,
|
|
41
|
-
stdout: result.stdout ?? "",
|
|
42
|
-
stderr: result.stderr ?? "",
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function firstLine(text) {
|
|
47
|
-
return text.trim().split("\n")[0] ?? ""
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function usage() {
|
|
51
|
-
return `Usage: harness-alchemist install-check [project-directory] [options]
|
|
52
|
-
|
|
53
|
-
Install-level verification: drives the local harness CLIs (claude, codex,
|
|
54
|
-
agy, opencode, dsh) against the project's plugin package and asserts each
|
|
55
|
-
harness can discover it. Static validation is a prerequisite; run
|
|
56
|
-
'harness-alchemist validate' first.
|
|
57
|
-
|
|
58
|
-
Options:
|
|
59
|
-
--harness <id> Limit to one harness (claude, codex, agy, opencode, dsh).
|
|
60
|
-
Repeatable.
|
|
61
|
-
--keep Keep installed plugins and marketplaces after the check.
|
|
62
|
-
--json Print a machine-readable result.
|
|
63
|
-
--help Show this help.`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseArgs(argv) {
|
|
67
|
-
const options = { harnesses: [], keep: false, json: false }
|
|
68
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
69
|
-
const arg = argv[index]
|
|
70
|
-
if (arg === "--help" || arg === "-h") return { help: true }
|
|
71
|
-
if (arg === "--keep") options.keep = true
|
|
72
|
-
else if (arg === "--json") options.json = true
|
|
73
|
-
else if (arg === "--harness") {
|
|
74
|
-
const value = argv[index + 1]
|
|
75
|
-
if (!value || value.startsWith("--")) throw new Error("--harness requires a value")
|
|
76
|
-
if (!["claude", "codex", "agy", "opencode", "dsh"].includes(value)) {
|
|
77
|
-
throw new Error(`Unknown harness '${value}'. Choose from claude, codex, agy, opencode, dsh`)
|
|
78
|
-
}
|
|
79
|
-
options.harnesses.push(value)
|
|
80
|
-
index += 1
|
|
81
|
-
} else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`)
|
|
82
|
-
else if (options.project) throw new Error("Only one project directory may be supplied")
|
|
83
|
-
else options.project = arg
|
|
84
|
-
}
|
|
85
|
-
return options
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function readJson(path, problems) {
|
|
89
|
-
try {
|
|
90
|
-
return JSON.parse(await readFile(path, "utf8"))
|
|
91
|
-
} catch (error) {
|
|
92
|
-
problems.push(`${path}: invalid JSON (${error.message})`)
|
|
93
|
-
return undefined
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function skillNames(skillsDirectory) {
|
|
98
|
-
if (!existsSync(skillsDirectory)) return []
|
|
99
|
-
return readdirSync(skillsDirectory)
|
|
100
|
-
.filter((entry) => existsSync(join(skillsDirectory, entry, "SKILL.md")))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function runInstallCheck(argv) {
|
|
104
|
-
let options
|
|
105
|
-
try {
|
|
106
|
-
options = parseArgs(argv)
|
|
107
|
-
} catch (error) {
|
|
108
|
-
console.error(error.message)
|
|
109
|
-
console.error(usage())
|
|
110
|
-
return 2
|
|
111
|
-
}
|
|
112
|
-
if (options.help) {
|
|
113
|
-
console.log(usage())
|
|
114
|
-
return 0
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
118
|
-
const root = options.project
|
|
119
|
-
? resolve(options.project)
|
|
120
|
-
: findProjectRoot(process.cwd()) ?? findProjectRoot(scriptDirectory)
|
|
121
|
-
if (!root) {
|
|
122
|
-
console.error("Could not find a universal plugin project. Pass its directory explicitly.")
|
|
123
|
-
return 2
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const problems = []
|
|
127
|
-
const layoutPath = join(root, "alchemy.json")
|
|
128
|
-
const layout = existsSync(layoutPath) ? (await readJson(layoutPath, problems)) ?? {} : {}
|
|
129
|
-
const pluginRoot = resolve(root, layout.pluginRoot ?? ".")
|
|
130
|
-
const runtime = layout.runtime ?? "npm"
|
|
131
|
-
|
|
132
|
-
const claudePlugin = await readJson(join(pluginRoot, ".claude-plugin/plugin.json"), problems)
|
|
133
|
-
const claudeMarketplace = await readJson(join(root, ".claude-plugin/marketplace.json"), problems)
|
|
134
|
-
if (problems.length > 0) {
|
|
135
|
-
for (const problem of problems) console.error(problem)
|
|
136
|
-
return 1
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const pluginName = claudePlugin.name
|
|
140
|
-
const marketplaceName = claudeMarketplace.name
|
|
141
|
-
const skills = skillNames(join(pluginRoot, "skills"))
|
|
142
|
-
const selected = options.harnesses.length > 0
|
|
143
|
-
? options.harnesses
|
|
144
|
-
: ["claude", "codex", "agy", "opencode", "dsh"]
|
|
145
|
-
|
|
146
|
-
const results = []
|
|
147
|
-
const record = (harness, status, details) => {
|
|
148
|
-
results.push({ harness, status, details })
|
|
149
|
-
if (options.json) return
|
|
150
|
-
const marker = status === "pass" ? "✔" : status === "skip" ? "○" : "✖"
|
|
151
|
-
console.log(`${marker} ${harness}: ${status}`)
|
|
152
|
-
for (const detail of details) console.log(` ${detail}`)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const adapterPath = join(pluginRoot, "dist/opencode.js")
|
|
156
|
-
|
|
157
|
-
for (const harness of selected) {
|
|
158
|
-
if (harness === "claude") {
|
|
159
|
-
const details = []
|
|
160
|
-
let status = "pass"
|
|
161
|
-
const steps = [
|
|
162
|
-
["validate", pluginRoot, "--strict"],
|
|
163
|
-
["marketplace", "add", root],
|
|
164
|
-
["install", `${pluginName}@${marketplaceName}`],
|
|
165
|
-
["details", pluginName],
|
|
166
|
-
]
|
|
167
|
-
for (const args of steps) {
|
|
168
|
-
const result = run("claude", ["plugin", ...args])
|
|
169
|
-
if (result.missing) { status = "skip"; details.push("claude CLI not found"); break }
|
|
170
|
-
if (!result.ok) {
|
|
171
|
-
status = "fail"
|
|
172
|
-
details.push(`\`${command} ${args.join(" ")}\` failed: ${firstLine(result.stderr || result.stdout)}`)
|
|
173
|
-
break
|
|
174
|
-
}
|
|
175
|
-
if (args[0] === "details") {
|
|
176
|
-
for (const skill of skills) {
|
|
177
|
-
if (!result.stdout.includes(skill)) {
|
|
178
|
-
status = "fail"
|
|
179
|
-
details.push(`skill '${skill}' missing from plugin details`)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
details.push(`${skills.length} skills registered`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!options.keep) {
|
|
186
|
-
run("claude", ["plugin", "uninstall", `${pluginName}@${marketplaceName}`])
|
|
187
|
-
run("claude", ["plugin", "marketplace", "remove", marketplaceName])
|
|
188
|
-
}
|
|
189
|
-
record(harness, status, details)
|
|
190
|
-
} else if (harness === "codex") {
|
|
191
|
-
const details = []
|
|
192
|
-
let status = "pass"
|
|
193
|
-
const add = run("codex", ["plugin", "marketplace", "add", root])
|
|
194
|
-
if (add.missing) {
|
|
195
|
-
record(harness, "skip", ["codex CLI not found"])
|
|
196
|
-
continue
|
|
197
|
-
}
|
|
198
|
-
if (!add.ok) {
|
|
199
|
-
record(harness, "fail", [`marketplace add failed: ${firstLine(add.stderr || add.stdout)}`])
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
202
|
-
const install = run("codex", ["plugin", "add", `${pluginName}@${marketplaceName}`])
|
|
203
|
-
if (!install.ok) {
|
|
204
|
-
status = "fail"
|
|
205
|
-
details.push(`plugin add failed: ${firstLine(install.stderr || install.stdout)}`)
|
|
206
|
-
} else {
|
|
207
|
-
const list = run("codex", ["plugin", "list"])
|
|
208
|
-
if (!list.ok || !list.stdout.includes(`${pluginName}@${marketplaceName}`)) {
|
|
209
|
-
status = "fail"
|
|
210
|
-
details.push("plugin not listed as installed")
|
|
211
|
-
} else if (!list.stdout.includes("installed, enabled")) {
|
|
212
|
-
status = "fail"
|
|
213
|
-
details.push("plugin installed but not enabled")
|
|
214
|
-
} else {
|
|
215
|
-
details.push("installed and enabled")
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (!options.keep) {
|
|
219
|
-
run("codex", ["plugin", "remove", `${pluginName}@${marketplaceName}`])
|
|
220
|
-
run("codex", ["plugin", "marketplace", "remove", marketplaceName])
|
|
221
|
-
}
|
|
222
|
-
record(harness, status, details)
|
|
223
|
-
} else if (harness === "agy") {
|
|
224
|
-
const details = []
|
|
225
|
-
const validate = run("agy", ["plugin", "validate", pluginRoot])
|
|
226
|
-
if (validate.missing) {
|
|
227
|
-
record(harness, "skip", ["agy CLI not found"])
|
|
228
|
-
continue
|
|
229
|
-
}
|
|
230
|
-
if (!validate.ok) {
|
|
231
|
-
record(harness, "fail", [`validate failed: ${firstLine(validate.stderr || validate.stdout)}`])
|
|
232
|
-
continue
|
|
233
|
-
}
|
|
234
|
-
const install = run("agy", ["plugin", "install", pluginRoot])
|
|
235
|
-
if (!install.ok) {
|
|
236
|
-
record(harness, "fail", [`install failed: ${firstLine(install.stderr || install.stdout)}`])
|
|
237
|
-
continue
|
|
238
|
-
}
|
|
239
|
-
const list = run("agy", ["plugin", "list"])
|
|
240
|
-
if (!list.ok || !list.stdout.includes(pluginName)) {
|
|
241
|
-
record(harness, "fail", ["plugin missing from agy plugin list"])
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
244
|
-
details.push("validated, installed, and listed")
|
|
245
|
-
if (!options.keep) run("agy", ["plugin", "uninstall", pluginName])
|
|
246
|
-
record(harness, "pass", details)
|
|
247
|
-
} else if (harness === "opencode") {
|
|
248
|
-
if (runtime === "npm" && !existsSync(adapterPath)) {
|
|
249
|
-
record(harness, "fail", [
|
|
250
|
-
`npm runtime requires a built adapter; missing ${adapterPath}`,
|
|
251
|
-
"run `npm run build` in the plugin package first",
|
|
252
|
-
])
|
|
253
|
-
continue
|
|
254
|
-
}
|
|
255
|
-
const isolation = await mkdtemp(join(tmpdir(), "ha-opencode-"))
|
|
256
|
-
const configDirectory = join(isolation, "opencode")
|
|
257
|
-
const skillsDirectory = join(configDirectory, "skills")
|
|
258
|
-
await cp(join(pluginRoot, "skills"), skillsDirectory, { recursive: true })
|
|
259
|
-
const config = { $schema: "https://opencode.ai/config.json" }
|
|
260
|
-
if (runtime === "npm") {
|
|
261
|
-
config.plugin = [`file://${adapterPath}`]
|
|
262
|
-
}
|
|
263
|
-
await writeFile(join(configDirectory, "opencode.json"), `${JSON.stringify(config, null, 2)}\n`)
|
|
264
|
-
const environment = {
|
|
265
|
-
...process.env,
|
|
266
|
-
XDG_CONFIG_HOME: isolation,
|
|
267
|
-
HOME: isolation,
|
|
268
|
-
}
|
|
269
|
-
const skillCheck = run("opencode", ["debug", "skill"], { env: environment })
|
|
270
|
-
if (skillCheck.missing) {
|
|
271
|
-
await rm(isolation, { recursive: true, force: true })
|
|
272
|
-
record(harness, "skip", ["opencode CLI not found"])
|
|
273
|
-
continue
|
|
274
|
-
}
|
|
275
|
-
const details = []
|
|
276
|
-
let status = "pass"
|
|
277
|
-
const missing = skills.filter((skill) => !skillCheck.stdout.includes(`"name": "${skill}"`))
|
|
278
|
-
if (missing.length > 0) {
|
|
279
|
-
status = "fail"
|
|
280
|
-
details.push(`skills not discovered: ${missing.join(", ")}`)
|
|
281
|
-
} else {
|
|
282
|
-
details.push(`${skills.length} skills discovered`)
|
|
283
|
-
}
|
|
284
|
-
if (runtime === "npm" && status === "pass") {
|
|
285
|
-
const startup = run("opencode", ["debug", "startup"], { env: environment })
|
|
286
|
-
if (!startup.ok) {
|
|
287
|
-
status = "fail"
|
|
288
|
-
details.push(`startup failed: ${firstLine(startup.stderr || startup.stdout)}`)
|
|
289
|
-
} else {
|
|
290
|
-
details.push("plugin loaded at startup")
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
await rm(isolation, { recursive: true, force: true })
|
|
294
|
-
record(harness, status, details)
|
|
295
|
-
} else if (harness === "dsh") {
|
|
296
|
-
if (runtime !== "npm") {
|
|
297
|
-
record(harness, "skip", ["skills runtime has no Cordis bundle; install skills via filesystem roots instead"])
|
|
298
|
-
continue
|
|
299
|
-
}
|
|
300
|
-
if (!existsSync(adapterPath)) {
|
|
301
|
-
record(harness, "fail", [
|
|
302
|
-
`npm runtime requires built adapters; missing ${join(pluginRoot, "dist/deepseek.js")}`,
|
|
303
|
-
"run `npm run build` in the plugin package first",
|
|
304
|
-
])
|
|
305
|
-
continue
|
|
306
|
-
}
|
|
307
|
-
const dshHome = await mkdtemp(join(tmpdir(), "ha-dsh-"))
|
|
308
|
-
const environment = { ...process.env, DSH_HOME: dshHome }
|
|
309
|
-
let dshCommand = "dsh"
|
|
310
|
-
let dshPrefix = []
|
|
311
|
-
if (spawnSync("dsh", ["--version"], { encoding: "utf8" }).error?.code === "ENOENT") {
|
|
312
|
-
dshCommand = "npx"
|
|
313
|
-
dshPrefix = ["-y", "@deepseek-ai/dsh"]
|
|
314
|
-
}
|
|
315
|
-
const add = run(dshCommand, [...dshPrefix, "plugin", "--profile", "install-check", "add", pluginRoot], { env: environment })
|
|
316
|
-
if (add.missing) {
|
|
317
|
-
await rm(dshHome, { recursive: true, force: true })
|
|
318
|
-
record(harness, "skip", ["dsh CLI not found and npx unavailable"])
|
|
319
|
-
continue
|
|
320
|
-
}
|
|
321
|
-
if (!add.ok) {
|
|
322
|
-
await rm(dshHome, { recursive: true, force: true })
|
|
323
|
-
record(harness, "fail", [`profile add failed: ${firstLine(add.stderr || add.stdout)}`])
|
|
324
|
-
continue
|
|
325
|
-
}
|
|
326
|
-
const dump = run(dshCommand, [...dshPrefix, "--profile", "install-check", "--dump-config"], { env: environment })
|
|
327
|
-
const details = []
|
|
328
|
-
let status = "pass"
|
|
329
|
-
if (!dump.ok || !dump.stdout.includes(`- id: ${pluginName}`)) {
|
|
330
|
-
status = "fail"
|
|
331
|
-
details.push("cordis insert entry missing from composed profile")
|
|
332
|
-
} else {
|
|
333
|
-
details.push("cordis bundle composed into isolated profile")
|
|
334
|
-
}
|
|
335
|
-
await rm(dshHome, { recursive: true, force: true })
|
|
336
|
-
record(harness, status, details)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const failed = results.filter((entry) => entry.status === "fail")
|
|
341
|
-
const skipped = results.filter((entry) => entry.status === "skip")
|
|
342
|
-
if (options.json) {
|
|
343
|
-
console.log(JSON.stringify({
|
|
344
|
-
project: root,
|
|
345
|
-
plugin: pluginName,
|
|
346
|
-
runtime,
|
|
347
|
-
results,
|
|
348
|
-
summary: {
|
|
349
|
-
pass: results.length - failed.length - skipped.length,
|
|
350
|
-
fail: failed.length,
|
|
351
|
-
skip: skipped.length,
|
|
352
|
-
},
|
|
353
|
-
}, null, 2))
|
|
354
|
-
} else {
|
|
355
|
-
console.log(
|
|
356
|
-
failed.length === 0
|
|
357
|
-
? `Install check passed for ${pluginName} (${results.length - skipped.length} verified, ${skipped.length} skipped)`
|
|
358
|
-
: `Install check failed for ${pluginName}: ${failed.length} of ${results.length} harnesses failed`,
|
|
359
|
-
)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return failed.length > 0 ? 1 : 0
|
|
363
|
-
}
|