api-to-cli 0.1.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/LICENSE +21 -0
- package/PROJECT_BRIEF.md +65 -0
- package/README.md +130 -0
- package/SPEC.md +99 -0
- package/bin/api-to-cli.js +5 -0
- package/examples/trello/api-to-cli.config.js +60 -0
- package/examples/trello/trelloapi-agent/README.md +11 -0
- package/examples/trello/trelloapi-agent/agentbridge.manifest.json +68 -0
- package/examples/trello/trelloapi-agent/cli/README.md +25 -0
- package/examples/trello/trelloapi-agent/cli/bin/trelloapi.js +37 -0
- package/examples/trello/trelloapi-agent/cli/commands/get-board.js +41 -0
- package/examples/trello/trelloapi-agent/cli/commands/list-board-lists.js +41 -0
- package/examples/trello/trelloapi-agent/cli/commands/list-list-cards.js +41 -0
- package/examples/trello/trelloapi-agent/cli/lib/client.js +90 -0
- package/examples/trello/trelloapi-agent/cli/lib/output.js +21 -0
- package/examples/trello/trelloapi-agent/cli/package.json +16 -0
- package/examples/trello/trelloapi-agent/skill/SKILL.md +34 -0
- package/examples/trello/trelloapi-cli/README.md +25 -0
- package/examples/trello/trelloapi-cli/bin/trelloapi.js +37 -0
- package/examples/trello/trelloapi-cli/commands/get-board.js +41 -0
- package/examples/trello/trelloapi-cli/commands/list-board-lists.js +41 -0
- package/examples/trello/trelloapi-cli/commands/list-list-cards.js +41 -0
- package/examples/trello/trelloapi-cli/lib/client.js +90 -0
- package/examples/trello/trelloapi-cli/lib/output.js +21 -0
- package/examples/trello/trelloapi-cli/package.json +16 -0
- package/package.json +48 -0
- package/src/commands/generate.js +36 -0
- package/src/commands/scaffold.js +110 -0
- package/src/commands/validate.js +30 -0
- package/src/index.js +92 -0
- package/src/lib/config-utils.js +21 -0
- package/src/lib/generate-cli.js +295 -0
- package/src/lib/generate-manifest.js +51 -0
- package/src/lib/generate-skill.js +50 -0
- package/src/lib/load-config.js +120 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { loadConfig } = require('../lib/load-config');
|
|
3
|
+
|
|
4
|
+
async function validate(flags) {
|
|
5
|
+
if (!flags.config) {
|
|
6
|
+
throw new Error('Missing required flag: --config <path>');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const configPath = path.resolve(process.cwd(), String(flags.config));
|
|
10
|
+
const config = loadConfig(configPath);
|
|
11
|
+
|
|
12
|
+
console.log(
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
ok: true,
|
|
15
|
+
command: 'validate',
|
|
16
|
+
configPath,
|
|
17
|
+
summary: {
|
|
18
|
+
name: config.name,
|
|
19
|
+
version: config.version,
|
|
20
|
+
apiBase: config.apiBase,
|
|
21
|
+
commandCount: config.commands.length,
|
|
22
|
+
hasAuth: Boolean(config.auth && config.auth.credentials && config.auth.credentials.length)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
validate
|
|
30
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { generate } = require('./commands/generate');
|
|
2
|
+
const { validate } = require('./commands/validate');
|
|
3
|
+
const { scaffold } = require('./commands/scaffold');
|
|
4
|
+
|
|
5
|
+
function printUsage() {
|
|
6
|
+
console.error(
|
|
7
|
+
[
|
|
8
|
+
'Usage:',
|
|
9
|
+
' api-to-cli generate --config <path> --output <dir>',
|
|
10
|
+
' api-to-cli validate --config <path>',
|
|
11
|
+
' api-to-cli scaffold --config <path> --output <dir> [--with-skill] [--with-manifest]',
|
|
12
|
+
'',
|
|
13
|
+
'Commands:',
|
|
14
|
+
' generate Generate a CLI from config',
|
|
15
|
+
' validate Validate config only',
|
|
16
|
+
' scaffold Generate CLI + optional skill/manifest bundle'
|
|
17
|
+
].join('\n')
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseFlags(args) {
|
|
22
|
+
const flags = {};
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
25
|
+
const arg = args[i];
|
|
26
|
+
|
|
27
|
+
if (!arg.startsWith('--')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const key = arg.slice(2);
|
|
32
|
+
const value = args[i + 1];
|
|
33
|
+
|
|
34
|
+
if (!value || value.startsWith('--')) {
|
|
35
|
+
flags[key] = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
flags[key] = value;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return flags;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function run(argv) {
|
|
47
|
+
const [command, ...rest] = argv;
|
|
48
|
+
|
|
49
|
+
if (!command || command === '--help' || command === '-h') {
|
|
50
|
+
printUsage();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const flags = parseFlags(rest);
|
|
56
|
+
|
|
57
|
+
if (command === 'generate') {
|
|
58
|
+
await generate(flags);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === 'validate') {
|
|
63
|
+
await validate(flags);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (command === 'scaffold') {
|
|
68
|
+
await scaffold(flags);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(`Unknown command: ${command}`);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(
|
|
75
|
+
JSON.stringify(
|
|
76
|
+
{
|
|
77
|
+
error: true,
|
|
78
|
+
code: 'GENERATOR_FAILED',
|
|
79
|
+
message: error.message,
|
|
80
|
+
details: {}
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
run
|
|
92
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function toKebab(input) {
|
|
2
|
+
return String(input)
|
|
3
|
+
.trim()
|
|
4
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
7
|
+
.replace(/^-+|-+$/g, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getAuthEnvVars(config) {
|
|
11
|
+
const authVars = (config.auth && Array.isArray(config.auth.credentials))
|
|
12
|
+
? config.auth.credentials.map((credential) => credential.envVar)
|
|
13
|
+
: [];
|
|
14
|
+
|
|
15
|
+
return [...new Set(authVars)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
toKebab,
|
|
20
|
+
getAuthEnvVars
|
|
21
|
+
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { toKebab } = require('./config-utils');
|
|
4
|
+
|
|
5
|
+
function ensureDir(dirPath) {
|
|
6
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function commandToMethodName(commandName) {
|
|
10
|
+
return String(commandName)
|
|
11
|
+
.split(/[^a-zA-Z0-9]/)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((part, index) => {
|
|
14
|
+
const lower = part.toLowerCase();
|
|
15
|
+
return index === 0 ? lower : lower[0].toUpperCase() + lower.slice(1);
|
|
16
|
+
})
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderPackageJson(config) {
|
|
21
|
+
const packageJson = {
|
|
22
|
+
name: `${toKebab(config.name)}-cli`,
|
|
23
|
+
version: config.version,
|
|
24
|
+
description: `${config.name} CLI generated by AgentBridge`,
|
|
25
|
+
license: 'MIT',
|
|
26
|
+
type: 'commonjs',
|
|
27
|
+
bin: {
|
|
28
|
+
[toKebab(config.name)]: `./bin/${toKebab(config.name)}.js`
|
|
29
|
+
},
|
|
30
|
+
scripts: {
|
|
31
|
+
start: `node ./bin/${toKebab(config.name)}.js`
|
|
32
|
+
},
|
|
33
|
+
dependencies: {
|
|
34
|
+
commander: '^12.1.0'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderOutputLib() {
|
|
42
|
+
return `function json(data, pretty) {
|
|
43
|
+
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
44
|
+
process.stdout.write(text + '\\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function error(payload, pretty) {
|
|
48
|
+
const envelope = {
|
|
49
|
+
error: true,
|
|
50
|
+
code: payload.code || 'REQUEST_FAILED',
|
|
51
|
+
message: payload.message || 'Request failed',
|
|
52
|
+
details: payload.details || {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const text = pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope);
|
|
56
|
+
process.stderr.write(text + '\\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
json,
|
|
61
|
+
error
|
|
62
|
+
};
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderClientLib(config) {
|
|
67
|
+
const authConfig = JSON.stringify(config.auth || { credentials: [] }, null, 2);
|
|
68
|
+
|
|
69
|
+
return `async function request(command, options) {
|
|
70
|
+
const auth = ${authConfig};
|
|
71
|
+
const params = new URLSearchParams();
|
|
72
|
+
const commandParams = command.params || {};
|
|
73
|
+
let resolvedPath = command.path;
|
|
74
|
+
const headers = {
|
|
75
|
+
accept: 'application/json'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
(auth.credentials || []).forEach((credential) => {
|
|
79
|
+
const envValue = process.env[credential.envVar];
|
|
80
|
+
|
|
81
|
+
if (!envValue) {
|
|
82
|
+
throw new Error(\`Missing required auth environment variable: \${credential.envVar}\`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const authValue = credential.prefix ? \`\${credential.prefix}\${envValue}\` : envValue;
|
|
86
|
+
|
|
87
|
+
if (credential.in === 'header') {
|
|
88
|
+
headers[credential.name] = authValue;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
params.append(credential.name, authValue);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Object.entries(commandParams).forEach(([name, schema]) => {
|
|
96
|
+
const value = options[name];
|
|
97
|
+
|
|
98
|
+
if ((value === undefined || value === null || value === '') && schema.required) {
|
|
99
|
+
throw new Error(\`Missing required parameter: --\${name}\`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (value === undefined || value === null || value === '') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = \`{\${name}}\`;
|
|
107
|
+
|
|
108
|
+
if (resolvedPath.includes(token)) {
|
|
109
|
+
resolvedPath = resolvedPath.replaceAll(token, encodeURIComponent(String(value)));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
params.append(name, String(value));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const query = params.toString();
|
|
117
|
+
const url = '${config.apiBase}' + resolvedPath + (query ? '?' + query : '');
|
|
118
|
+
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
method: command.method,
|
|
121
|
+
headers
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
let body = text;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
body = text ? JSON.parse(text) : null;
|
|
129
|
+
} catch (_err) {
|
|
130
|
+
body = text;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const error = new Error(\`HTTP \${response.status}\`);
|
|
135
|
+
error.statusCode = response.status;
|
|
136
|
+
error.responseBody = body;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return body;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
request
|
|
145
|
+
};
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderCommandModule(command) {
|
|
150
|
+
const functionName = commandToMethodName(command.name);
|
|
151
|
+
const serializedCommand = JSON.stringify(command, null, 2);
|
|
152
|
+
|
|
153
|
+
return `const { request } = require('../lib/client');
|
|
154
|
+
const output = require('../lib/output');
|
|
155
|
+
|
|
156
|
+
const command = ${serializedCommand};
|
|
157
|
+
|
|
158
|
+
async function ${functionName}(options) {
|
|
159
|
+
try {
|
|
160
|
+
const data = await request(command, options);
|
|
161
|
+
output.json(data, Boolean(options.pretty));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
output.error(
|
|
164
|
+
{
|
|
165
|
+
code: error.statusCode ? 'HTTP_ERROR' : 'REQUEST_FAILED',
|
|
166
|
+
message: error.message,
|
|
167
|
+
details: {
|
|
168
|
+
statusCode: error.statusCode || null,
|
|
169
|
+
command: command.name
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
Boolean(options.pretty)
|
|
173
|
+
);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
run: ${functionName},
|
|
180
|
+
command
|
|
181
|
+
};
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderBinFile(config) {
|
|
186
|
+
const imports = config.commands
|
|
187
|
+
.map((command, index) => {
|
|
188
|
+
const varName = `cmd${index}`;
|
|
189
|
+
return `const ${varName} = require('../commands/${toKebab(command.name)}');`;
|
|
190
|
+
})
|
|
191
|
+
.join('\n');
|
|
192
|
+
|
|
193
|
+
const registrations = config.commands
|
|
194
|
+
.map((command, index) => {
|
|
195
|
+
const varName = `cmd${index}`;
|
|
196
|
+
const params = command.params || {};
|
|
197
|
+
const options = Object.entries(params)
|
|
198
|
+
.map(([paramName, schema]) => {
|
|
199
|
+
const flagName = `--${toKebab(paramName)} <value>`;
|
|
200
|
+
const desc = schema.description || `${paramName} parameter`;
|
|
201
|
+
return ` .option('${flagName}', '${desc}')`;
|
|
202
|
+
})
|
|
203
|
+
.join('\n');
|
|
204
|
+
|
|
205
|
+
return `program
|
|
206
|
+
.command('${command.name}')
|
|
207
|
+
.description('${command.description.replace(/'/g, "\\'")}')
|
|
208
|
+
${options ? `${options}\n` : ''} .option('--pretty', 'Pretty-print JSON')
|
|
209
|
+
.action((options) => ${varName}.run(options));`;
|
|
210
|
+
})
|
|
211
|
+
.join('\n\n');
|
|
212
|
+
|
|
213
|
+
return `#!/usr/bin/env node
|
|
214
|
+
|
|
215
|
+
const { Command } = require('commander');
|
|
216
|
+
|
|
217
|
+
${imports}
|
|
218
|
+
|
|
219
|
+
const program = new Command();
|
|
220
|
+
|
|
221
|
+
program
|
|
222
|
+
.name('${toKebab(config.name)}')
|
|
223
|
+
.description('${config.name} CLI generated by AgentBridge')
|
|
224
|
+
.version('${config.version}');
|
|
225
|
+
|
|
226
|
+
${registrations}
|
|
227
|
+
|
|
228
|
+
program.parse(process.argv);
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderReadme(config) {
|
|
233
|
+
const commandList = config.commands
|
|
234
|
+
.map((command) => `- \`${toKebab(config.name)} ${command.name}\` - ${command.description}`)
|
|
235
|
+
.join('\n');
|
|
236
|
+
const authVars = (config.auth && Array.isArray(config.auth.credentials))
|
|
237
|
+
? config.auth.credentials.map((credential) => credential.envVar)
|
|
238
|
+
: [];
|
|
239
|
+
const uniqueAuthVars = [...new Set(authVars)];
|
|
240
|
+
const authSection = uniqueAuthVars.length
|
|
241
|
+
? `## Auth\n\nSet required environment variables before running commands:\n\n${uniqueAuthVars.map((envVar) => `- \`${envVar}\``).join('\n')}\n\nDo not pass secrets as command flags.\n\n`
|
|
242
|
+
: '';
|
|
243
|
+
|
|
244
|
+
return `# ${config.name} CLI
|
|
245
|
+
|
|
246
|
+
Generated by AgentBridge (api-to-cli).
|
|
247
|
+
|
|
248
|
+
## Install
|
|
249
|
+
|
|
250
|
+
\`\`\`bash
|
|
251
|
+
npm install
|
|
252
|
+
npm link
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
${authSection}## Commands
|
|
256
|
+
|
|
257
|
+
${commandList}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function writeFile(filePath, content, executable = false) {
|
|
262
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
263
|
+
if (executable) {
|
|
264
|
+
fs.chmodSync(filePath, 0o755);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function generateCliProject({ config, outputPath }) {
|
|
269
|
+
const binName = toKebab(config.name);
|
|
270
|
+
const binDir = path.join(outputPath, 'bin');
|
|
271
|
+
const commandsDir = path.join(outputPath, 'commands');
|
|
272
|
+
const libDir = path.join(outputPath, 'lib');
|
|
273
|
+
|
|
274
|
+
ensureDir(outputPath);
|
|
275
|
+
ensureDir(binDir);
|
|
276
|
+
ensureDir(commandsDir);
|
|
277
|
+
ensureDir(libDir);
|
|
278
|
+
|
|
279
|
+
writeFile(path.join(outputPath, 'package.json'), renderPackageJson(config));
|
|
280
|
+
writeFile(path.join(outputPath, 'README.md'), renderReadme(config));
|
|
281
|
+
writeFile(path.join(libDir, 'output.js'), renderOutputLib());
|
|
282
|
+
writeFile(path.join(libDir, 'client.js'), renderClientLib(config));
|
|
283
|
+
writeFile(path.join(binDir, `${binName}.js`), renderBinFile(config), true);
|
|
284
|
+
|
|
285
|
+
config.commands.forEach((command) => {
|
|
286
|
+
writeFile(
|
|
287
|
+
path.join(commandsDir, `${toKebab(command.name)}.js`),
|
|
288
|
+
renderCommandModule(command)
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
generateCliProject
|
|
295
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { toKebab, getAuthEnvVars } = require('./config-utils');
|
|
4
|
+
|
|
5
|
+
function buildManifest(config, cliProjectPath, skillPath) {
|
|
6
|
+
const binName = toKebab(config.name);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
schemaVersion: '1.0',
|
|
10
|
+
generatedBy: 'AgentBridge',
|
|
11
|
+
generatedAt: new Date().toISOString(),
|
|
12
|
+
project: {
|
|
13
|
+
name: config.name,
|
|
14
|
+
version: config.version
|
|
15
|
+
},
|
|
16
|
+
cli: {
|
|
17
|
+
projectPath: cliProjectPath,
|
|
18
|
+
packageName: `${binName}-cli`,
|
|
19
|
+
binary: binName,
|
|
20
|
+
install: ['npm install', 'npm link']
|
|
21
|
+
},
|
|
22
|
+
auth: {
|
|
23
|
+
envVars: getAuthEnvVars(config)
|
|
24
|
+
},
|
|
25
|
+
commands: config.commands.map((command) => ({
|
|
26
|
+
name: command.name,
|
|
27
|
+
description: command.description,
|
|
28
|
+
method: command.method,
|
|
29
|
+
path: command.path,
|
|
30
|
+
params: Object.entries(command.params || {}).map(([name, schema]) => ({
|
|
31
|
+
name,
|
|
32
|
+
required: Boolean(schema.required),
|
|
33
|
+
description: schema.description || ''
|
|
34
|
+
}))
|
|
35
|
+
})),
|
|
36
|
+
agent: {
|
|
37
|
+
skillPath: skillPath || null
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeManifest(outputPath, manifest) {
|
|
43
|
+
const filePath = path.join(outputPath, 'agentbridge.manifest.json');
|
|
44
|
+
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
45
|
+
return filePath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
buildManifest,
|
|
50
|
+
writeManifest
|
|
51
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { toKebab, getAuthEnvVars } = require('./config-utils');
|
|
4
|
+
|
|
5
|
+
function renderSkill(config, cliProjectPath) {
|
|
6
|
+
const binName = toKebab(config.name);
|
|
7
|
+
const envVars = getAuthEnvVars(config);
|
|
8
|
+
const envSection = envVars.length
|
|
9
|
+
? envVars.map((envVar) => `- export ${envVar}=\"<value>\"`).join('\n')
|
|
10
|
+
: '- No auth env vars required';
|
|
11
|
+
|
|
12
|
+
const commandDocs = config.commands
|
|
13
|
+
.map((command) => {
|
|
14
|
+
const flags = Object.entries(command.params || {})
|
|
15
|
+
.map(([name, schema]) => {
|
|
16
|
+
const flag = `--${toKebab(name)} <value>`;
|
|
17
|
+
const req = schema.required ? 'required' : 'optional';
|
|
18
|
+
return ` - ${flag} (${req})`;
|
|
19
|
+
})
|
|
20
|
+
.join('\n');
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
`- ${command.name}: ${command.description}`,
|
|
24
|
+
flags || ' - no params',
|
|
25
|
+
` - example: ${binName} ${command.name}${flags ? ` ${Object.keys(command.params || {}).map((p) => `--${toKebab(p)} <value>`).join(' ')}` : ''}`
|
|
26
|
+
].join('\n');
|
|
27
|
+
})
|
|
28
|
+
.join('\n');
|
|
29
|
+
|
|
30
|
+
return `# ${config.name} CLI Skill\n\n## Purpose\nUse the generated ${config.name} CLI from AgentBridge. Always prefer JSON output for machine parsing.\n\n## Location\n- CLI project: ${cliProjectPath}\n- Binary name: ${binName}\n\n## Setup\n1. cd ${cliProjectPath}\n2. npm install\n3. npm link\n\n## Auth\n${envSection}\n\n## Commands\n${commandDocs}\n\n## Rules\n- Do not echo or log auth secrets.\n- Do not pass credentials as command flags.\n- Parse command stdout as JSON.\n- Treat non-zero exits as failure and read stderr JSON envelope.\n`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeSkillPackage(outputPath, config, cliProjectPath) {
|
|
34
|
+
const skillDir = path.join(outputPath, 'skill');
|
|
35
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const skillContent = renderSkill(config, cliProjectPath);
|
|
38
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
39
|
+
|
|
40
|
+
fs.writeFileSync(skillPath, skillContent, 'utf8');
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
skillDir,
|
|
44
|
+
skillPath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
writeSkillPackage
|
|
50
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function fail(message) {
|
|
5
|
+
throw new Error(`Invalid config: ${message}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isObject(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isNonEmptyString(value) {
|
|
13
|
+
return typeof value === 'string' && Boolean(value.trim());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateAuthCredential(credential, index) {
|
|
17
|
+
if (!isObject(credential)) {
|
|
18
|
+
fail(`auth.credentials[${index}] must be an object`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!isNonEmptyString(credential.envVar)) {
|
|
22
|
+
fail(`auth.credentials[${index}].envVar must be a non-empty string`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (credential.in !== 'header' && credential.in !== 'query') {
|
|
26
|
+
fail(`auth.credentials[${index}].in must be either "header" or "query"`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!isNonEmptyString(credential.name)) {
|
|
30
|
+
fail(`auth.credentials[${index}].name must be a non-empty string`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (credential.prefix !== undefined && typeof credential.prefix !== 'string') {
|
|
34
|
+
fail(`auth.credentials[${index}].prefix must be a string when provided`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateAuth(auth) {
|
|
39
|
+
if (!isObject(auth)) {
|
|
40
|
+
fail('auth must be an object when provided');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(auth.credentials) || auth.credentials.length === 0) {
|
|
44
|
+
fail('auth.credentials must be a non-empty array when auth is provided');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
auth.credentials.forEach(validateAuthCredential);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateCommand(command, index) {
|
|
51
|
+
if (!isObject(command)) {
|
|
52
|
+
fail(`commands[${index}] must be an object`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isNonEmptyString(command.name)) {
|
|
56
|
+
fail(`commands[${index}].name must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isNonEmptyString(command.description)) {
|
|
60
|
+
fail(`commands[${index}].description must be a non-empty string`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command.method !== 'GET') {
|
|
64
|
+
fail(`commands[${index}].method must be GET for MVP`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof command.path !== 'string' || !command.path.startsWith('/')) {
|
|
68
|
+
fail(`commands[${index}].path must be a string starting with /`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (command.params !== undefined && !isObject(command.params)) {
|
|
72
|
+
fail(`commands[${index}].params must be an object when provided`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateConfig(config) {
|
|
77
|
+
if (!isObject(config)) {
|
|
78
|
+
fail('config must export an object');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isNonEmptyString(config.name)) {
|
|
82
|
+
fail('name must be a non-empty string');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isNonEmptyString(config.version)) {
|
|
86
|
+
fail('version must be a non-empty string');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof config.apiBase !== 'string' || !/^https?:\/\//.test(config.apiBase)) {
|
|
90
|
+
fail('apiBase must be an http(s) URL');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(config.commands) || config.commands.length === 0) {
|
|
94
|
+
fail('commands must be a non-empty array');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.auth !== undefined) {
|
|
98
|
+
validateAuth(config.auth);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
config.commands.forEach(validateCommand);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function loadConfig(configPath) {
|
|
105
|
+
if (!fs.existsSync(configPath)) {
|
|
106
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const resolved = path.resolve(configPath);
|
|
110
|
+
delete require.cache[resolved];
|
|
111
|
+
const config = require(resolved);
|
|
112
|
+
|
|
113
|
+
validateConfig(config);
|
|
114
|
+
|
|
115
|
+
return config;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
loadConfig
|
|
120
|
+
};
|