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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +14 -0
  2. package/.codex-plugin/plugin.json +13 -0
  3. package/LICENSE +21 -0
  4. package/README.md +140 -0
  5. package/bin/harness-alchemist.mjs +43 -0
  6. package/cordis.patch.yml +3 -0
  7. package/dist/deepseek.d.ts +4 -0
  8. package/dist/deepseek.d.ts.map +1 -0
  9. package/dist/deepseek.js +5 -0
  10. package/dist/deepseek.js.map +1 -0
  11. package/dist/opencode.d.ts +3 -0
  12. package/dist/opencode.d.ts.map +1 -0
  13. package/dist/opencode.js +5 -0
  14. package/dist/opencode.js.map +1 -0
  15. package/lib/create.mjs +319 -0
  16. package/lib/validate.mjs +433 -0
  17. package/package.json +86 -0
  18. package/plugin.json +5 -0
  19. package/skills/harness-alchemist/SKILL.md +58 -0
  20. package/skills/harness-alchemist/references/antigravity.md +37 -0
  21. package/skills/harness-alchemist/references/claude-code.md +51 -0
  22. package/skills/harness-alchemist/references/codex.md +37 -0
  23. package/skills/harness-alchemist/references/compatibility.md +53 -0
  24. package/skills/harness-alchemist/references/deepseek-harness.md +56 -0
  25. package/skills/harness-alchemist/references/opencode.md +51 -0
  26. package/skills/harness-alchemist/references/publishing.md +66 -0
  27. package/templates/README.md +9 -0
  28. package/templates/v0.1.0/licenses/Apache-2.0.txt +201 -0
  29. package/templates/v0.1.0/template.json +13 -0
  30. package/templates/v0.1.0/universal-typescript/.agents/plugins/marketplace.json.tpl +20 -0
  31. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/SKILL.md.tpl +33 -0
  32. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/references/compatibility.md.tpl +23 -0
  33. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/check-package.mjs.tpl +69 -0
  34. package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/scripts/sync-metadata.mjs.tpl +124 -0
  35. package/templates/v0.1.0/universal-typescript/.claude-plugin/marketplace.json.tpl +19 -0
  36. package/templates/v0.1.0/universal-typescript/.claude-plugin/plugin.json.tpl +14 -0
  37. package/templates/v0.1.0/universal-typescript/.codex-plugin/plugin.json.tpl +13 -0
  38. package/templates/v0.1.0/universal-typescript/.github/workflows/npm-publish.yml.tpl +43 -0
  39. package/templates/v0.1.0/universal-typescript/.gitignore.tpl +9 -0
  40. package/templates/v0.1.0/universal-typescript/AGENTS.md.tpl +16 -0
  41. package/templates/v0.1.0/universal-typescript/README.md.tpl +131 -0
  42. package/templates/v0.1.0/universal-typescript/cordis.patch.yml.tpl +3 -0
  43. package/templates/v0.1.0/universal-typescript/package.json.tpl +70 -0
  44. package/templates/v0.1.0/universal-typescript/plugin.json.tpl +5 -0
  45. package/templates/v0.1.0/universal-typescript/skills/shared-skill/SKILL.md.tpl +18 -0
  46. package/templates/v0.1.0/universal-typescript/src/deepseek.ts.tpl +7 -0
  47. package/templates/v0.1.0/universal-typescript/src/opencode.ts.tpl +7 -0
  48. package/templates/v0.1.0/universal-typescript/tests/runtimes.test.mjs.tpl +15 -0
  49. package/templates/v0.1.0/universal-typescript/tsconfig.json.tpl +17 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "harness-alchemist",
4
+ "displayName": "Harness Alchemist",
5
+ "version": "0.1.0",
6
+ "description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
7
+ "author": {
8
+ "name": "Haochuan Zhang"
9
+ },
10
+ "homepage": "https://github.com/lunarmoon26/harness-alchemist",
11
+ "repository": "https://github.com/lunarmoon26/harness-alchemist",
12
+ "license": "MIT",
13
+ "skills": "./skills/"
14
+ }
@@ -0,0 +1,13 @@
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
+ "author": {
6
+ "name": "Haochuan Zhang",
7
+ "url": "https://github.com/lunarmoon26/harness-alchemist"
8
+ },
9
+ "homepage": "https://github.com/lunarmoon26/harness-alchemist",
10
+ "repository": "https://github.com/lunarmoon26/harness-alchemist",
11
+ "license": "MIT",
12
+ "skills": "./skills/"
13
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Haochuan Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Harness Alchemist
2
+
3
+ Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex/ChatGPT, OpenCode, Google Antigravity, and DeepSeek Harness/Cordis.
4
+
5
+ Harness Alchemist is a zero-runtime-dependency CLI for Node.js and Bun. Its own repository follows the same universal plugin layout that it generates. The first frozen scaffold is `templates/v0.1.0`.
6
+
7
+ ## Quick Start
8
+
9
+ Run with Node.js:
10
+
11
+ ```bash
12
+ npx harness-alchemist create ./my-plugin \
13
+ --description "What the plugin does" \
14
+ --author "Example Team" \
15
+ --repository example/my-plugin
16
+ ```
17
+
18
+ Or with Bun:
19
+
20
+ ```bash
21
+ bunx harness-alchemist create ./my-plugin \
22
+ --description "What the plugin does" \
23
+ --author "Example Team" \
24
+ --repository example/my-plugin
25
+ ```
26
+
27
+ The destination must be missing or empty. Harness Alchemist renders into a staging directory, validates the result, and moves it into place without replacing existing content.
28
+
29
+ ## Commands
30
+
31
+ ```text
32
+ harness-alchemist create <directory> [options]
33
+ harness-alchemist validate [directory] [--external] [--json]
34
+ harness-alchemist templates
35
+ harness-alchemist version
36
+ ```
37
+
38
+ `init` and `new` are aliases for `create`. Run `harness-alchemist create --help` for metadata and license options.
39
+
40
+ New projects use the unreleased canonical `v0.1.0` template. Unknown versions
41
+ fail instead of silently selecting a different layout.
42
+
43
+ ## Generated Layout
44
+
45
+ ```text
46
+ .agents/
47
+ plugins/ Codex repository marketplace
48
+ skills/develop-<name>/ Project-local maintenance skill and scripts
49
+ .claude-plugin/ Claude plugin and marketplace manifests
50
+ .codex-plugin/ Codex/ChatGPT plugin manifest
51
+ skills/<name>/SKILL.md Shared installable Agent Skill
52
+ src/opencode.ts OpenCode npm plugin entrypoint
53
+ src/deepseek.ts Cordis plugin entrypoint
54
+ cordis.patch.yml DeepSeek Harness bundle layer
55
+ plugin.json Antigravity plugin manifest
56
+ package.json Canonical package metadata
57
+ ```
58
+
59
+ Generated repositories start at version `0.1.0` with inert OpenCode and Cordis entrypoints. Add only the runtime hooks or services the plugin actually needs.
60
+
61
+ The recursive part is intentional: Harness Alchemist itself uses this layout, and every generated repository receives `.agents/skills/develop-<name>/` with its own sync, validation, package-payload, and compatibility workflow.
62
+
63
+ ## Guidance Tiers
64
+
65
+ The repository-maintainer skill, the published CLI skill, and a generated
66
+ project's local maintenance skill have different audiences and scopes. See
67
+ [docs/skill-tiers.md](docs/skill-tiers.md) before moving guidance between them.
68
+
69
+ ## Canonical Template
70
+
71
+ `templates/v0.1.0/universal-typescript/` is the unreleased canonical scaffold. It includes the generated project's local maintenance skill, scoped to that project's ownership boundary. `lib/create.mjs` supplies the version-specific validator and license during the atomic render.
72
+
73
+ Future incompatible scaffold changes should create a new `templates/<version>/`
74
+ snapshot instead of mutating `v0.1.0` after release.
75
+
76
+ ## Local Development
77
+
78
+ Requires Node.js 22.20 or newer for the full build and package checks. The scaffolding and validation CLI also runs directly with Bun 1.2 or newer.
79
+
80
+ ```bash
81
+ npm install
82
+ npm run verify
83
+ node ./bin/harness-alchemist.mjs templates
84
+ bun ./bin/harness-alchemist.mjs templates
85
+ ```
86
+
87
+ After changing package metadata:
88
+
89
+ ```bash
90
+ npm run sync
91
+ npm run verify
92
+ ```
93
+
94
+ Inspect the publish payload before release:
95
+
96
+ ```bash
97
+ npm pack --dry-run
98
+ ```
99
+
100
+ ## Package Surfaces
101
+
102
+ - npm binary: `harness-alchemist`
103
+ - Shared Agent Skill: `skills/harness-alchemist/SKILL.md`
104
+ - OpenCode package-root export: `harness-alchemist`
105
+ - Cordis export: `harness-alchemist/deepseek`
106
+ - DeepSeek bundle patch: `harness-alchemist/cordis.patch.yml`
107
+
108
+ The runtime entrypoints are intentionally inert in v0.1.0. The CLI and Agent Skill are the implemented product surfaces.
109
+
110
+ ## Harness Installation
111
+
112
+ Claude Code:
113
+
114
+ ```bash
115
+ claude plugin marketplace add lunarmoon26/harness-alchemist
116
+ claude plugin install harness-alchemist@harness-alchemist-plugins
117
+ ```
118
+
119
+ Codex and ChatGPT:
120
+
121
+ ```bash
122
+ codex plugin marketplace add lunarmoon26/harness-alchemist
123
+ ```
124
+
125
+ OpenCode shared skill:
126
+
127
+ ```bash
128
+ npx skills add lunarmoon26/harness-alchemist --agent opencode
129
+ ```
130
+
131
+ DeepSeek Harness after npm publication:
132
+
133
+ ```bash
134
+ dsh plugin --profile demo add harness-alchemist
135
+ dsh --profile demo --dump-config
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises"
4
+
5
+ import { listTemplates, runCreate } from "../lib/create.mjs"
6
+ import { runValidate } from "../lib/validate.mjs"
7
+
8
+ const packageJson = JSON.parse(
9
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
10
+ )
11
+
12
+ function usage() {
13
+ return `Harness Alchemist ${packageJson.version}
14
+
15
+ Usage: harness-alchemist <command> [options]
16
+
17
+ Commands:
18
+ create <directory> Create a universal coding-agent plugin repository.
19
+ validate [directory] Validate a generated repository.
20
+ templates List bundled canonical templates.
21
+ version Print the CLI version.
22
+ help Show this help.
23
+
24
+ Aliases: init and new are aliases for create.`
25
+ }
26
+
27
+ const [command, ...args] = process.argv.slice(2)
28
+
29
+ if (!command || command === "help" || command === "--help" || command === "-h") {
30
+ console.log(usage())
31
+ } else if (command === "version" || command === "--version" || command === "-v") {
32
+ console.log(packageJson.version)
33
+ } else if (command === "templates") {
34
+ console.log(listTemplates())
35
+ } else if (["create", "init", "new"].includes(command)) {
36
+ process.exitCode = await runCreate(args)
37
+ } else if (command === "validate") {
38
+ process.exitCode = await runValidate(args.length > 0 ? args : [process.cwd()])
39
+ } else {
40
+ console.error(`Unknown command: ${command}\n`)
41
+ console.error(usage())
42
+ process.exitCode = 2
43
+ }
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: harness-alchemist
3
+ name: 'harness-alchemist/deepseek'
@@ -0,0 +1,4 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ export declare const name = "harness-alchemist";
3
+ export declare function apply(_context: Context): void;
4
+ //# sourceMappingURL=deepseek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC,wBAAgB,KAAK,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAE7C"}
@@ -0,0 +1,5 @@
1
+ export const name = "harness-alchemist";
2
+ export function apply(_context) {
3
+ // Register Cordis services, tools, or lifecycle effects here.
4
+ }
5
+ //# sourceMappingURL=deepseek.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,UAAU,KAAK,CAAC,QAAiB;IACrC,8DAA8D;AAChE,CAAC"}
@@ -0,0 +1,3 @@
1
+ declare const plugin: () => Promise<{}>;
2
+ export default plugin;
3
+ //# sourceMappingURL=opencode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM,mBAEO,CAAA;AAEnB,eAAe,MAAM,CAAA"}
@@ -0,0 +1,5 @@
1
+ const plugin = (async () => {
2
+ return {};
3
+ });
4
+ export default plugin;
5
+ //# sourceMappingURL=opencode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;IACzB,OAAO,EAAE,CAAA;AACX,CAAC,CAAkB,CAAA;AAEnB,eAAe,MAAM,CAAA"}
package/lib/create.mjs ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { randomUUID } from "node:crypto"
4
+ import { existsSync } from "node:fs"
5
+ import { 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 state = await destinationState(options.output)
241
+ if (state === "not-directory" || state === "non-empty") {
242
+ console.error(`Destination must be missing or empty: ${options.output}`)
243
+ return 1
244
+ }
245
+
246
+ const tokens = {
247
+ NAME: options.name,
248
+ NAME_JSON: JSON.stringify(options.name),
249
+ DISPLAY_NAME: options.displayName,
250
+ DISPLAY_NAME_JSON: JSON.stringify(options.displayName),
251
+ DESCRIPTION: options.description,
252
+ DESCRIPTION_JSON: JSON.stringify(options.description),
253
+ SHARED_SKILL_DESCRIPTION_JSON: JSON.stringify(
254
+ `${options.description} Use when the user requests ${options.displayName} workflows or explicitly asks to use the ${options.name} plugin.`,
255
+ ),
256
+ DEVELOPMENT_SKILL_DESCRIPTION_JSON: JSON.stringify(
257
+ `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.`,
258
+ ),
259
+ PACKAGE_NAME: options.packageName,
260
+ PACKAGE_NAME_JSON: JSON.stringify(options.packageName),
261
+ AUTHOR: options.author,
262
+ AUTHOR_JSON: JSON.stringify(options.author),
263
+ REPOSITORY_SOURCE: options.repository.installSource,
264
+ REPOSITORY_SOURCE_JSON: JSON.stringify(options.repository.installSource),
265
+ REPOSITORY_URL: options.repository.url,
266
+ REPOSITORY_URL_JSON: JSON.stringify(options.repository.url),
267
+ MARKETPLACE: options.marketplace,
268
+ MARKETPLACE_JSON: JSON.stringify(options.marketplace),
269
+ LICENSE: options.license,
270
+ LICENSE_JSON: JSON.stringify(options.license),
271
+ YEAR: String(new Date().getFullYear()),
272
+ }
273
+
274
+ if (options.dryRun) {
275
+ console.log(JSON.stringify({
276
+ output: options.output,
277
+ template: options.templateVersion,
278
+ plugin: options.name,
279
+ package: options.packageName,
280
+ files: await listTemplateFiles(templateRoot, options),
281
+ }, null, 2))
282
+ return 0
283
+ }
284
+
285
+ await mkdir(dirname(options.output), { recursive: true })
286
+ const staging = join(dirname(options.output), `.${basename(options.output)}.scaffold-${randomUUID()}`)
287
+
288
+ try {
289
+ await mkdir(staging)
290
+ await renderTemplates(templateRoot, staging, options, tokens)
291
+ const localScripts = join(staging, ".agents/skills", `develop-${options.name}`, "scripts")
292
+ await mkdir(localScripts, { recursive: true })
293
+ await cp(join(projectRoot, "lib/validate.mjs"), join(localScripts, "validate.mjs"))
294
+ await writeFile(join(staging, "LICENSE"), await licenseText(canonicalRoot, options))
295
+
296
+ const validation = spawnSync(process.execPath, [join(projectRoot, "lib/validate.mjs"), staging], {
297
+ encoding: "utf8",
298
+ })
299
+ if (validation.status !== 0) {
300
+ throw new Error(`Generated project failed validation:\n${validation.stderr || validation.stdout}`)
301
+ }
302
+
303
+ if (state === "empty") await rmdir(options.output)
304
+ await rename(staging, options.output)
305
+
306
+ console.log(`Created universal plugin scaffold at ${options.output}`)
307
+ console.log(`Next: cd ${JSON.stringify(options.output)} && npm install && npm run verify`)
308
+ console.log(`Bun: cd ${JSON.stringify(options.output)} && bun install && bun run verify`)
309
+ return 0
310
+ } catch (error) {
311
+ await rm(staging, { recursive: true, force: true })
312
+ console.error(error.message)
313
+ return 1
314
+ }
315
+ }
316
+
317
+ if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
318
+ process.exitCode = await runCreate(process.argv.slice(2))
319
+ }