@assistkick/create 1.18.0 → 1.21.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 (30) hide show
  1. package/package.json +1 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/mcp_config.ts +89 -0
  3. package/templates/assistkick-product-system/packages/backend/src/server.ts +8 -1
  4. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +7 -0
  5. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +26 -1
  6. package/templates/assistkick-product-system/packages/backend/src/services/mcp_config_service.ts +134 -0
  7. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +34 -0
  8. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +14 -1
  9. package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx +208 -0
  10. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_superb_roxanne_simpson.sql +8 -0
  11. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +1019 -23
  12. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +7 -0
  13. package/templates/assistkick-product-system/packages/shared/db/schema.ts +10 -0
  14. package/templates/assistkick-product-system/packages/shared/lib/app_use_flow.ts +484 -0
  15. package/templates/assistkick-product-system/packages/shared/lib/openapi.ts +146 -0
  16. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  17. package/templates/assistkick-product-system/packages/shared/tools/agent_builder.ts +341 -0
  18. package/templates/assistkick-product-system/packages/shared/tools/app_use_record.ts +268 -0
  19. package/templates/assistkick-product-system/packages/shared/tools/app_use_run.ts +348 -0
  20. package/templates/assistkick-product-system/packages/shared/tools/app_use_validate.ts +67 -0
  21. package/templates/assistkick-product-system/packages/shared/tools/openapi_describe.ts +59 -0
  22. package/templates/assistkick-product-system/packages/shared/tools/openapi_list.ts +69 -0
  23. package/templates/assistkick-product-system/packages/shared/tools/openapi_schema.ts +67 -0
  24. package/templates/assistkick-product-system/packages/shared/tools/workflow_builder.ts +754 -0
  25. package/templates/skills/assistkick-agent-builder/SKILL.md +168 -0
  26. package/templates/skills/assistkick-app-use/SKILL.md +296 -0
  27. package/templates/skills/assistkick-app-use/references/agent-browser.md +1156 -0
  28. package/templates/skills/assistkick-openapi-explorer/SKILL.md +78 -0
  29. package/templates/skills/assistkick-openapi-explorer/cache/.gitignore +2 -0
  30. package/templates/skills/assistkick-workflow-builder/SKILL.md +234 -0
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * app_use_run — Execute a YAML flow file against a target platform.
5
+ *
6
+ * Parses the flow, translates each step to agent-browser commands (for web),
7
+ * executes them sequentially, reports pass/fail per step with timing.
8
+ *
9
+ * Usage:
10
+ * pnpm tsx packages/shared/tools/app_use_run.ts <flow.yaml> [--env KEY=VALUE] [--headed] [--platform web]
11
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type click --selector "@e1"
12
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type fill --selector "@e2" --value "hello"
13
+ * pnpm tsx packages/shared/tools/app_use_run.ts exec --type snapshot --params '{"interactive":true}'
14
+ * pnpm tsx packages/shared/tools/app_use_run.ts assert-ai <screenshot-path> --prompt "The login form is visible"
15
+ */
16
+
17
+ import {program} from 'commander';
18
+ import chalk from 'chalk';
19
+ import {execSync, spawn} from 'node:child_process';
20
+ import {
21
+ type FlowResult,
22
+ type FlowStep,
23
+ interpolate,
24
+ readFlowFile,
25
+ type StepResult,
26
+ stepToAgentBrowserArgs,
27
+ validateFlow,
28
+ } from '../lib/app_use_flow.js';
29
+
30
+ // ── Helpers ───────────────────────────────────────────────────────────────────
31
+
32
+ function runAgentBrowser(args: string[], timeoutMs = 30_000): { stdout: string; exitCode: number } {
33
+ const cmd = `agent-browser ${args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
34
+ try {
35
+ const stdout = execSync(cmd, {
36
+ timeout: timeoutMs,
37
+ encoding: 'utf-8',
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ });
40
+ return { stdout: stdout.trim(), exitCode: 0 };
41
+ } catch (err: any) {
42
+ return {
43
+ stdout: err.stdout?.toString()?.trim() || '',
44
+ exitCode: err.status ?? 1,
45
+ };
46
+ }
47
+ }
48
+
49
+ function assertVisibleInSnapshot(snapshotOutput: string, text: string): boolean {
50
+ // Check if the text appears in the accessibility snapshot
51
+ return snapshotOutput.toLowerCase().includes(text.toLowerCase());
52
+ }
53
+
54
+ async function assertWithAI(screenshotPath: string, prompt: string): Promise<{ passes: boolean; details: string }> {
55
+ const systemPrompt = [
56
+ 'You are a UI test assertion engine. You analyze screenshots to verify assertions.',
57
+ 'Output ONLY valid JSON with these fields:',
58
+ '{"passes": boolean, "confidence": "high"|"medium"|"low", "details": "brief explanation"}',
59
+ 'No other text, no code fences.',
60
+ ].join(' ');
61
+
62
+ const fullPrompt = `Look at the screenshot and determine: ${prompt}`;
63
+
64
+ return new Promise((resolve) => {
65
+ const args = [
66
+ '-p', fullPrompt,
67
+ '--system-prompt', systemPrompt,
68
+ '--model', 'claude-haiku-4-5',
69
+ '--output-format', 'text',
70
+ '--max-turns', '1',
71
+ '--dangerously-skip-permissions',
72
+ screenshotPath,
73
+ ];
74
+
75
+ const child = spawn('claude', args, {
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ env: { ...process.env },
78
+ timeout: 30_000,
79
+ });
80
+
81
+ let stdout = '';
82
+ child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
83
+
84
+ child.on('close', (code) => {
85
+ if (code === 0 && stdout.trim()) {
86
+ try {
87
+ // Try parsing JSON directly
88
+ let jsonStr = stdout.trim();
89
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
90
+ if (fenceMatch) jsonStr = fenceMatch[1].trim();
91
+ const result = JSON.parse(jsonStr);
92
+ resolve({ passes: !!result.passes, details: result.details || '' });
93
+ } catch {
94
+ // If JSON parsing fails, check for positive keywords
95
+ const lower = stdout.toLowerCase();
96
+ const passes = lower.includes('"passes": true') || lower.includes('"passes":true');
97
+ resolve({ passes, details: stdout.trim() });
98
+ }
99
+ } else {
100
+ resolve({ passes: false, details: `AI assertion failed (exit code: ${code})` });
101
+ }
102
+ });
103
+
104
+ child.on('error', () => {
105
+ resolve({ passes: false, details: 'Failed to spawn Claude CLI for AI assertion' });
106
+ });
107
+ });
108
+ }
109
+
110
+ // ── Single step execution (for agent interactive use) ─────────────────────────
111
+
112
+ async function execSingleStep(opts: any): Promise<void> {
113
+ const step: FlowStep = {
114
+ type: opts.type,
115
+ selector: opts.selector,
116
+ value: opts.value,
117
+ params: opts.params ? JSON.parse(opts.params) : undefined,
118
+ };
119
+
120
+ const args = stepToAgentBrowserArgs(step);
121
+ const { stdout, exitCode } = runAgentBrowser(args);
122
+
123
+ if (exitCode === 0) {
124
+ console.log(stdout);
125
+ console.log('\n' + JSON.stringify({ status: 'ok', step: step.type, output: stdout }));
126
+ } else {
127
+ console.error(chalk.red(`Step failed: ${step.type}`));
128
+ if (stdout) console.error(stdout);
129
+ console.log('\n' + JSON.stringify({ status: 'error', step: step.type, output: stdout }));
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ // ── Flow execution ────────────────────────────────────────────────────────────
135
+
136
+ async function runFlow(flowPath: string, opts: any): Promise<void> {
137
+ const flow = readFlowFile(flowPath);
138
+
139
+ // Validate first
140
+ const issues = validateFlow(flow);
141
+ if (issues.length > 0) {
142
+ console.error(chalk.red('Flow validation failed:'));
143
+ for (const issue of issues) console.error(chalk.red(` - ${issue}`));
144
+ process.exit(1);
145
+ }
146
+
147
+ // Merge env from CLI and flow config
148
+ const env: Record<string, string> = { ...flow.config.env };
149
+ if (opts.env) {
150
+ for (const e of Array.isArray(opts.env) ? opts.env : [opts.env]) {
151
+ const [k, ...rest] = e.split('=');
152
+ if (k) env[k] = rest.join('=');
153
+ }
154
+ }
155
+
156
+ const platform = opts.platform || flow.config.platform || 'web';
157
+ if (platform !== 'web') {
158
+ console.error(chalk.red(`Platform "${platform}" is not yet supported. Only "web" is available.`));
159
+ process.exit(1);
160
+ }
161
+
162
+ const flowName = flow.config.name || flowPath;
163
+ console.log(chalk.cyan.bold(`\nRunning flow: ${flowName}`));
164
+ console.log(`Platform: ${platform} | Steps: ${flow.steps.length}`);
165
+ if (opts.headed) console.log('Mode: headed');
166
+ console.log();
167
+
168
+ // Auto-session: open browser if configured
169
+ if (flow.config.autoSession && flow.config.appId) {
170
+ const headedArgs = opts.headed ? ['--headed'] : [];
171
+ const openArgs = ['open', interpolate(flow.config.appId, env), ...headedArgs];
172
+ console.log(chalk.gray(` [setup] agent-browser ${openArgs.join(' ')}`));
173
+ runAgentBrowser(openArgs);
174
+ }
175
+
176
+ const results: StepResult[] = [];
177
+ const startTime = Date.now();
178
+ let aborted = false;
179
+
180
+ for (let i = 0; i < flow.steps.length; i++) {
181
+ if (aborted) {
182
+ results.push({
183
+ step: flow.steps[i], index: i, status: 'skipped', durationMs: 0,
184
+ });
185
+ continue;
186
+ }
187
+
188
+ const step = flow.steps[i];
189
+ const stepStart = Date.now();
190
+ const label = step.label || `${step.type}${step.value ? ` ${step.value}` : ''}${step.selector ? ` ${step.selector}` : ''}`;
191
+
192
+ try {
193
+ // Handle assertion steps specially
194
+ if (step.type === 'assertVisible' || step.type === 'assertNotVisible') {
195
+ const { stdout } = runAgentBrowser(['snapshot', '-i']);
196
+ const text = step.value || '';
197
+ const found = assertVisibleInSnapshot(stdout, text);
198
+ const shouldBeVisible = step.type === 'assertVisible';
199
+
200
+ if (found === shouldBeVisible) {
201
+ const ms = Date.now() - stepStart;
202
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
203
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
204
+ } else {
205
+ throw new Error(`Expected "${text}" to be ${shouldBeVisible ? 'visible' : 'not visible'}`);
206
+ }
207
+ continue;
208
+ }
209
+
210
+ if (step.type === 'assertUrl') {
211
+ const { stdout } = runAgentBrowser(['get', 'url']);
212
+ const expected = step.value ? interpolate(step.value, env) : '';
213
+ if (!stdout.includes(expected)) {
214
+ throw new Error(`URL assertion failed: expected "${expected}" in "${stdout}"`);
215
+ }
216
+ const ms = Date.now() - stepStart;
217
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
218
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
219
+ continue;
220
+ }
221
+
222
+ if (step.type === 'assertText') {
223
+ const sel = step.selector ? interpolate(step.selector, env) : '';
224
+ const { stdout } = runAgentBrowser(['get', 'text', sel]);
225
+ const expected = step.value ? interpolate(step.value, env) : '';
226
+ if (!stdout.includes(expected)) {
227
+ throw new Error(`Text assertion failed: expected "${expected}" in "${stdout}"`);
228
+ }
229
+ const ms = Date.now() - stepStart;
230
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
231
+ results.push({ step, index: i, status: 'passed', durationMs: ms });
232
+ continue;
233
+ }
234
+
235
+ if (step.type === 'assertWithAI') {
236
+ const screenshotResult = runAgentBrowser(['screenshot']);
237
+ // Extract screenshot path from agent-browser output
238
+ const pathMatch = screenshotResult.stdout.match(/(?:saved to |Screenshot: ?)(.+\.png)/i);
239
+ const screenshotPath = pathMatch?.[1]?.trim();
240
+ if (!screenshotPath) {
241
+ throw new Error('Could not capture screenshot for AI assertion');
242
+ }
243
+ const aiResult = await assertWithAI(screenshotPath, step.value || '');
244
+ if (aiResult.passes) {
245
+ const ms = Date.now() - stepStart;
246
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms) — ${aiResult.details}`));
247
+ results.push({ step, index: i, status: 'passed', durationMs: ms, output: aiResult.details });
248
+ } else {
249
+ throw new Error(`AI assertion failed: ${aiResult.details}`);
250
+ }
251
+ continue;
252
+ }
253
+
254
+ // Normal step: translate to agent-browser command and run
255
+ const args = stepToAgentBrowserArgs(step, env);
256
+ const { stdout, exitCode } = runAgentBrowser(args);
257
+
258
+ if (exitCode !== 0) {
259
+ throw new Error(stdout || `agent-browser exited with code ${exitCode}`);
260
+ }
261
+
262
+ const ms = Date.now() - stepStart;
263
+ console.log(chalk.green(` [${i + 1}/${flow.steps.length}] PASS ${label} (${ms}ms)`));
264
+ results.push({ step, index: i, status: 'passed', durationMs: ms, output: stdout });
265
+
266
+ } catch (err) {
267
+ const ms = Date.now() - stepStart;
268
+ const errMsg = (err as Error).message;
269
+
270
+ if (step.optional) {
271
+ console.log(chalk.yellow(` [${i + 1}/${flow.steps.length}] SKIP ${label} (optional, ${ms}ms) — ${errMsg}`));
272
+ results.push({ step, index: i, status: 'skipped', durationMs: ms, error: errMsg });
273
+ } else {
274
+ console.log(chalk.red(` [${i + 1}/${flow.steps.length}] FAIL ${label} (${ms}ms) — ${errMsg}`));
275
+ results.push({ step, index: i, status: 'failed', durationMs: ms, error: errMsg });
276
+ aborted = true;
277
+ }
278
+ }
279
+ }
280
+
281
+ // Auto-session: close browser
282
+ if (flow.config.autoSession) {
283
+ runAgentBrowser(['close']);
284
+ }
285
+
286
+ // Summary
287
+ const totalMs = Date.now() - startTime;
288
+ const passed = results.filter(r => r.status === 'passed').length;
289
+ const failed = results.filter(r => r.status === 'failed').length;
290
+ const skipped = results.filter(r => r.status === 'skipped').length;
291
+ const status = failed === 0 ? 'passed' : 'failed';
292
+
293
+ console.log();
294
+ const statusColor = status === 'passed' ? chalk.green : chalk.red;
295
+ console.log(statusColor.bold(`${status.toUpperCase()}: ${passed} passed, ${failed} failed, ${skipped} skipped (${totalMs}ms)`));
296
+
297
+ const result: FlowResult = {
298
+ flowName, platform, status, totalSteps: flow.steps.length,
299
+ passed, failed, skipped, durationMs: totalMs, steps: results,
300
+ };
301
+ console.log('\n' + JSON.stringify(result));
302
+
303
+ if (failed > 0) process.exit(1);
304
+ }
305
+
306
+ // ── CLI ───────────────────────────────────────────────────────────────────────
307
+
308
+ program
309
+ .argument('<action>', 'Flow file path, or "exec" for single step, or "assert-ai" for AI assertion')
310
+ .argument('[target]', 'Screenshot path (for assert-ai)')
311
+ .option('--env <pairs...>', 'Environment variables (KEY=VALUE)')
312
+ .option('--headed', 'Run browser in headed mode')
313
+ .option('--platform <platform>', 'Override platform (web, ios, android)')
314
+ .option('--type <type>', 'Step type (for exec action)')
315
+ .option('--selector <sel>', 'Element selector (for exec action)')
316
+ .option('--value <val>', 'Step value (for exec action)')
317
+ .option('--params <json>', 'Additional params as JSON (for exec action)')
318
+ .option('--prompt <text>', 'Assertion prompt (for assert-ai action)')
319
+ .parse();
320
+
321
+ const [action, target] = program.args;
322
+ const opts = program.opts();
323
+
324
+ (async () => {
325
+ try {
326
+ if (action === 'exec') {
327
+ if (!opts.type) throw new Error('--type is required for exec action');
328
+ await execSingleStep(opts);
329
+ } else if (action === 'assert-ai') {
330
+ if (!target) throw new Error('Screenshot path is required for assert-ai');
331
+ if (!opts.prompt) throw new Error('--prompt is required for assert-ai');
332
+ const result = await assertWithAI(target, opts.prompt);
333
+ if (result.passes) {
334
+ console.log(chalk.green(`PASS: ${result.details}`));
335
+ } else {
336
+ console.log(chalk.red(`FAIL: ${result.details}`));
337
+ }
338
+ console.log(JSON.stringify(result));
339
+ if (!result.passes) process.exit(1);
340
+ } else {
341
+ // Treat action as a flow file path
342
+ await runFlow(action, opts);
343
+ }
344
+ } catch (err) {
345
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
346
+ process.exit(1);
347
+ }
348
+ })();
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * app_use_validate — Validate a YAML flow file without executing it.
5
+ *
6
+ * Usage:
7
+ * pnpm tsx packages/shared/tools/app_use_validate.ts <flow.yaml>
8
+ * pnpm tsx packages/shared/tools/app_use_validate.ts <flow.yaml> --show-steps
9
+ */
10
+
11
+ import { program } from 'commander';
12
+ import chalk from 'chalk';
13
+ import { readFlowFile, validateFlow } from '../lib/app_use_flow.js';
14
+
15
+ program
16
+ .argument('<flow>', 'Path to flow YAML file')
17
+ .option('--show-steps', 'Show parsed steps for debugging')
18
+ .parse();
19
+
20
+ const [flowPath] = program.args;
21
+ const opts = program.opts();
22
+
23
+ (async () => {
24
+ try {
25
+ const flow = readFlowFile(flowPath);
26
+ const issues = validateFlow(flow);
27
+
28
+ console.log(chalk.cyan.bold(`\nFlow: ${flow.config.name || flowPath}\n`));
29
+ console.log(` Platform: ${flow.config.platform || 'web'}`);
30
+ if (flow.config.appId) console.log(` App: ${flow.config.appId}`);
31
+ console.log(` Steps: ${flow.steps.length}`);
32
+ console.log(` Auto-session: ${flow.config.autoSession !== false}`);
33
+ if (flow.config.env) {
34
+ console.log(` Env vars: ${Object.keys(flow.config.env).join(', ')}`);
35
+ }
36
+ console.log();
37
+
38
+ if (opts.showSteps) {
39
+ console.log(chalk.cyan(' Steps:'));
40
+ for (let i = 0; i < flow.steps.length; i++) {
41
+ const s = flow.steps[i];
42
+ const parts: string[] = [s.type];
43
+ if (s.selector) parts.push(s.selector);
44
+ if (s.value) parts.push(`"${s.value}"`);
45
+ if (s.optional) parts.push('(optional)');
46
+ if (s.label) parts.push(`— ${s.label}`);
47
+ console.log(` ${i + 1}. ${parts.join(' ')}`);
48
+ }
49
+ console.log();
50
+ }
51
+
52
+ if (issues.length === 0) {
53
+ console.log(chalk.green.bold('Valid flow — no issues found'));
54
+ console.log(JSON.stringify({ valid: true, steps: flow.steps.length, issues: [] }));
55
+ } else {
56
+ console.log(chalk.yellow.bold(`Found ${issues.length} issue(s):`));
57
+ for (const issue of issues) {
58
+ console.log(chalk.yellow(` - ${issue}`));
59
+ }
60
+ console.log('\n' + JSON.stringify({ valid: false, steps: flow.steps.length, issues }));
61
+ process.exit(1);
62
+ }
63
+ } catch (err) {
64
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
65
+ process.exit(1);
66
+ }
67
+ })();
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * openapi_describe — Show full details of a specific API endpoint with all $ref references inlined.
5
+ *
6
+ * Usage:
7
+ * pnpm tsx packages/shared/tools/openapi_describe.ts <SPEC_PATH_OR_URL> <METHOD> <PATH>
8
+ *
9
+ * Example:
10
+ * pnpm tsx packages/shared/tools/openapi_describe.ts docs/api-spec.json GET /auth/verify
11
+ */
12
+
13
+ import { program } from 'commander';
14
+ import chalk from 'chalk';
15
+ import { loadSpec, inlineRefs } from '../lib/openapi.js';
16
+
17
+ program
18
+ .argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
19
+ .argument('<method>', 'HTTP method (GET, POST, PUT, DELETE, etc.)')
20
+ .argument('<path>', 'API path (e.g. /auth/verify)')
21
+ .parse();
22
+
23
+ const [specSource, methodArg, pathArg] = program.args;
24
+
25
+ (async () => {
26
+ try {
27
+ const spec = await loadSpec(specSource);
28
+ const paths = spec.paths ?? {};
29
+ const method = methodArg.toLowerCase();
30
+
31
+ if (!(pathArg in paths)) {
32
+ console.error(chalk.red(`Path '${pathArg}' not found. Available paths:`));
33
+ for (const p of Object.keys(paths).sort()) {
34
+ console.log(` ${p}`);
35
+ }
36
+ process.exit(1);
37
+ }
38
+
39
+ if (!(method in paths[pathArg])) {
40
+ const available = Object.keys(paths[pathArg]).map(m => m.toUpperCase());
41
+ console.error(chalk.red(`Method '${methodArg.toUpperCase()}' not found for '${pathArg}'. Available: ${available.join(', ')}`));
42
+ process.exit(1);
43
+ }
44
+
45
+ const endpoint = paths[pathArg][method];
46
+ const inlined = inlineRefs(spec, endpoint);
47
+
48
+ console.log(chalk.cyan.bold(`## ${methodArg.toUpperCase()} ${pathArg}`));
49
+ console.log();
50
+ if (endpoint.tags) console.log(`Tags: ${endpoint.tags.join(', ')}`);
51
+ if (endpoint.summary) console.log(`Summary: ${endpoint.summary}`);
52
+ if (endpoint.description) console.log(`Description: ${endpoint.description}`);
53
+ console.log();
54
+ console.log(JSON.stringify(inlined, null, 2));
55
+ } catch (err) {
56
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
57
+ process.exit(1);
58
+ }
59
+ })();
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * openapi_list — List all API endpoints from an OpenAPI spec grouped by tag.
5
+ *
6
+ * Usage:
7
+ * pnpm tsx packages/shared/tools/openapi_list.ts <SPEC_PATH_OR_URL>
8
+ *
9
+ * Example (local): pnpm tsx packages/shared/tools/openapi_list.ts docs/api-spec.json
10
+ * Example (URL): pnpm tsx packages/shared/tools/openapi_list.ts https://example.com/openapi.json
11
+ */
12
+
13
+ import { program } from 'commander';
14
+ import chalk from 'chalk';
15
+ import { loadSpec } from '../lib/openapi.js';
16
+
17
+ program
18
+ .argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
19
+ .parse();
20
+
21
+ const [specSource] = program.args;
22
+
23
+ (async () => {
24
+ try {
25
+ const spec = await loadSpec(specSource);
26
+ const paths = spec.paths ?? {};
27
+
28
+ const title = spec.info?.title ?? 'N/A';
29
+ const version = spec.info?.version ?? 'N/A';
30
+ const totalEndpoints = Object.values(paths).reduce(
31
+ (sum: number, methods: any) => sum + Object.keys(methods).length,
32
+ 0,
33
+ );
34
+ const totalSchemas = Object.keys(spec.components?.schemas ?? {}).length;
35
+
36
+ console.log(`API: ${chalk.bold(title)}`);
37
+ console.log(`Version: ${version}`);
38
+ console.log(`Total endpoints: ${chalk.green(String(totalEndpoints))}`);
39
+ console.log(`Total schemas: ${chalk.green(String(totalSchemas))}`);
40
+ console.log();
41
+
42
+ // Group by tag
43
+ const tagged: Record<string, Array<[string, string, string]>> = {};
44
+
45
+ for (const [path, methods] of Object.entries(paths).sort()) {
46
+ for (const [method, ep] of Object.entries(methods as Record<string, any>).sort()) {
47
+ const tags: string[] = ep.tags ?? ['Untagged'];
48
+ const summary = (ep.summary ?? ep.description ?? '').split('\n')[0].slice(0, 80);
49
+ for (const tag of tags) {
50
+ if (!tagged[tag]) tagged[tag] = [];
51
+ tagged[tag].push([method.toUpperCase(), path, summary]);
52
+ }
53
+ }
54
+ }
55
+
56
+ for (const tag of Object.keys(tagged).sort()) {
57
+ console.log(chalk.cyan.bold(`## ${tag}`));
58
+ for (const [method, path, summary] of tagged[tag]) {
59
+ let line = ` ${method.padEnd(7)} ${path}`;
60
+ if (summary) line += ` — ${summary}`;
61
+ console.log(line);
62
+ }
63
+ console.log();
64
+ }
65
+ } catch (err) {
66
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
67
+ process.exit(1);
68
+ }
69
+ })();
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * openapi_schema — Get a named schema and all its referenced child schemas from an OpenAPI spec.
5
+ *
6
+ * Usage:
7
+ * pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> <SCHEMA_NAME>
8
+ * pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> --list
9
+ *
10
+ * Example:
11
+ * pnpm tsx packages/shared/tools/openapi_schema.ts docs/api-spec.json User
12
+ * pnpm tsx packages/shared/tools/openapi_schema.ts docs/api-spec.json --list
13
+ */
14
+
15
+ import { program } from 'commander';
16
+ import chalk from 'chalk';
17
+ import { loadSpec, resolveSchemaTree } from '../lib/openapi.js';
18
+
19
+ program
20
+ .argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
21
+ .argument('[schema]', 'Schema name (or use --list to see all)')
22
+ .option('--list', 'List all available schema names')
23
+ .parse();
24
+
25
+ const [specSource, schemaArg] = program.args;
26
+ const opts = program.opts();
27
+
28
+ (async () => {
29
+ try {
30
+ const spec = await loadSpec(specSource);
31
+ const schemas = spec.components?.schemas ?? {};
32
+
33
+ if (opts.list || schemaArg === '--list') {
34
+ console.log(chalk.cyan.bold(`Available schemas (${Object.keys(schemas).length}):`));
35
+ for (const name of Object.keys(schemas).sort()) {
36
+ const props = schemas[name].properties ?? {};
37
+ const propNames = Object.keys(props).slice(0, 5);
38
+ let suffix = propNames.length > 0 ? ` — fields: ${propNames.join(', ')}` : '';
39
+ if (Object.keys(props).length > 5) {
40
+ suffix += `, ... (+${Object.keys(props).length - 5} more)`;
41
+ }
42
+ console.log(` ${chalk.bold(name)}${suffix}`);
43
+ }
44
+ return;
45
+ }
46
+
47
+ if (!schemaArg) {
48
+ console.error(chalk.red('Provide a schema name or use --list to see available schemas.'));
49
+ process.exit(1);
50
+ }
51
+
52
+ if (!(schemaArg in schemas)) {
53
+ console.error(chalk.red(`Schema '${schemaArg}' not found. Use --list to see available schemas.`));
54
+ process.exit(1);
55
+ }
56
+
57
+ const tree = resolveSchemaTree(spec, schemaArg);
58
+
59
+ console.log(chalk.cyan.bold(`## Schema: ${schemaArg}`));
60
+ console.log(`Includes ${Object.keys(tree).length} schema(s): ${Object.keys(tree).join(', ')}`);
61
+ console.log();
62
+ console.log(JSON.stringify(tree, null, 2));
63
+ } catch (err) {
64
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
65
+ process.exit(1);
66
+ }
67
+ })();