create-restforge-skills 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/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # RESTForge — Agent Skills (portable)
2
+
3
+ Vendor-neutral [Agent Skills](https://agentskills.io) for RESTForge. The same
4
+ end-to-end RESTForge workflow knowledge, packaged in the open `SKILL.md` format
5
+ so it works across any skills-compatible agent — **Claude Code, Cursor, Gemini
6
+ CLI, and OpenAI Codex** — not only Claude Code.
7
+
8
+ ## What this package is (and is not)
9
+
10
+ This package is the **portable** distribution of RESTForge agent knowledge.
11
+
12
+ - It contains pure Agent Skills: a `skills/restforge/SKILL.md` plus `references/`.
13
+ - It has **no** client-specific packaging — no Claude Code `plugin.json`,
14
+ `marketplace.json`, or `.mcp.json`.
15
+
16
+ It is a **complement to**, not a replacement for, [`restforge-plugins`](../packages/restforge-plugins).
17
+
18
+ | | `restforge-plugins` | `restforge-skills` (this package) |
19
+ |---|---|---|
20
+ | Target | Claude Code only | Claude Code, Cursor, Gemini CLI, Codex |
21
+ | Distribution | Claude Code plugin + marketplace | Copy/symlink the skill folder into each client's skills directory |
22
+ | MCP registration | Bundled (`.mcp.json`) | Done per client by the user (see below) |
23
+ | Knowledge source | `skills/restforge-skills/` | Same knowledge, portable form |
24
+
25
+ Both packages wrap the **same** workflow knowledge and depend on the **same**
26
+ execution engine: the `@restforgejs/mcp-server` MCP server. A skill describes
27
+ *how* to drive RESTForge; the MCP server is *what* actually executes the
28
+ operations. The skill is useless without the MCP server registered in the client.
29
+
30
+ ## Directory layout
31
+
32
+ ```
33
+ restforge-skills/
34
+ ├── README.md ← this file
35
+ ├── package.json ← npm package: create-restforge-skills
36
+ ├── bump-and-publish.bat ← version bump + npm publish
37
+ ├── cli/
38
+ │ └── index.js ← installer that copies the skill into each client
39
+ ├── docs/ ← internal docs, NOT published (excluded from npm files)
40
+ └── skills/
41
+ └── restforge/
42
+ ├── SKILL.md ← the portable skill (name + description + workflow)
43
+ └── references/ ← progressive-disclosure reference material
44
+ ```
45
+
46
+ ## Install
47
+
48
+ One command. It detects your installed clients (Claude Code, Cursor), copies the
49
+ skill into each, and registers the RESTForge MCP server — because the skill is
50
+ inert without it:
51
+
52
+ ```bash
53
+ npx create-restforge-skills
54
+ ```
55
+
56
+ The MCP server is registered via `npx -y @restforgejs/mcp-server`, so there is no
57
+ separate global install. The MCP config is **merged, not overwritten**, and the
58
+ existing file is backed up to `<config>.bak` first.
59
+
60
+ ### Where to run it (working directory)
61
+
62
+ This is an **installer**, not a project scaffolder. Unlike `create-restforge-app`,
63
+ it takes **no positional folder argument** (`myapp` / `.`). The install location
64
+ is chosen by `--scope`, not by where you stand or by a path you pass.
65
+
66
+ - **Default (`--scope=user`) — run it from anywhere.** The current directory is
67
+ ignored. The skill goes to your home (`~/.claude/skills/`, `~/.cursor/skills/`)
68
+ and is available across **all** your projects. This is the normal usage: a skill
69
+ is editor/agent tooling, not part of any one app's source.
70
+
71
+ - **`--scope=project` — run it inside the project folder.** Here the current
72
+ directory matters: the skill and MCP config are written relative to it
73
+ (`./.claude/...`, `./.cursor/...`), so you must `cd` into the target project
74
+ first. Use this to commit the skill with a specific repo so the team gets it on
75
+ clone.
76
+
77
+ ```bash
78
+ # normal — anywhere, global for every project
79
+ npx create-restforge-skills
80
+
81
+ # attach to one project — cd in first
82
+ cd path/to/my-project
83
+ npx create-restforge-skills --scope=project
84
+ ```
85
+
86
+ Note: a bare path such as `npx create-restforge-skills .` is **not** recognized as
87
+ "install here" — it is treated as an unknown argument. Use `--scope=project`.
88
+
89
+ ### Options
90
+
91
+ ```bash
92
+ npx create-restforge-skills --client=cursor # one client only
93
+ npx create-restforge-skills --scope=project # into ./.claude or ./.cursor (commit for the team)
94
+ npx create-restforge-skills --mcp-command=global # use the installed restforge-mcp binary instead of npx
95
+ npx create-restforge-skills --no-mcp # skill only; you manage MCP yourself
96
+ npx create-restforge-skills --dry-run # preview, write nothing
97
+ npx create-restforge-skills --force # overwrite an existing skill (update)
98
+ ```
99
+
100
+ After install, set a valid RESTForge `LICENSE` for `codegen_*`/`runtime_*`
101
+ operations (Designer/frontend tools need no license). On Claude Code, if the
102
+ skills directory was just created, restart the client so it watches the new
103
+ directory.
104
+
105
+ ### Where it installs
106
+
107
+ | Client | Skill (user scope) | MCP config |
108
+ |---|---|---|
109
+ | Claude Code | `~/.claude/skills/restforge/` | `~/.claude.json` |
110
+ | Cursor | `~/.cursor/skills/restforge/` | `~/.cursor/mcp.json` |
111
+
112
+ > For Claude Code, a turnkey alternative is the `restforge-plugins` package,
113
+ > which bundles the skill **and** MCP registration in one `/plugin install`.
114
+
115
+ ### Manual install (no CLI)
116
+
117
+ The skill folder is self-contained — copy `skills/restforge/` (including
118
+ `references/`) into the client's skills directory and add
119
+ `{ "mcpServers": { "restforge": { "command": "npx", "args": ["-y", "@restforgejs/mcp-server"] } } }`
120
+ to the client's MCP config.
121
+
122
+ ### Gemini CLI / OpenAI Codex
123
+
124
+ Planned. Both consume the same `skills/restforge/` folder; only the skills
125
+ directory path and MCP config file differ per client.
126
+
127
+ ## Source of truth
128
+
129
+ The knowledge in this skill mirrors the RESTForge handbook
130
+ ([`restforge-handbook`](../restforge-handbook)) and the MCP tool surface
131
+ ([`mcp-server`](../packages/mcp-server)). When RESTForge behavior changes, update the
132
+ handbook first, then this skill — the skill is downstream documentation of real
133
+ tool behavior, never an independent source.
134
+
135
+ ## License
136
+
137
+ MIT.
package/cli/index.js ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * create-restforge-skills
6
+ *
7
+ * Installs the bundled RESTForge Agent Skill (skills/restforge/) into the
8
+ * skills directory of each detected client (Claude Code, Cursor, ...) AND
9
+ * registers the RESTForge MCP server, because the skill is inert without it.
10
+ *
11
+ * The skill is the payload; this CLI is the delivery mechanism. It copies the
12
+ * whole skill folder — SKILL.md AND references/ — so reference links keep
13
+ * working, and merges (never overwrites) the MCP server into each client config.
14
+ *
15
+ * It does NOT set a license; that secret stays a manual step.
16
+ *
17
+ * Zero runtime dependencies: Node built-ins only, for fast `npx` and broad
18
+ * platform support (Windows included).
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+ const { buildEntry, registerMcp } = require('./mcp');
25
+
26
+ const SKILL_NAME = 'restforge';
27
+ // Bundled skill source: <package>/skills/restforge, resolved relative to this file.
28
+ const SOURCE_DIR = path.join(__dirname, '..', 'skills', SKILL_NAME);
29
+
30
+ /**
31
+ * Client registry. Each client knows where its skills live (user vs project
32
+ * scope) and where its MCP config file is. Paths are computed lazily so we never
33
+ * touch the filesystem for a client the user did not select.
34
+ */
35
+ const CLIENTS = {
36
+ claude: {
37
+ label: 'Claude Code',
38
+ homeMarker: () => path.join(os.homedir(), '.claude'),
39
+ skillsDir: (scope, cwd) =>
40
+ scope === 'project'
41
+ ? path.join(cwd, '.claude', 'skills')
42
+ : path.join(os.homedir(), '.claude', 'skills'),
43
+ mcpConfig: (scope, cwd) =>
44
+ scope === 'project'
45
+ ? path.join(cwd, '.mcp.json')
46
+ : path.join(os.homedir(), '.claude.json'),
47
+ },
48
+ cursor: {
49
+ label: 'Cursor',
50
+ homeMarker: () => path.join(os.homedir(), '.cursor'),
51
+ skillsDir: (scope, cwd) =>
52
+ scope === 'project'
53
+ ? path.join(cwd, '.cursor', 'skills')
54
+ : path.join(os.homedir(), '.cursor', 'skills'),
55
+ mcpConfig: (scope, cwd) =>
56
+ scope === 'project'
57
+ ? path.join(cwd, '.cursor', 'mcp.json')
58
+ : path.join(os.homedir(), '.cursor', 'mcp.json'),
59
+ },
60
+ };
61
+
62
+ function parseArgs(argv) {
63
+ const opts = {
64
+ clients: null, // null = auto-detect
65
+ scope: 'user', // 'user' | 'project'
66
+ dryRun: false,
67
+ force: false,
68
+ mcp: true, // register MCP by default — the skill is useless without it
69
+ mcpCommand: 'npx', // 'npx' (no global install) | 'global' (restforge-mcp)
70
+ yes: false,
71
+ help: false,
72
+ };
73
+ for (const arg of argv) {
74
+ if (arg === '--help' || arg === '-h') opts.help = true;
75
+ else if (arg === '--yes' || arg === '-y') opts.yes = true;
76
+ else if (arg === '--dry-run') opts.dryRun = true;
77
+ else if (arg === '--force') opts.force = true;
78
+ else if (arg === '--no-mcp') opts.mcp = false;
79
+ else if (arg === '--with-mcp') opts.mcp = true; // accepted; now the default
80
+ else if (arg.startsWith('--mcp-command=')) {
81
+ opts.mcpCommand = arg.slice('--mcp-command='.length).trim().toLowerCase();
82
+ } else if (arg.startsWith('--client=')) {
83
+ opts.clients = arg
84
+ .slice('--client='.length)
85
+ .split(',')
86
+ .map((c) => c.trim().toLowerCase())
87
+ .filter(Boolean);
88
+ } else if (arg.startsWith('--scope=')) {
89
+ opts.scope = arg.slice('--scope='.length).trim().toLowerCase();
90
+ } else {
91
+ console.error(`Unknown argument: ${arg}`);
92
+ opts.help = true;
93
+ }
94
+ }
95
+ return opts;
96
+ }
97
+
98
+ const HELP = `
99
+ create-restforge-skills — install the RESTForge Agent Skill into your clients.
100
+
101
+ Usage:
102
+ npx create-restforge-skills [options]
103
+
104
+ By default this copies the skill AND registers the RESTForge MCP server (via npx),
105
+ so it works out of the box. The MCP config is merged, not overwritten, and the
106
+ existing file is backed up to <config>.bak first.
107
+
108
+ Options:
109
+ --client=<list> Comma-separated clients: claude,cursor. Default: auto-detect.
110
+ --scope=<scope> user (default) installs globally for you; project installs
111
+ into ./.claude or ./.cursor in the current directory.
112
+ --no-mcp Skip MCP registration (only do this if you know why —
113
+ the skill cannot call anything without the MCP server).
114
+ --mcp-command=<c> npx (default, no global install needed) or
115
+ global (uses the installed restforge-mcp binary).
116
+ --force Overwrite an existing restforge skill folder.
117
+ --dry-run Show what would happen without writing anything.
118
+ --yes, -y Run non-interactively, accepting defaults.
119
+ --help, -h Show this help.
120
+ `;
121
+
122
+ /** Decide which clients to target. */
123
+ function resolveClients(opts) {
124
+ if (opts.clients && opts.clients.length) {
125
+ const unknown = opts.clients.filter((c) => !CLIENTS[c]);
126
+ if (unknown.length) {
127
+ console.error(`Unknown client(s): ${unknown.join(', ')}`);
128
+ console.error(`Supported: ${Object.keys(CLIENTS).join(', ')}`);
129
+ process.exit(1);
130
+ }
131
+ return opts.clients;
132
+ }
133
+ // Auto-detect: a client counts as present if its home marker dir exists.
134
+ return Object.keys(CLIENTS).filter((key) => {
135
+ try {
136
+ return fs.statSync(CLIENTS[key].homeMarker()).isDirectory();
137
+ } catch {
138
+ return false;
139
+ }
140
+ });
141
+ }
142
+
143
+ /** Build the concrete install targets for the chosen clients and scope. */
144
+ function buildTargets(clientKeys, opts) {
145
+ const cwd = process.cwd();
146
+ return clientKeys.map((key) => {
147
+ const client = CLIENTS[key];
148
+ return {
149
+ key,
150
+ label: client.label,
151
+ dest: path.join(client.skillsDir(opts.scope, cwd), SKILL_NAME),
152
+ mcpConfig: client.mcpConfig(opts.scope, cwd),
153
+ };
154
+ });
155
+ }
156
+
157
+ /** Copy the skill folder (SKILL.md + references/) into one target. */
158
+ function installSkill(target, opts) {
159
+ const exists = fs.existsSync(target.dest);
160
+ if (exists && !opts.force) {
161
+ return { status: 'skipped', reason: 'already exists (use --force to overwrite)' };
162
+ }
163
+ if (opts.dryRun) {
164
+ return { status: exists ? 'would-overwrite' : 'would-install' };
165
+ }
166
+ fs.mkdirSync(path.dirname(target.dest), { recursive: true });
167
+ if (exists) fs.rmSync(target.dest, { recursive: true, force: true });
168
+ fs.cpSync(SOURCE_DIR, target.dest, { recursive: true });
169
+ return { status: exists ? 'overwritten' : 'installed' };
170
+ }
171
+
172
+ const pad = (s) => String(s).padEnd(12);
173
+
174
+ function main() {
175
+ const opts = parseArgs(process.argv.slice(2));
176
+ if (opts.help) {
177
+ process.stdout.write(HELP);
178
+ return;
179
+ }
180
+
181
+ if (!['user', 'project'].includes(opts.scope)) {
182
+ console.error(`Invalid --scope: ${opts.scope} (expected "user" or "project")`);
183
+ process.exit(1);
184
+ }
185
+ if (!['npx', 'global'].includes(opts.mcpCommand)) {
186
+ console.error(`Invalid --mcp-command: ${opts.mcpCommand} (expected "npx" or "global")`);
187
+ process.exit(1);
188
+ }
189
+ if (!fs.existsSync(path.join(SOURCE_DIR, 'SKILL.md'))) {
190
+ console.error(`Bundled skill not found at ${SOURCE_DIR}. Broken package?`);
191
+ process.exit(1);
192
+ }
193
+
194
+ const clientKeys = resolveClients(opts);
195
+ if (!clientKeys.length) {
196
+ console.error('No target client detected (looked for ~/.claude and ~/.cursor).');
197
+ console.error('Pass one explicitly, e.g. --client=claude,cursor');
198
+ process.exit(1);
199
+ }
200
+
201
+ const targets = buildTargets(clientKeys, opts);
202
+ const mcpEntry = buildEntry(opts.mcpCommand);
203
+
204
+ console.log(
205
+ `\nRESTForge skill — ${opts.dryRun ? 'dry run' : 'install'} (scope: ${opts.scope}` +
206
+ `, mcp: ${opts.mcp ? opts.mcpCommand : 'off'})\n`
207
+ );
208
+
209
+ for (const target of targets) {
210
+ const skill = installSkill(target, opts);
211
+ console.log(` ${pad(target.label)} ${target.dest}`);
212
+ console.log(` ${pad('')} → skill: ${skill.status}${skill.reason ? ` (${skill.reason})` : ''}`);
213
+
214
+ if (opts.mcp) {
215
+ const mcp = registerMcp(target.mcpConfig, mcpEntry, opts);
216
+ console.log(` ${pad('')} → mcp: ${mcp.status}${mcp.error ? ` — ${mcp.error}` : ''}`);
217
+ console.log(` ${pad('')} ${target.mcpConfig}`);
218
+ if (mcp.backup) console.log(` ${pad('')} backup: ${mcp.backup}`);
219
+ if (mcp.status === 'error') {
220
+ console.log(` ${pad('')} add manually: "mcpServers": { "restforge": ${JSON.stringify(mcpEntry)} }`);
221
+ }
222
+ }
223
+ }
224
+
225
+ console.log('\nNext step:');
226
+ console.log(' Set a valid RESTForge LICENSE for codegen_*/runtime_* operations.');
227
+ console.log(' (Designer/frontend tools work without a license.)');
228
+ if (clientKeys.includes('claude')) {
229
+ console.log(' Claude Code: if its skills dir was just created, restart the client so it');
230
+ console.log(' watches the new directory.');
231
+ }
232
+ console.log('');
233
+ }
234
+
235
+ main();
package/cli/mcp.js ADDED
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MCP registration for create-restforge-skills.
5
+ *
6
+ * The skill is inert without the RESTForge MCP server, so registration is the
7
+ * default behavior. This module merges (never overwrites) a `restforge` entry
8
+ * into a client's MCP config and backs the file up first.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Build the MCP server entry.
16
+ * - 'npx' (default): no separate global install — npx fetches on first run.
17
+ * - 'global': uses the globally-installed `restforge-mcp` binary (faster start).
18
+ */
19
+ function buildEntry(mcpCommand) {
20
+ if (mcpCommand === 'global') {
21
+ return { command: 'restforge-mcp' };
22
+ }
23
+ return { command: 'npx', args: ['-y', '@restforgejs/mcp-server'] };
24
+ }
25
+
26
+ function readConfig(file) {
27
+ if (!fs.existsSync(file)) return { existed: false, data: {} };
28
+ const raw = fs.readFileSync(file, 'utf8').trim();
29
+ if (!raw) return { existed: true, data: {} };
30
+ let data;
31
+ try {
32
+ data = JSON.parse(raw);
33
+ } catch (err) {
34
+ const e = new Error(`config is not valid JSON: ${err.message}`);
35
+ e.code = 'EBADJSON';
36
+ throw e;
37
+ }
38
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) {
39
+ const e = new Error('config root is not a JSON object');
40
+ e.code = 'EBADJSON';
41
+ throw e;
42
+ }
43
+ return { existed: true, data };
44
+ }
45
+
46
+ /**
47
+ * Merge the `restforge` MCP server into `file`.
48
+ * Returns { status, backup?, error? } where status is one of:
49
+ * registered | updated | already-registered | would-register | would-update | error
50
+ * Never overwrites unrelated keys; backs up an existing file before writing.
51
+ */
52
+ function registerMcp(file, entry, opts) {
53
+ let cfg;
54
+ try {
55
+ cfg = readConfig(file);
56
+ } catch (err) {
57
+ if (err.code === 'EBADJSON') {
58
+ // Refuse to touch a config we cannot safely parse.
59
+ return { status: 'error', error: err.message };
60
+ }
61
+ throw err;
62
+ }
63
+
64
+ const data = cfg.data;
65
+ const servers =
66
+ data.mcpServers && typeof data.mcpServers === 'object' && !Array.isArray(data.mcpServers)
67
+ ? data.mcpServers
68
+ : {};
69
+
70
+ const existing = servers.restforge;
71
+ if (existing && JSON.stringify(existing) === JSON.stringify(entry)) {
72
+ return { status: 'already-registered' };
73
+ }
74
+
75
+ const willUpdate = Boolean(existing);
76
+ if (opts.dryRun) {
77
+ return { status: willUpdate ? 'would-update' : 'would-register' };
78
+ }
79
+
80
+ let backup;
81
+ if (cfg.existed) {
82
+ backup = file + '.bak';
83
+ fs.copyFileSync(file, backup);
84
+ }
85
+
86
+ servers.restforge = entry;
87
+ data.mcpServers = servers;
88
+ fs.mkdirSync(path.dirname(file), { recursive: true });
89
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
90
+
91
+ return { status: willUpdate ? 'updated' : 'registered', backup };
92
+ }
93
+
94
+ module.exports = { buildEntry, registerMcp };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "create-restforge-skills",
3
+ "version": "0.1.0",
4
+ "description": "Install the RESTForge Agent Skill into Claude Code, Cursor, and other skills-compatible clients.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "create-restforge-skills": "cli/index.js"
8
+ },
9
+ "files": [
10
+ "cli",
11
+ "skills",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "restforge",
19
+ "agent-skills",
20
+ "skill",
21
+ "claude-code",
22
+ "cursor",
23
+ "mcp"
24
+ ],
25
+ "author": {
26
+ "name": "RESTForge",
27
+ "url": "https://restforge.dev"
28
+ },
29
+ "license": "MIT"
30
+ }