api-to-cli 0.1.2 → 0.1.3

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 (62) hide show
  1. package/README.md +126 -4
  2. package/examples/openapi/sample-openapi-agent/README.md +12 -0
  3. package/examples/openapi/sample-openapi-agent/agentbridge.manifest.json +85 -0
  4. package/examples/openapi/sample-openapi-agent/cli/README.md +18 -0
  5. package/examples/openapi/sample-openapi-agent/cli/bin/sample-crm-api.js +64 -0
  6. package/examples/openapi/sample-openapi-agent/cli/commands/create-contact.js +59 -0
  7. package/examples/openapi/sample-openapi-agent/cli/commands/delete-contacts-by-contactid.js +45 -0
  8. package/examples/openapi/sample-openapi-agent/cli/commands/get-contacts-by-contactid.js +45 -0
  9. package/examples/openapi/sample-openapi-agent/cli/commands/list-contacts.js +45 -0
  10. package/examples/openapi/sample-openapi-agent/cli/commands/patch-contacts-by-contactid.js +60 -0
  11. package/examples/openapi/sample-openapi-agent/cli/lib/client.js +244 -0
  12. package/examples/openapi/sample-openapi-agent/cli/lib/output.js +21 -0
  13. package/examples/openapi/sample-openapi-agent/cli/package.json +16 -0
  14. package/examples/openapi/sample-openapi-agent/skill/SKILL.md +50 -0
  15. package/examples/openapi/sample-openapi-cli/README.md +18 -0
  16. package/examples/openapi/sample-openapi-cli/bin/sample-crm-api.js +64 -0
  17. package/examples/openapi/sample-openapi-cli/commands/create-contact.js +59 -0
  18. package/examples/openapi/sample-openapi-cli/commands/delete-contacts-by-contactid.js +45 -0
  19. package/examples/openapi/sample-openapi-cli/commands/get-contacts-by-contactid.js +45 -0
  20. package/examples/openapi/sample-openapi-cli/commands/list-contacts.js +45 -0
  21. package/examples/openapi/sample-openapi-cli/commands/patch-contacts-by-contactid.js +60 -0
  22. package/examples/openapi/sample-openapi-cli/lib/client.js +244 -0
  23. package/examples/openapi/sample-openapi-cli/lib/output.js +21 -0
  24. package/examples/openapi/sample-openapi-cli/node_modules/.package-lock.json +15 -0
  25. package/examples/openapi/sample-openapi-cli/node_modules/commander/LICENSE +22 -0
  26. package/examples/openapi/sample-openapi-cli/node_modules/commander/Readme.md +1157 -0
  27. package/examples/openapi/sample-openapi-cli/node_modules/commander/esm.mjs +16 -0
  28. package/examples/openapi/sample-openapi-cli/node_modules/commander/index.js +24 -0
  29. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/argument.js +149 -0
  30. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/command.js +2509 -0
  31. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/error.js +39 -0
  32. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/help.js +520 -0
  33. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/option.js +330 -0
  34. package/examples/openapi/sample-openapi-cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  35. package/examples/openapi/sample-openapi-cli/node_modules/commander/package-support.json +16 -0
  36. package/examples/openapi/sample-openapi-cli/node_modules/commander/package.json +84 -0
  37. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/esm.d.mts +3 -0
  38. package/examples/openapi/sample-openapi-cli/node_modules/commander/typings/index.d.ts +969 -0
  39. package/examples/openapi/sample-openapi-cli/package.json +16 -0
  40. package/examples/openapi/sample-openapi.yaml +67 -0
  41. package/examples/trello/trelloapi-agent/README.md +1 -0
  42. package/examples/trello/trelloapi-agent/agentbridge.manifest.json +1 -1
  43. package/examples/trello/trelloapi-agent/cli/commands/get-board.js +4 -0
  44. package/examples/trello/trelloapi-agent/cli/commands/list-board-lists.js +4 -0
  45. package/examples/trello/trelloapi-agent/cli/commands/list-list-cards.js +4 -0
  46. package/examples/trello/trelloapi-agent/cli/lib/client.js +174 -9
  47. package/examples/trello/trelloapi-cli/commands/get-board.js +4 -0
  48. package/examples/trello/trelloapi-cli/commands/list-board-lists.js +4 -0
  49. package/examples/trello/trelloapi-cli/commands/list-list-cards.js +4 -0
  50. package/examples/trello/trelloapi-cli/lib/client.js +174 -9
  51. package/package.json +8 -2
  52. package/src/commands/doctor.js +234 -0
  53. package/src/commands/generate.js +4 -8
  54. package/src/commands/init.js +154 -0
  55. package/src/commands/scaffold.js +9 -9
  56. package/src/commands/validate.js +6 -10
  57. package/src/index.js +21 -5
  58. package/src/lib/generate-cli.js +208 -15
  59. package/src/lib/generate-skill.js +24 -2
  60. package/src/lib/load-config.js +39 -3
  61. package/src/lib/openapi-to-config.js +314 -0
  62. package/src/lib/resolve-config-input.js +50 -0
@@ -0,0 +1,234 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadConfig } = require('../lib/load-config');
4
+ const { loadConfigFromOpenApi } = require('../lib/openapi-to-config');
5
+
6
+ const COMMON_SPEC_PATHS = [
7
+ '/openapi.json',
8
+ '/openapi.yaml',
9
+ '/openapi.yml',
10
+ '/swagger.json',
11
+ '/v1/openapi.json',
12
+ '/v1/swagger.json',
13
+ '/.well-known/openapi.json'
14
+ ];
15
+
16
+ function compareVersions(a, b) {
17
+ const pa = a.split('.').map((n) => Number(n));
18
+ const pb = b.split('.').map((n) => Number(n));
19
+ const len = Math.max(pa.length, pb.length);
20
+
21
+ for (let i = 0; i < len; i += 1) {
22
+ const da = pa[i] || 0;
23
+ const db = pb[i] || 0;
24
+ if (da > db) {
25
+ return 1;
26
+ }
27
+ if (da < db) {
28
+ return -1;
29
+ }
30
+ }
31
+
32
+ return 0;
33
+ }
34
+
35
+ function makeCheck(name, ok, details, fix) {
36
+ return {
37
+ name,
38
+ ok,
39
+ details,
40
+ fix: fix || null
41
+ };
42
+ }
43
+
44
+ function isHttpUrl(value) {
45
+ return /^https?:\/\//i.test(String(value));
46
+ }
47
+
48
+ function normalizeBaseUrl(url) {
49
+ return String(url).replace(/\/+$/, '');
50
+ }
51
+
52
+ function buildSpecCandidates(baseUrl) {
53
+ const normalized = normalizeBaseUrl(baseUrl);
54
+ const directLooksLikeSpec = /(openapi|swagger)\.(json|ya?ml)$/i.test(normalized);
55
+
56
+ const candidates = [normalized];
57
+ if (!directLooksLikeSpec) {
58
+ COMMON_SPEC_PATHS.forEach((specPath) => {
59
+ candidates.push(`${normalized}${specPath}`);
60
+ });
61
+ }
62
+
63
+ return [...new Set(candidates)];
64
+ }
65
+
66
+ async function fetchWithTimeout(url, timeoutMs) {
67
+ const controller = new AbortController();
68
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
69
+ try {
70
+ const response = await fetch(url, { signal: controller.signal });
71
+ return response;
72
+ } finally {
73
+ clearTimeout(timer);
74
+ }
75
+ }
76
+
77
+ async function doctor(flags) {
78
+ const checks = [];
79
+
80
+ const nodeVersion = process.versions.node;
81
+ const nodeOk = compareVersions(nodeVersion, '18.0.0') >= 0;
82
+ checks.push(
83
+ makeCheck(
84
+ 'node-version',
85
+ nodeOk,
86
+ `Detected Node.js ${nodeVersion}`,
87
+ nodeOk ? null : 'Upgrade Node.js to v18 or newer.'
88
+ )
89
+ );
90
+
91
+ const fetchOk = typeof fetch === 'function';
92
+ checks.push(
93
+ makeCheck(
94
+ 'fetch-availability',
95
+ fetchOk,
96
+ fetchOk ? 'Global fetch is available.' : 'Global fetch is not available.',
97
+ fetchOk ? null : 'Use Node.js v18+ where fetch is built in.'
98
+ )
99
+ );
100
+
101
+ const cwd = process.cwd();
102
+ const cwdWritable = (() => {
103
+ try {
104
+ fs.accessSync(cwd, fs.constants.W_OK);
105
+ return true;
106
+ } catch (_error) {
107
+ return false;
108
+ }
109
+ })();
110
+ checks.push(
111
+ makeCheck(
112
+ 'cwd-writable',
113
+ cwdWritable,
114
+ `Working directory: ${cwd}`,
115
+ cwdWritable ? null : 'Use a directory where you have write permissions.'
116
+ )
117
+ );
118
+
119
+ if (flags.config) {
120
+ const configPath = path.resolve(process.cwd(), String(flags.config));
121
+ try {
122
+ const config = loadConfig(configPath);
123
+ checks.push(
124
+ makeCheck(
125
+ 'config-validation',
126
+ true,
127
+ `Config valid: ${configPath} (${config.commands.length} commands)`,
128
+ null
129
+ )
130
+ );
131
+ } catch (error) {
132
+ checks.push(
133
+ makeCheck(
134
+ 'config-validation',
135
+ false,
136
+ `Config invalid: ${configPath} (${error.message})`,
137
+ 'Fix the config schema and re-run doctor.'
138
+ )
139
+ );
140
+ }
141
+ }
142
+
143
+ if (flags.spec) {
144
+ const specInput = String(flags.spec);
145
+ try {
146
+ const config = await loadConfigFromOpenApi({
147
+ specInput,
148
+ name: flags.name ? String(flags.name) : undefined,
149
+ version: flags.version ? String(flags.version) : undefined
150
+ });
151
+ checks.push(
152
+ makeCheck(
153
+ 'spec-parse',
154
+ true,
155
+ `Spec parsed: ${specInput} (${config.commands.length} commands)`
156
+ )
157
+ );
158
+ } catch (error) {
159
+ checks.push(
160
+ makeCheck(
161
+ 'spec-parse',
162
+ false,
163
+ `Spec parse failed: ${specInput} (${error.message})`,
164
+ 'Ensure spec is valid OpenAPI 3.x JSON/YAML and accessible.'
165
+ )
166
+ );
167
+ }
168
+ }
169
+
170
+ if (flags.url) {
171
+ const baseUrl = String(flags.url);
172
+ const isUrl = isHttpUrl(baseUrl);
173
+
174
+ if (!isUrl) {
175
+ checks.push(
176
+ makeCheck(
177
+ 'url-format',
178
+ false,
179
+ `URL is not HTTP(S): ${baseUrl}`,
180
+ 'Pass a full base URL like https://api.example.com'
181
+ )
182
+ );
183
+ } else {
184
+ checks.push(makeCheck('url-format', true, `Valid HTTP(S) URL: ${baseUrl}`));
185
+ const candidates = buildSpecCandidates(baseUrl);
186
+ let discovered = null;
187
+
188
+ for (const candidate of candidates) {
189
+ try {
190
+ const response = await fetchWithTimeout(candidate, 4000);
191
+ if (!response.ok) {
192
+ continue;
193
+ }
194
+
195
+ const text = await response.text();
196
+ if (/("openapi"\s*:|^openapi\s*:)/m.test(text)) {
197
+ discovered = candidate;
198
+ break;
199
+ }
200
+ } catch (_error) {
201
+ // continue checking candidates
202
+ }
203
+ }
204
+
205
+ checks.push(
206
+ makeCheck(
207
+ 'openapi-discovery',
208
+ Boolean(discovered),
209
+ discovered
210
+ ? `Discovered OpenAPI candidate: ${discovered}`
211
+ : `No OpenAPI discovered from ${baseUrl}`,
212
+ discovered ? null : 'Run init anyway to generate starter config, then edit endpoints manually.'
213
+ )
214
+ );
215
+ }
216
+ }
217
+
218
+ const ok = checks.every((check) => check.ok);
219
+
220
+ console.log(
221
+ JSON.stringify({
222
+ ok,
223
+ command: 'doctor',
224
+ checks,
225
+ next: ok
226
+ ? 'Environment looks good. Run generate or scaffold.'
227
+ : 'Address failed checks and rerun doctor.'
228
+ })
229
+ );
230
+ }
231
+
232
+ module.exports = {
233
+ doctor
234
+ };
@@ -1,20 +1,14 @@
1
1
  const path = require('path');
2
- const { loadConfig } = require('../lib/load-config');
3
2
  const { generateCliProject } = require('../lib/generate-cli');
3
+ const { resolveConfigInput } = require('../lib/resolve-config-input');
4
4
 
5
5
  async function generate(flags) {
6
- if (!flags.config) {
7
- throw new Error('Missing required flag: --config <path>');
8
- }
9
-
10
6
  if (!flags.output) {
11
7
  throw new Error('Missing required flag: --output <dir>');
12
8
  }
13
9
 
14
- const configPath = path.resolve(process.cwd(), String(flags.config));
15
10
  const outputPath = path.resolve(process.cwd(), String(flags.output));
16
-
17
- const config = loadConfig(configPath);
11
+ const { config, source } = await resolveConfigInput(flags);
18
12
 
19
13
  generateCliProject({
20
14
  config,
@@ -26,6 +20,8 @@ async function generate(flags) {
26
20
  ok: true,
27
21
  command: 'generate',
28
22
  outputPath,
23
+ inputType: source.type,
24
+ input: source.value,
29
25
  generatedCommands: config.commands.map((command) => command.name)
30
26
  })
31
27
  );
@@ -0,0 +1,154 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadConfigFromOpenApi } = require('../lib/openapi-to-config');
4
+
5
+ const COMMON_SPEC_PATHS = [
6
+ '/openapi.json',
7
+ '/openapi.yaml',
8
+ '/openapi.yml',
9
+ '/swagger.json',
10
+ '/v1/openapi.json',
11
+ '/v1/swagger.json',
12
+ '/.well-known/openapi.json'
13
+ ];
14
+
15
+ function normalizeBaseUrl(url) {
16
+ return String(url).replace(/\/+$/, '');
17
+ }
18
+
19
+ function inferNameFromUrl(urlValue) {
20
+ try {
21
+ const host = new URL(urlValue).hostname;
22
+ const label = host.split('.').filter(Boolean)[0] || 'myapi';
23
+ return label.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase();
24
+ } catch (_error) {
25
+ return 'myapi';
26
+ }
27
+ }
28
+
29
+ function buildSpecCandidates(baseUrl) {
30
+ const normalized = normalizeBaseUrl(baseUrl);
31
+ const directLooksLikeSpec = /(openapi|swagger)\.(json|ya?ml)$/i.test(normalized);
32
+
33
+ const candidates = [normalized];
34
+ if (!directLooksLikeSpec) {
35
+ COMMON_SPEC_PATHS.forEach((specPath) => {
36
+ candidates.push(`${normalized}${specPath}`);
37
+ });
38
+ }
39
+
40
+ return [...new Set(candidates)];
41
+ }
42
+
43
+ function renderConfig(config) {
44
+ return `module.exports = ${JSON.stringify(config, null, 2)};\n`;
45
+ }
46
+
47
+ function renderStarterConfig({ name, version, apiBase }) {
48
+ const starter = {
49
+ name,
50
+ version,
51
+ apiBase,
52
+ auth: {
53
+ credentials: [
54
+ {
55
+ envVar: `${name.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`,
56
+ in: 'header',
57
+ name: 'Authorization',
58
+ prefix: 'Bearer '
59
+ }
60
+ ]
61
+ },
62
+ commands: [
63
+ {
64
+ name: 'health',
65
+ description: 'Health check endpoint (edit path/method as needed)',
66
+ method: 'GET',
67
+ path: '/health',
68
+ params: {}
69
+ }
70
+ ]
71
+ };
72
+
73
+ return renderConfig(starter);
74
+ }
75
+
76
+ async function tryOpenApiCandidates({ candidates, name, version }) {
77
+ const failures = [];
78
+
79
+ for (const candidate of candidates) {
80
+ try {
81
+ const config = await loadConfigFromOpenApi({
82
+ specInput: candidate,
83
+ name,
84
+ version
85
+ });
86
+
87
+ return {
88
+ config,
89
+ discoveredSpec: candidate,
90
+ failures
91
+ };
92
+ } catch (error) {
93
+ failures.push({
94
+ candidate,
95
+ message: error.message
96
+ });
97
+ }
98
+ }
99
+
100
+ return {
101
+ config: null,
102
+ discoveredSpec: null,
103
+ failures
104
+ };
105
+ }
106
+
107
+ async function init(flags) {
108
+ if (!flags.url) {
109
+ throw new Error('Missing required flag: --url <api-base-url>');
110
+ }
111
+
112
+ const baseUrl = String(flags.url);
113
+ const outputPath = path.resolve(process.cwd(), String(flags.output || './api-to-cli.config.js'));
114
+ const name = String(flags.name || inferNameFromUrl(baseUrl));
115
+ const version = String(flags.version || '1.0.0');
116
+
117
+ const candidates = buildSpecCandidates(baseUrl);
118
+ const result = await tryOpenApiCandidates({ candidates, name, version });
119
+
120
+ let mode = 'starter';
121
+ let discoveredSpec = null;
122
+ let fileText = renderStarterConfig({
123
+ name,
124
+ version,
125
+ apiBase: normalizeBaseUrl(baseUrl)
126
+ });
127
+
128
+ if (result.config) {
129
+ mode = 'openapi';
130
+ discoveredSpec = result.discoveredSpec;
131
+ fileText = renderConfig(result.config);
132
+ }
133
+
134
+ fs.writeFileSync(outputPath, fileText, 'utf8');
135
+
136
+ console.log(
137
+ JSON.stringify({
138
+ ok: true,
139
+ command: 'init',
140
+ mode,
141
+ baseUrl: normalizeBaseUrl(baseUrl),
142
+ outputPath,
143
+ discoveredSpec,
144
+ attemptedSpecCandidates: candidates,
145
+ hint: mode === 'openapi'
146
+ ? 'OpenAPI discovered. You can run: api-to-cli generate --config <outputPath> --output <dir>'
147
+ : 'No OpenAPI discovered. Edit the generated config and then run generate.'
148
+ })
149
+ );
150
+ }
151
+
152
+ module.exports = {
153
+ init
154
+ };
@@ -1,9 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { loadConfig } = require('../lib/load-config');
4
3
  const { generateCliProject } = require('../lib/generate-cli');
5
4
  const { writeSkillPackage } = require('../lib/generate-skill');
6
5
  const { buildManifest, writeManifest } = require('../lib/generate-manifest');
6
+ const { resolveConfigInput } = require('../lib/resolve-config-input');
7
7
 
8
8
  function hasOwn(obj, key) {
9
9
  return Object.prototype.hasOwnProperty.call(obj, key);
@@ -31,7 +31,8 @@ function writeScaffoldReadme(outputPath, data) {
31
31
  '# AgentBridge Scaffold Output',
32
32
  '',
33
33
  '## Contents',
34
- `- CLI project: ${data.cliProjectPath}`
34
+ `- CLI project: ${data.cliProjectPath}`,
35
+ `- Input source: ${data.inputType} (${data.inputValue})`
35
36
  ];
36
37
 
37
38
  if (data.skillPath) {
@@ -48,10 +49,6 @@ function writeScaffoldReadme(outputPath, data) {
48
49
  }
49
50
 
50
51
  async function scaffold(flags) {
51
- if (!flags.config) {
52
- throw new Error('Missing required flag: --config <path>');
53
- }
54
-
55
52
  if (!flags.output) {
56
53
  throw new Error('Missing required flag: --output <dir>');
57
54
  }
@@ -61,11 +58,10 @@ async function scaffold(flags) {
61
58
  const skillRelativePath = './skill/SKILL.md';
62
59
  const manifestRelativePath = './agentbridge.manifest.json';
63
60
 
64
- const configPath = path.resolve(process.cwd(), String(flags.config));
65
61
  const outputPath = path.resolve(process.cwd(), String(flags.output));
66
62
  const cliProjectPath = path.join(outputPath, 'cli');
63
+ const { config, source } = await resolveConfigInput(flags);
67
64
 
68
- const config = loadConfig(configPath);
69
65
  fs.mkdirSync(outputPath, { recursive: true });
70
66
 
71
67
  generateCliProject({
@@ -89,7 +85,9 @@ async function scaffold(flags) {
89
85
  writeScaffoldReadme(outputPath, {
90
86
  cliProjectPath: cliRelativePath,
91
87
  skillPath,
92
- manifestPath
88
+ manifestPath,
89
+ inputType: source.type,
90
+ inputValue: source.value
93
91
  });
94
92
 
95
93
  console.log(
@@ -97,6 +95,8 @@ async function scaffold(flags) {
97
95
  ok: true,
98
96
  command: 'scaffold',
99
97
  outputPath,
98
+ inputType: source.type,
99
+ input: source.value,
100
100
  cliProjectPath: cliRelativePath,
101
101
  skillPath,
102
102
  manifestPath,
@@ -1,25 +1,21 @@
1
- const path = require('path');
2
- const { loadConfig } = require('../lib/load-config');
1
+ const { resolveConfigInput } = require('../lib/resolve-config-input');
3
2
 
4
3
  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);
4
+ const { config, source } = await resolveConfigInput(flags);
11
5
 
12
6
  console.log(
13
7
  JSON.stringify({
14
8
  ok: true,
15
9
  command: 'validate',
16
- configPath,
10
+ inputType: source.type,
11
+ input: source.value,
17
12
  summary: {
18
13
  name: config.name,
19
14
  version: config.version,
20
15
  apiBase: config.apiBase,
21
16
  commandCount: config.commands.length,
22
- hasAuth: Boolean(config.auth && config.auth.credentials && config.auth.credentials.length)
17
+ hasAuth: Boolean(config.auth && config.auth.credentials && config.auth.credentials.length),
18
+ methods: [...new Set(config.commands.map((command) => command.method))]
23
19
  }
24
20
  })
25
21
  );
package/src/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ const { doctor } = require('./commands/doctor');
1
2
  const { generate } = require('./commands/generate');
3
+ const { init } = require('./commands/init');
2
4
  const { validate } = require('./commands/validate');
3
5
  const { scaffold } = require('./commands/scaffold');
4
6
 
@@ -6,13 +8,17 @@ function printUsage() {
6
8
  console.error(
7
9
  [
8
10
  '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]',
11
+ ' api-to-cli doctor [--config <path>] [--spec <path-or-url>] [--url <api-base-url>]',
12
+ ' api-to-cli init --url <api-base-url> [--output <path>] [--name <cli-name>] [--version <semver>]',
13
+ ' api-to-cli generate (--config <path> | --spec <path-or-url>) --output <dir> [--name <cli-name>]',
14
+ ' api-to-cli validate (--config <path> | --spec <path-or-url>) [--name <cli-name>]',
15
+ ' api-to-cli scaffold (--config <path> | --spec <path-or-url>) --output <dir> [--name <cli-name>] [--with-skill] [--with-manifest]',
12
16
  '',
13
17
  'Commands:',
14
- ' generate Generate a CLI from config',
15
- ' validate Validate config only',
18
+ ' doctor Run environment and input diagnostics',
19
+ ' init Discover OpenAPI and generate starter config',
20
+ ' generate Generate a CLI from config/spec',
21
+ ' validate Validate config/spec only',
16
22
  ' scaffold Generate CLI + optional skill/manifest bundle'
17
23
  ].join('\n')
18
24
  );
@@ -54,6 +60,16 @@ async function run(argv) {
54
60
  try {
55
61
  const flags = parseFlags(rest);
56
62
 
63
+ if (command === 'doctor') {
64
+ await doctor(flags);
65
+ return;
66
+ }
67
+
68
+ if (command === 'init') {
69
+ await init(flags);
70
+ return;
71
+ }
72
+
57
73
  if (command === 'generate') {
58
74
  await generate(flags);
59
75
  return;