create-restforge-skills 1.0.0 → 1.0.2
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/cli/index.js +250 -250
- package/cli/mcp.js +103 -94
- package/package.json +2 -1
- package/skills/restforge/SKILL.md +230 -773
- package/skills/restforge/agents/openai.yaml +4 -4
- package/skills/restforge/references/auth.md +185 -145
- package/skills/restforge/references/backend-pipeline.md +388 -0
- package/skills/restforge/references/data-seeding.md +27 -0
- package/skills/restforge/references/dbschema-catalog.md +2 -2
- package/skills/restforge/references/field-validation.md +6 -4
- package/skills/restforge/references/frontend-pipeline.md +178 -0
- package/skills/restforge/references/rdf-advanced.md +1 -1
- package/skills/restforge/references/troubleshooting.md +144 -0
package/cli/index.js
CHANGED
|
@@ -1,250 +1,250 @@
|
|
|
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
|
-
* Uses a TOML parser for Codex configuration; works on Windows, macOS and Linux.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const os = require('os');
|
|
23
|
-
const { buildEntry, registerMcp } = require('./mcp');
|
|
24
|
-
const { registerCodexMcp, formatCodexMcp } = require('./codex');
|
|
25
|
-
const codexHome = () => process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
26
|
-
|
|
27
|
-
const SKILL_NAME = 'restforge';
|
|
28
|
-
// Bundled skill source: <package>/skills/restforge, resolved relative to this file.
|
|
29
|
-
const SOURCE_DIR = path.join(__dirname, '..', 'skills', SKILL_NAME);
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Client registry. Each client knows where its skills live (user vs project
|
|
33
|
-
* scope) and where its MCP config file is. Paths are computed lazily so we never
|
|
34
|
-
* touch the filesystem for a client the user did not select.
|
|
35
|
-
*/
|
|
36
|
-
const CLIENTS = {
|
|
37
|
-
codex: {
|
|
38
|
-
label: 'Codex',
|
|
39
|
-
homeMarker: codexHome,
|
|
40
|
-
skillsDir: (scope, cwd) => path.join(scope === 'project' ? cwd : os.homedir(), '.agents', 'skills'),
|
|
41
|
-
mcpConfig: (scope, cwd) => path.join(scope === 'project' ? path.join(cwd, '.codex') : codexHome(), 'config.toml'),
|
|
42
|
-
},
|
|
43
|
-
claude: {
|
|
44
|
-
label: 'Claude Code',
|
|
45
|
-
homeMarker: () => path.join(os.homedir(), '.claude'),
|
|
46
|
-
skillsDir: (scope, cwd) =>
|
|
47
|
-
scope === 'project'
|
|
48
|
-
? path.join(cwd, '.claude', 'skills')
|
|
49
|
-
: path.join(os.homedir(), '.claude', 'skills'),
|
|
50
|
-
mcpConfig: (scope, cwd) =>
|
|
51
|
-
scope === 'project'
|
|
52
|
-
? path.join(cwd, '.mcp.json')
|
|
53
|
-
: path.join(os.homedir(), '.claude.json'),
|
|
54
|
-
},
|
|
55
|
-
cursor: {
|
|
56
|
-
label: 'Cursor',
|
|
57
|
-
homeMarker: () => path.join(os.homedir(), '.cursor'),
|
|
58
|
-
skillsDir: (scope, cwd) =>
|
|
59
|
-
scope === 'project'
|
|
60
|
-
? path.join(cwd, '.cursor', 'skills')
|
|
61
|
-
: path.join(os.homedir(), '.cursor', 'skills'),
|
|
62
|
-
mcpConfig: (scope, cwd) =>
|
|
63
|
-
scope === 'project'
|
|
64
|
-
? path.join(cwd, '.cursor', 'mcp.json')
|
|
65
|
-
: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function parseArgs(argv) {
|
|
70
|
-
const opts = {
|
|
71
|
-
clients: null, // null = auto-detect
|
|
72
|
-
scope: 'user', // 'user' | 'project'
|
|
73
|
-
dryRun: false,
|
|
74
|
-
force: false,
|
|
75
|
-
mcp: true, // register MCP by default — the skill is useless without it
|
|
76
|
-
mcpCommand: 'npx', // 'npx' (no global install) | 'global' (restforge-mcp)
|
|
77
|
-
yes: false,
|
|
78
|
-
help: false,
|
|
79
|
-
};
|
|
80
|
-
for (const arg of argv) {
|
|
81
|
-
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
82
|
-
else if (arg === '--yes' || arg === '-y') opts.yes = true;
|
|
83
|
-
else if (arg === '--dry-run') opts.dryRun = true;
|
|
84
|
-
else if (arg === '--force') opts.force = true;
|
|
85
|
-
else if (arg === '--no-mcp') opts.mcp = false;
|
|
86
|
-
else if (arg === '--with-mcp') opts.mcp = true; // accepted; now the default
|
|
87
|
-
else if (arg.startsWith('--mcp-command=')) {
|
|
88
|
-
opts.mcpCommand = arg.slice('--mcp-command='.length).trim().toLowerCase();
|
|
89
|
-
} else if (arg.startsWith('--client=')) {
|
|
90
|
-
opts.clients = arg
|
|
91
|
-
.slice('--client='.length)
|
|
92
|
-
.split(',')
|
|
93
|
-
.map((c) => c.trim().toLowerCase())
|
|
94
|
-
.filter(Boolean);
|
|
95
|
-
} else if (arg.startsWith('--scope=')) {
|
|
96
|
-
opts.scope = arg.slice('--scope='.length).trim().toLowerCase();
|
|
97
|
-
} else {
|
|
98
|
-
console.error(`Unknown argument: ${arg}`);
|
|
99
|
-
opts.help = true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return opts;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const HELP = `
|
|
106
|
-
create-restforge-skills — install the RESTForge Agent Skill into your clients.
|
|
107
|
-
|
|
108
|
-
Usage:
|
|
109
|
-
npx create-restforge-skills [options]
|
|
110
|
-
|
|
111
|
-
By default this copies the skill AND registers the RESTForge MCP server (via npx),
|
|
112
|
-
so it works out of the box. The MCP config is merged, not overwritten, and the
|
|
113
|
-
existing file is backed up to <config>.bak first.
|
|
114
|
-
|
|
115
|
-
Options:
|
|
116
|
-
--client=<list> Comma-separated clients: claude,cursor,codex. Default: auto-detect.
|
|
117
|
-
--scope=<scope> user (default) installs globally for you; project installs
|
|
118
|
-
into ./.claude, ./.cursor or ./.agents (Codex).
|
|
119
|
-
--no-mcp Skip MCP registration (only do this if you know why —
|
|
120
|
-
the skill cannot call anything without the MCP server).
|
|
121
|
-
--mcp-command=<c> npx (default, no global install needed) or
|
|
122
|
-
global (uses the installed restforge-mcp binary).
|
|
123
|
-
--force Overwrite an existing restforge skill folder.
|
|
124
|
-
--dry-run Show what would happen without writing anything.
|
|
125
|
-
--yes, -y Run non-interactively, accepting defaults.
|
|
126
|
-
--help, -h Show this help.
|
|
127
|
-
`;
|
|
128
|
-
|
|
129
|
-
/** Decide which clients to target. */
|
|
130
|
-
function resolveClients(opts) {
|
|
131
|
-
if (opts.clients && opts.clients.length) {
|
|
132
|
-
const unknown = opts.clients.filter((c) => !CLIENTS[c]);
|
|
133
|
-
if (unknown.length) {
|
|
134
|
-
console.error(`Unknown client(s): ${unknown.join(', ')}`);
|
|
135
|
-
console.error(`Supported: ${Object.keys(CLIENTS).join(', ')}`);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
return opts.clients;
|
|
139
|
-
}
|
|
140
|
-
// Auto-detect: a client counts as present if its home marker dir exists.
|
|
141
|
-
return Object.keys(CLIENTS).filter((key) => {
|
|
142
|
-
try {
|
|
143
|
-
return fs.statSync(CLIENTS[key].homeMarker()).isDirectory();
|
|
144
|
-
} catch {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Build the concrete install targets for the chosen clients and scope. */
|
|
151
|
-
function buildTargets(clientKeys, opts) {
|
|
152
|
-
const cwd = process.cwd();
|
|
153
|
-
return clientKeys.map((key) => {
|
|
154
|
-
const client = CLIENTS[key];
|
|
155
|
-
return {
|
|
156
|
-
key,
|
|
157
|
-
label: client.label,
|
|
158
|
-
dest: path.join(client.skillsDir(opts.scope, cwd), SKILL_NAME),
|
|
159
|
-
mcpConfig: client.mcpConfig(opts.scope, cwd),
|
|
160
|
-
};
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Copy the skill folder (SKILL.md + references/) into one target. */
|
|
165
|
-
function installSkill(target, opts) {
|
|
166
|
-
const exists = fs.existsSync(target.dest);
|
|
167
|
-
if (exists && !opts.force) {
|
|
168
|
-
return { status: 'skipped', reason: 'already exists (use --force to overwrite)' };
|
|
169
|
-
}
|
|
170
|
-
if (opts.dryRun) {
|
|
171
|
-
return { status: exists ? 'would-overwrite' : 'would-install' };
|
|
172
|
-
}
|
|
173
|
-
fs.mkdirSync(path.dirname(target.dest), { recursive: true });
|
|
174
|
-
if (exists) fs.rmSync(target.dest, { recursive: true, force: true });
|
|
175
|
-
fs.cpSync(SOURCE_DIR, target.dest, { recursive: true });
|
|
176
|
-
return { status: exists ? 'overwritten' : 'installed' };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const pad = (s) => String(s).padEnd(12);
|
|
180
|
-
|
|
181
|
-
function main() {
|
|
182
|
-
const opts = parseArgs(process.argv.slice(2));
|
|
183
|
-
if (opts.help) {
|
|
184
|
-
process.stdout.write(HELP);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!['user', 'project'].includes(opts.scope)) {
|
|
189
|
-
console.error(`Invalid --scope: ${opts.scope} (expected "user" or "project")`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
if (!['npx', 'global'].includes(opts.mcpCommand)) {
|
|
193
|
-
console.error(`Invalid --mcp-command: ${opts.mcpCommand} (expected "npx" or "global")`);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
if (!fs.existsSync(path.join(SOURCE_DIR, 'SKILL.md'))) {
|
|
197
|
-
console.error(`Bundled skill not found at ${SOURCE_DIR}. Broken package?`);
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const clientKeys = resolveClients(opts);
|
|
202
|
-
if (!clientKeys.length) {
|
|
203
|
-
console.error('No target client detected (looked for ~/.claude, ~/.cursor and CODEX_HOME or ~/.codex).');
|
|
204
|
-
console.error('Pass one explicitly, e.g. --client=codex');
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const targets = buildTargets(clientKeys, opts);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(` ${pad(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const register = target.key === 'codex' ? registerCodexMcp : registerMcp;
|
|
223
|
-
const mcp = register(target.mcpConfig, mcpEntry, opts);
|
|
224
|
-
console.log(` ${pad('')} → mcp: ${mcp.status}${mcp.error ? ` — ${mcp.error}` : ''}`);
|
|
225
|
-
console.log(` ${pad('')} ${target.mcpConfig}`);
|
|
226
|
-
if (mcp.backup) console.log(` ${pad('')} backup: ${mcp.backup}`);
|
|
227
|
-
if (mcp.status === 'error') {
|
|
228
|
-
process.exitCode = 1;
|
|
229
|
-
const manual = target.key === 'codex' ? formatCodexMcp(mcpEntry) : `"mcpServers": { "restforge": ${JSON.stringify(mcpEntry)} }`;
|
|
230
|
-
console.log(` ${pad('')} add manually: ${manual}`);
|
|
231
|
-
}
|
|
232
|
-
if (mcp.status === 'preserved') console.log(' Existing Codex RESTForge settings kept; edit config.toml to change them.');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.log('\nNext step:');
|
|
237
|
-
console.log(' Set a valid RESTForge LICENSE for codegen_*/runtime_* operations.');
|
|
238
|
-
console.log(' (Designer/frontend tools work without a license.)');
|
|
239
|
-
if (clientKeys.includes('claude')) {
|
|
240
|
-
console.log(' Claude Code: if its skills dir was just created, restart the client so it');
|
|
241
|
-
console.log(' watches the new directory.');
|
|
242
|
-
}
|
|
243
|
-
if (clientKeys.includes('codex')) {
|
|
244
|
-
console.log(' Codex: restart the session, then invoke $restforge. Use /mcp to check the server.');
|
|
245
|
-
if (opts.scope === 'project') console.log(' Codex only loads project .codex/config.toml in trusted projects.');
|
|
246
|
-
}
|
|
247
|
-
console.log('');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
main();
|
|
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
|
+
* Uses a TOML parser for Codex configuration; works on Windows, macOS and Linux.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const { buildEntry, registerMcp } = require('./mcp');
|
|
24
|
+
const { registerCodexMcp, formatCodexMcp } = require('./codex');
|
|
25
|
+
const codexHome = () => process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
26
|
+
|
|
27
|
+
const SKILL_NAME = 'restforge';
|
|
28
|
+
// Bundled skill source: <package>/skills/restforge, resolved relative to this file.
|
|
29
|
+
const SOURCE_DIR = path.join(__dirname, '..', 'skills', SKILL_NAME);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Client registry. Each client knows where its skills live (user vs project
|
|
33
|
+
* scope) and where its MCP config file is. Paths are computed lazily so we never
|
|
34
|
+
* touch the filesystem for a client the user did not select.
|
|
35
|
+
*/
|
|
36
|
+
const CLIENTS = {
|
|
37
|
+
codex: {
|
|
38
|
+
label: 'Codex',
|
|
39
|
+
homeMarker: codexHome,
|
|
40
|
+
skillsDir: (scope, cwd) => path.join(scope === 'project' ? cwd : os.homedir(), '.agents', 'skills'),
|
|
41
|
+
mcpConfig: (scope, cwd) => path.join(scope === 'project' ? path.join(cwd, '.codex') : codexHome(), 'config.toml'),
|
|
42
|
+
},
|
|
43
|
+
claude: {
|
|
44
|
+
label: 'Claude Code',
|
|
45
|
+
homeMarker: () => path.join(os.homedir(), '.claude'),
|
|
46
|
+
skillsDir: (scope, cwd) =>
|
|
47
|
+
scope === 'project'
|
|
48
|
+
? path.join(cwd, '.claude', 'skills')
|
|
49
|
+
: path.join(os.homedir(), '.claude', 'skills'),
|
|
50
|
+
mcpConfig: (scope, cwd) =>
|
|
51
|
+
scope === 'project'
|
|
52
|
+
? path.join(cwd, '.mcp.json')
|
|
53
|
+
: path.join(os.homedir(), '.claude.json'),
|
|
54
|
+
},
|
|
55
|
+
cursor: {
|
|
56
|
+
label: 'Cursor',
|
|
57
|
+
homeMarker: () => path.join(os.homedir(), '.cursor'),
|
|
58
|
+
skillsDir: (scope, cwd) =>
|
|
59
|
+
scope === 'project'
|
|
60
|
+
? path.join(cwd, '.cursor', 'skills')
|
|
61
|
+
: path.join(os.homedir(), '.cursor', 'skills'),
|
|
62
|
+
mcpConfig: (scope, cwd) =>
|
|
63
|
+
scope === 'project'
|
|
64
|
+
? path.join(cwd, '.cursor', 'mcp.json')
|
|
65
|
+
: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function parseArgs(argv) {
|
|
70
|
+
const opts = {
|
|
71
|
+
clients: null, // null = auto-detect
|
|
72
|
+
scope: 'user', // 'user' | 'project'
|
|
73
|
+
dryRun: false,
|
|
74
|
+
force: false,
|
|
75
|
+
mcp: true, // register MCP by default — the skill is useless without it
|
|
76
|
+
mcpCommand: 'npx', // 'npx' (no global install) | 'global' (restforge-mcp)
|
|
77
|
+
yes: false,
|
|
78
|
+
help: false,
|
|
79
|
+
};
|
|
80
|
+
for (const arg of argv) {
|
|
81
|
+
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
82
|
+
else if (arg === '--yes' || arg === '-y') opts.yes = true;
|
|
83
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
84
|
+
else if (arg === '--force') opts.force = true;
|
|
85
|
+
else if (arg === '--no-mcp') opts.mcp = false;
|
|
86
|
+
else if (arg === '--with-mcp') opts.mcp = true; // accepted; now the default
|
|
87
|
+
else if (arg.startsWith('--mcp-command=')) {
|
|
88
|
+
opts.mcpCommand = arg.slice('--mcp-command='.length).trim().toLowerCase();
|
|
89
|
+
} else if (arg.startsWith('--client=')) {
|
|
90
|
+
opts.clients = arg
|
|
91
|
+
.slice('--client='.length)
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((c) => c.trim().toLowerCase())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
} else if (arg.startsWith('--scope=')) {
|
|
96
|
+
opts.scope = arg.slice('--scope='.length).trim().toLowerCase();
|
|
97
|
+
} else {
|
|
98
|
+
console.error(`Unknown argument: ${arg}`);
|
|
99
|
+
opts.help = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return opts;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const HELP = `
|
|
106
|
+
create-restforge-skills — install the RESTForge Agent Skill into your clients.
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
npx create-restforge-skills [options]
|
|
110
|
+
|
|
111
|
+
By default this copies the skill AND registers the RESTForge MCP server (via npx),
|
|
112
|
+
so it works out of the box. The MCP config is merged, not overwritten, and the
|
|
113
|
+
existing file is backed up to <config>.bak first.
|
|
114
|
+
|
|
115
|
+
Options:
|
|
116
|
+
--client=<list> Comma-separated clients: claude,cursor,codex. Default: auto-detect.
|
|
117
|
+
--scope=<scope> user (default) installs globally for you; project installs
|
|
118
|
+
into ./.claude, ./.cursor or ./.agents (Codex).
|
|
119
|
+
--no-mcp Skip MCP registration (only do this if you know why —
|
|
120
|
+
the skill cannot call anything without the MCP server).
|
|
121
|
+
--mcp-command=<c> npx (default, no global install needed) or
|
|
122
|
+
global (uses the installed restforge-mcp binary).
|
|
123
|
+
--force Overwrite an existing restforge skill folder.
|
|
124
|
+
--dry-run Show what would happen without writing anything.
|
|
125
|
+
--yes, -y Run non-interactively, accepting defaults.
|
|
126
|
+
--help, -h Show this help.
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
/** Decide which clients to target. */
|
|
130
|
+
function resolveClients(opts) {
|
|
131
|
+
if (opts.clients && opts.clients.length) {
|
|
132
|
+
const unknown = opts.clients.filter((c) => !CLIENTS[c]);
|
|
133
|
+
if (unknown.length) {
|
|
134
|
+
console.error(`Unknown client(s): ${unknown.join(', ')}`);
|
|
135
|
+
console.error(`Supported: ${Object.keys(CLIENTS).join(', ')}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
return opts.clients;
|
|
139
|
+
}
|
|
140
|
+
// Auto-detect: a client counts as present if its home marker dir exists.
|
|
141
|
+
return Object.keys(CLIENTS).filter((key) => {
|
|
142
|
+
try {
|
|
143
|
+
return fs.statSync(CLIENTS[key].homeMarker()).isDirectory();
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Build the concrete install targets for the chosen clients and scope. */
|
|
151
|
+
function buildTargets(clientKeys, opts) {
|
|
152
|
+
const cwd = process.cwd();
|
|
153
|
+
return clientKeys.map((key) => {
|
|
154
|
+
const client = CLIENTS[key];
|
|
155
|
+
return {
|
|
156
|
+
key,
|
|
157
|
+
label: client.label,
|
|
158
|
+
dest: path.join(client.skillsDir(opts.scope, cwd), SKILL_NAME),
|
|
159
|
+
mcpConfig: client.mcpConfig(opts.scope, cwd),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Copy the skill folder (SKILL.md + references/) into one target. */
|
|
165
|
+
function installSkill(target, opts) {
|
|
166
|
+
const exists = fs.existsSync(target.dest);
|
|
167
|
+
if (exists && !opts.force) {
|
|
168
|
+
return { status: 'skipped', reason: 'already exists (use --force to overwrite)' };
|
|
169
|
+
}
|
|
170
|
+
if (opts.dryRun) {
|
|
171
|
+
return { status: exists ? 'would-overwrite' : 'would-install' };
|
|
172
|
+
}
|
|
173
|
+
fs.mkdirSync(path.dirname(target.dest), { recursive: true });
|
|
174
|
+
if (exists) fs.rmSync(target.dest, { recursive: true, force: true });
|
|
175
|
+
fs.cpSync(SOURCE_DIR, target.dest, { recursive: true });
|
|
176
|
+
return { status: exists ? 'overwritten' : 'installed' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pad = (s) => String(s).padEnd(12);
|
|
180
|
+
|
|
181
|
+
function main() {
|
|
182
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
183
|
+
if (opts.help) {
|
|
184
|
+
process.stdout.write(HELP);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!['user', 'project'].includes(opts.scope)) {
|
|
189
|
+
console.error(`Invalid --scope: ${opts.scope} (expected "user" or "project")`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
if (!['npx', 'global'].includes(opts.mcpCommand)) {
|
|
193
|
+
console.error(`Invalid --mcp-command: ${opts.mcpCommand} (expected "npx" or "global")`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (!fs.existsSync(path.join(SOURCE_DIR, 'SKILL.md'))) {
|
|
197
|
+
console.error(`Bundled skill not found at ${SOURCE_DIR}. Broken package?`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const clientKeys = resolveClients(opts);
|
|
202
|
+
if (!clientKeys.length) {
|
|
203
|
+
console.error('No target client detected (looked for ~/.claude, ~/.cursor and CODEX_HOME or ~/.codex).');
|
|
204
|
+
console.error('Pass one explicitly, e.g. --client=codex');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const targets = buildTargets(clientKeys, opts);
|
|
209
|
+
|
|
210
|
+
console.log(
|
|
211
|
+
`\nRESTForge skill — ${opts.dryRun ? 'dry run' : 'install'} (scope: ${opts.scope}` +
|
|
212
|
+
`, mcp: ${opts.mcp ? opts.mcpCommand : 'off'})\n`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
for (const target of targets) {
|
|
216
|
+
const skill = installSkill(target, opts);
|
|
217
|
+
console.log(` ${pad(target.label)} ${target.dest}`);
|
|
218
|
+
console.log(` ${pad('')} → skill: ${skill.status}${skill.reason ? ` (${skill.reason})` : ''}`);
|
|
219
|
+
|
|
220
|
+
if (opts.mcp) {
|
|
221
|
+
const mcpEntry = buildEntry(opts.mcpCommand, target.key);
|
|
222
|
+
const register = target.key === 'codex' ? registerCodexMcp : registerMcp;
|
|
223
|
+
const mcp = register(target.mcpConfig, mcpEntry, opts);
|
|
224
|
+
console.log(` ${pad('')} → mcp: ${mcp.status}${mcp.error ? ` — ${mcp.error}` : ''}`);
|
|
225
|
+
console.log(` ${pad('')} ${target.mcpConfig}`);
|
|
226
|
+
if (mcp.backup) console.log(` ${pad('')} backup: ${mcp.backup}`);
|
|
227
|
+
if (mcp.status === 'error') {
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
const manual = target.key === 'codex' ? formatCodexMcp(mcpEntry) : `"mcpServers": { "restforge": ${JSON.stringify(mcpEntry)} }`;
|
|
230
|
+
console.log(` ${pad('')} add manually: ${manual}`);
|
|
231
|
+
}
|
|
232
|
+
if (mcp.status === 'preserved') console.log(' Existing Codex RESTForge settings kept; edit config.toml to change them.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('\nNext step:');
|
|
237
|
+
console.log(' Set a valid RESTForge LICENSE for codegen_*/runtime_* operations.');
|
|
238
|
+
console.log(' (Designer/frontend tools work without a license.)');
|
|
239
|
+
if (clientKeys.includes('claude')) {
|
|
240
|
+
console.log(' Claude Code: if its skills dir was just created, restart the client so it');
|
|
241
|
+
console.log(' watches the new directory.');
|
|
242
|
+
}
|
|
243
|
+
if (clientKeys.includes('codex')) {
|
|
244
|
+
console.log(' Codex: restart the session, then invoke $restforge. Use /mcp to check the server.');
|
|
245
|
+
if (opts.scope === 'project') console.log(' Codex only loads project .codex/config.toml in trusted projects.');
|
|
246
|
+
}
|
|
247
|
+
console.log('');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main();
|