spaps 0.5.0 → 0.5.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/AI_TOOLS.json +114 -0
- package/README.md +204 -38
- package/bin/spaps.js +5 -312
- package/package.json +10 -6
- package/src/ai-helper.js +20 -20
- package/src/ai-tool-spec.js +298 -0
- package/src/cli-dispatcher.js +233 -0
- package/src/config.js +5 -0
- package/src/docs-html.js +3 -2
- package/src/docs-system.js +78 -129
- package/src/doctor.js +217 -0
- package/src/handlers.js +174 -0
- package/src/help-system.js +5 -3
- package/src/local-server.js +181 -16
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tool Spec generator for SPAPS
|
|
3
|
+
* - Produces OpenAI-style function schemas for common SPAPS actions
|
|
4
|
+
* - Keeps defaults safe for local development (no API key required)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { DEFAULT_PORT } = require('./config');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
function tryLoadManifest() {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.resolve(process.cwd(), 'docs/manifest.json'),
|
|
14
|
+
path.resolve(__dirname, '../../../docs/manifest.json')
|
|
15
|
+
];
|
|
16
|
+
for (const p of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(p)) {
|
|
19
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tryLoadOpenAPI() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.resolve(process.cwd(), 'docs/api-reference.yaml'),
|
|
30
|
+
path.resolve(__dirname, '../../../docs/api-reference.yaml')
|
|
31
|
+
];
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(p)) {
|
|
35
|
+
const yaml = require('js-yaml');
|
|
36
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
37
|
+
return yaml.load(raw);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildOpenAIToolSpec({ port = DEFAULT_PORT } = {}) {
|
|
45
|
+
const baseUrl = `http://localhost:${port}`;
|
|
46
|
+
const spec = {
|
|
47
|
+
name: 'spaps',
|
|
48
|
+
version: '0.1.0',
|
|
49
|
+
description: 'Auth + payments via SPAPS (local by default). Start with: npx spaps local',
|
|
50
|
+
base_url: baseUrl,
|
|
51
|
+
auth: {
|
|
52
|
+
local_mode: true,
|
|
53
|
+
production: {
|
|
54
|
+
header: 'X-API-Key',
|
|
55
|
+
env: 'SPAPS_API_KEY'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
tools: [
|
|
59
|
+
{
|
|
60
|
+
name: 'login',
|
|
61
|
+
description: 'Login with email/password. Local mode accepts any values.',
|
|
62
|
+
method: 'POST',
|
|
63
|
+
path: '/api/auth/login',
|
|
64
|
+
parameters: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
required: ['email', 'password'],
|
|
67
|
+
properties: {
|
|
68
|
+
email: { type: 'string', description: 'Email address' },
|
|
69
|
+
password: { type: 'string', description: 'Plain text password' }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'register',
|
|
75
|
+
description: 'Register a new user with email/password.',
|
|
76
|
+
method: 'POST',
|
|
77
|
+
path: '/api/auth/register',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
required: ['email', 'password'],
|
|
81
|
+
properties: {
|
|
82
|
+
email: { type: 'string' },
|
|
83
|
+
password: { type: 'string' }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'get_current_user',
|
|
89
|
+
description: 'Get the currently authenticated user. Uses bearer token from previous login.',
|
|
90
|
+
method: 'GET',
|
|
91
|
+
path: '/api/auth/user',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
authorization: { type: 'string', description: 'Bearer <access_token>' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'create_checkout_session',
|
|
101
|
+
description: 'Create a Stripe Checkout session. In local mode uses Stripe test or mock based on USE_REAL_STRIPE.',
|
|
102
|
+
method: 'POST',
|
|
103
|
+
path: '/api/stripe/checkout-sessions',
|
|
104
|
+
parameters: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
required: ['success_url', 'cancel_url'],
|
|
107
|
+
properties: {
|
|
108
|
+
price_id: { type: 'string', description: 'Existing Stripe price ID (preferred)' },
|
|
109
|
+
product_name: { type: 'string', description: 'Used when price_id not provided' },
|
|
110
|
+
amount: { type: 'number', description: 'Amount in cents if creating ad-hoc price' },
|
|
111
|
+
currency: { type: 'string', default: 'usd' },
|
|
112
|
+
success_url: { type: 'string' },
|
|
113
|
+
cancel_url: { type: 'string' }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'list_products',
|
|
119
|
+
description: 'List products (Stripe-backed or local).',
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/api/stripe/products',
|
|
122
|
+
parameters: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
active: { type: 'boolean' },
|
|
126
|
+
limit: { type: 'number' }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'request_magic_link',
|
|
132
|
+
description: 'Send a magic link for passwordless login (local mode simulates delivery).',
|
|
133
|
+
method: 'POST',
|
|
134
|
+
path: '/api/auth/magic-link',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
required: ['email'],
|
|
138
|
+
properties: {
|
|
139
|
+
email: { type: 'string' }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'get_wallet_nonce',
|
|
145
|
+
description: 'Get a nonce to sign for wallet authentication.',
|
|
146
|
+
method: 'POST',
|
|
147
|
+
path: '/api/auth/nonce',
|
|
148
|
+
parameters: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
required: ['wallet_address'],
|
|
151
|
+
properties: {
|
|
152
|
+
wallet_address: { type: 'string' },
|
|
153
|
+
chain_type: { type: 'string', enum: ['solana', 'ethereum', 'bitcoin', 'base'] }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Default error shapes used for enrichment/merging
|
|
161
|
+
const defaultErrors = {
|
|
162
|
+
'400': {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
success: { type: 'boolean' },
|
|
166
|
+
error: { type: 'object', properties: { code: { type: 'string' }, message: { type: 'string' } }, required: ['message'] }
|
|
167
|
+
},
|
|
168
|
+
required: ['error']
|
|
169
|
+
},
|
|
170
|
+
'401': {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: { error: { type: 'string', enum: ['unauthorized'] }, message: { type: 'string' } },
|
|
173
|
+
required: ['error']
|
|
174
|
+
},
|
|
175
|
+
'403': {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { error: { type: 'string', enum: ['forbidden'] }, message: { type: 'string' } },
|
|
178
|
+
required: ['error']
|
|
179
|
+
},
|
|
180
|
+
'429': {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: { error: { type: 'string', enum: ['rate_limited'] }, message: { type: 'string' } },
|
|
183
|
+
required: ['error']
|
|
184
|
+
},
|
|
185
|
+
'500': {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: { error: { type: 'string', enum: ['server_error'] }, message: { type: 'string' } },
|
|
188
|
+
required: ['error']
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Attempt to align paths/methods with docs/manifest.json if available
|
|
193
|
+
try {
|
|
194
|
+
const manifest = tryLoadManifest();
|
|
195
|
+
if (manifest && Array.isArray(manifest.endpoints)) {
|
|
196
|
+
const find = (method, pathStr) => manifest.endpoints.find(e => e.method === method && e.path === pathStr);
|
|
197
|
+
const patchTool = (toolName, method, pathStr) => {
|
|
198
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
199
|
+
const ep = find(method, pathStr);
|
|
200
|
+
if (t && ep) {
|
|
201
|
+
t.method = ep.method;
|
|
202
|
+
t.path = ep.path;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
patchTool('login', 'POST', '/api/auth/login');
|
|
206
|
+
patchTool('register', 'POST', '/api/auth/register');
|
|
207
|
+
patchTool('get_current_user', 'GET', '/api/auth/user');
|
|
208
|
+
patchTool('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
209
|
+
patchTool('list_products', 'GET', '/api/stripe/products');
|
|
210
|
+
patchTool('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
211
|
+
patchTool('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Best-effort alignment only
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Attempt to enrich parameter schemas from OpenAPI
|
|
218
|
+
try {
|
|
219
|
+
const openapi = tryLoadOpenAPI();
|
|
220
|
+
if (openapi && openapi.paths) {
|
|
221
|
+
const findOp = (method, pathStr) => {
|
|
222
|
+
const ops = openapi.paths[pathStr];
|
|
223
|
+
if (!ops) return null;
|
|
224
|
+
return ops[String(method).toLowerCase()] || null;
|
|
225
|
+
};
|
|
226
|
+
const setBodySchema = (toolName, method, pathStr) => {
|
|
227
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
228
|
+
const op = findOp(method, pathStr);
|
|
229
|
+
const schema = op?.requestBody?.content?.['application/json']?.schema;
|
|
230
|
+
if (t && schema) {
|
|
231
|
+
t.parameters = schema;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const setResponses = (toolName, method, pathStr) => {
|
|
235
|
+
const t = spec.tools.find(x => x.name === toolName);
|
|
236
|
+
const op = findOp(method, pathStr);
|
|
237
|
+
if (t && op && op.responses) {
|
|
238
|
+
const responses = {};
|
|
239
|
+
const examples = {};
|
|
240
|
+
for (const [code, obj] of Object.entries(op.responses)) {
|
|
241
|
+
const schema = obj?.content?.['application/json']?.schema;
|
|
242
|
+
if (schema) responses[code] = schema;
|
|
243
|
+
const content = obj?.content?.['application/json'];
|
|
244
|
+
if (content?.example !== undefined) {
|
|
245
|
+
examples[code] = content.example;
|
|
246
|
+
} else if (content?.examples && typeof content.examples === 'object') {
|
|
247
|
+
const ex = {};
|
|
248
|
+
for (const [name, val] of Object.entries(content.examples)) {
|
|
249
|
+
if (val && typeof val === 'object') {
|
|
250
|
+
if ('value' in val) ex[name] = val.value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (Object.keys(ex).length) examples[code] = ex;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(responses).length) {
|
|
257
|
+
// Merge with default errors for completeness
|
|
258
|
+
const merged = { ...defaultErrors, ...responses };
|
|
259
|
+
t.responses = merged;
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(examples).length) t.examples = examples;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
setBodySchema('login', 'POST', '/api/auth/login');
|
|
265
|
+
setBodySchema('register', 'POST', '/api/auth/register');
|
|
266
|
+
setBodySchema('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
267
|
+
setBodySchema('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
268
|
+
setBodySchema('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
269
|
+
setResponses('login', 'POST', '/api/auth/login');
|
|
270
|
+
setResponses('register', 'POST', '/api/auth/register');
|
|
271
|
+
setResponses('get_current_user', 'GET', '/api/auth/user');
|
|
272
|
+
setResponses('create_checkout_session', 'POST', '/api/stripe/checkout-sessions');
|
|
273
|
+
setResponses('list_products', 'GET', '/api/stripe/products');
|
|
274
|
+
setResponses('request_magic_link', 'POST', '/api/auth/magic-link');
|
|
275
|
+
setResponses('get_wallet_nonce', 'POST', '/api/auth/nonce');
|
|
276
|
+
// For GET endpoints with query/headers, leave minimal schema for simplicity
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore enrichment errors
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add default error shapes if responses missing
|
|
283
|
+
spec.tools.forEach(t => {
|
|
284
|
+
if (!t.responses) t.responses = defaultErrors;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return spec;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildToolSpec({ format = 'openai', port = DEFAULT_PORT } = {}) {
|
|
291
|
+
switch (format) {
|
|
292
|
+
case 'openai':
|
|
293
|
+
default:
|
|
294
|
+
return buildOpenAIToolSpec({ port });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = { buildToolSpec };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// CLI dispatcher: builds a Commander program with pluggable handlers
|
|
2
|
+
// Enables dry-run parsing for unit tests without executing side effects.
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const { DEFAULT_PORT } = require('./config');
|
|
6
|
+
|
|
7
|
+
function defineProgram({ handlers = {}, dryRun = false, version = '0.0.0', logo = null } = {}) {
|
|
8
|
+
const intents = [];
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
if (dryRun) {
|
|
12
|
+
program.allowUnknownOption(true);
|
|
13
|
+
// Tolerate stray operands during unit tests (Commander normally errors)
|
|
14
|
+
if (typeof program.allowExcessArguments === 'function') {
|
|
15
|
+
program.allowExcessArguments(true);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('spaps')
|
|
21
|
+
.description('CLI for Sweet Potato Authentication & Payment Service')
|
|
22
|
+
.version(version)
|
|
23
|
+
.option('--json', 'Output in JSON format for machine parsing');
|
|
24
|
+
|
|
25
|
+
function makeAction(name, shape) {
|
|
26
|
+
return async function actionWrapper(...args) {
|
|
27
|
+
// Commander 11 passes (options, command) for subcommands without args
|
|
28
|
+
// For commands with args, it passes (arg1, arg2, ..., options, command)
|
|
29
|
+
const cmd = args[args.length - 1];
|
|
30
|
+
const options = args[args.length - 2] || {};
|
|
31
|
+
const parentJson = program.opts().json;
|
|
32
|
+
const isJson = Boolean(options.json || parentJson);
|
|
33
|
+
|
|
34
|
+
const intent = { name, options: { ...shape(options, cmd, isJson) } };
|
|
35
|
+
intents.push(intent);
|
|
36
|
+
|
|
37
|
+
if (dryRun) return intent;
|
|
38
|
+
const handler = handlers[name];
|
|
39
|
+
if (typeof handler === 'function') {
|
|
40
|
+
return await handler(intent, { program, cmd });
|
|
41
|
+
}
|
|
42
|
+
return intent;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// spaps local
|
|
47
|
+
const cmdLocal = program
|
|
48
|
+
.command('local')
|
|
49
|
+
.description('Start local SPAPS server (no API keys required!)')
|
|
50
|
+
.option('-p, --port <port>', 'Port to run on', String(DEFAULT_PORT))
|
|
51
|
+
.option('-s, --stripe <mode>', 'Stripe mode: mock|real', 'mock')
|
|
52
|
+
.option('--seed <preset>', 'Seed local data: demo', 'none')
|
|
53
|
+
.option('-o, --open', 'Open browser automatically', false)
|
|
54
|
+
.option('--json', 'Output in JSON format')
|
|
55
|
+
.action(
|
|
56
|
+
makeAction('local', (opts, cmd, isJson) => {
|
|
57
|
+
const out = {
|
|
58
|
+
port: Number(opts.port),
|
|
59
|
+
open: Boolean(opts.open),
|
|
60
|
+
json: isJson,
|
|
61
|
+
};
|
|
62
|
+
// Include optional flags only if explicitly provided by user
|
|
63
|
+
try {
|
|
64
|
+
const srcStripe = typeof cmd.getOptionValueSource === 'function' ? cmd.getOptionValueSource('stripe') : null;
|
|
65
|
+
const srcSeed = typeof cmd.getOptionValueSource === 'function' ? cmd.getOptionValueSource('seed') : null;
|
|
66
|
+
if (srcStripe === 'cli') out.stripe = String(opts.stripe || '').toLowerCase();
|
|
67
|
+
if (srcSeed === 'cli') out.seed = String(opts.seed || '').toLowerCase();
|
|
68
|
+
} catch (_) {
|
|
69
|
+
// Commander versions without getOptionValueSource; fall back to only including when present
|
|
70
|
+
if (typeof opts.stripe !== 'undefined') out.stripe = String(opts.stripe).toLowerCase();
|
|
71
|
+
if (typeof opts.seed !== 'undefined') out.seed = String(opts.seed).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
if (dryRun) {
|
|
77
|
+
cmdLocal.allowUnknownOption(true);
|
|
78
|
+
if (typeof cmdLocal.allowExcessArguments === 'function') {
|
|
79
|
+
cmdLocal.allowExcessArguments(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// spaps quickstart
|
|
84
|
+
const cmdQuick = program
|
|
85
|
+
.command('quickstart')
|
|
86
|
+
.description('Get quick start instructions (for AI agents)')
|
|
87
|
+
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
88
|
+
.option('--json', 'Output in JSON format')
|
|
89
|
+
.action(makeAction('quickstart', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
cmdQuick.allowUnknownOption(true);
|
|
92
|
+
if (typeof cmdQuick.allowExcessArguments === 'function') {
|
|
93
|
+
cmdQuick.allowExcessArguments(true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// spaps status
|
|
98
|
+
const cmdStatus = program
|
|
99
|
+
.command('status')
|
|
100
|
+
.description('Check if SPAPS server is running')
|
|
101
|
+
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
102
|
+
.option('--json', 'Output in JSON format')
|
|
103
|
+
.action(makeAction('status', (opts, _cmd, isJson) => ({ port: Number(opts.port), json: isJson })));
|
|
104
|
+
if (dryRun) {
|
|
105
|
+
cmdStatus.allowUnknownOption(true);
|
|
106
|
+
if (typeof cmdStatus.allowExcessArguments === 'function') {
|
|
107
|
+
cmdStatus.allowExcessArguments(true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// spaps init
|
|
112
|
+
const cmdInit = program
|
|
113
|
+
.command('init')
|
|
114
|
+
.description('Initialize SPAPS in current project')
|
|
115
|
+
.option('--json', 'Output in JSON format')
|
|
116
|
+
.action(makeAction('init', (_opts, _cmd, isJson) => ({ json: isJson })));
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
cmdInit.allowUnknownOption(true);
|
|
119
|
+
if (typeof cmdInit.allowExcessArguments === 'function') {
|
|
120
|
+
cmdInit.allowExcessArguments(true);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// spaps create <name>
|
|
125
|
+
const cmdCreate = program
|
|
126
|
+
.command('create <name>')
|
|
127
|
+
.description('Create a new project with SPAPS (coming soon)')
|
|
128
|
+
.action(makeAction('create', (optsOrName, cmd) => ({ name: typeof optsOrName === 'string' ? optsOrName : cmd.args[0] })));
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
cmdCreate.allowUnknownOption(true);
|
|
131
|
+
if (typeof cmdCreate.allowExcessArguments === 'function') {
|
|
132
|
+
cmdCreate.allowExcessArguments(true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// spaps types
|
|
137
|
+
const cmdTypes = program
|
|
138
|
+
.command('types')
|
|
139
|
+
.description('Generate TypeScript types (coming soon)')
|
|
140
|
+
.action(makeAction('types', () => ({})));
|
|
141
|
+
if (dryRun) {
|
|
142
|
+
cmdTypes.allowUnknownOption(true);
|
|
143
|
+
if (typeof cmdTypes.allowExcessArguments === 'function') {
|
|
144
|
+
cmdTypes.allowExcessArguments(true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// spaps help
|
|
149
|
+
const cmdHelp = program
|
|
150
|
+
.command('help')
|
|
151
|
+
.description('Show help and guides')
|
|
152
|
+
.option('-i, --interactive', 'Interactive help mode')
|
|
153
|
+
.option('-q, --quick', 'Quick reference')
|
|
154
|
+
.action(
|
|
155
|
+
makeAction('help', (opts) => ({ interactive: Boolean(opts.interactive), quick: Boolean(opts.quick) }))
|
|
156
|
+
);
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
cmdHelp.allowUnknownOption(true);
|
|
159
|
+
if (typeof cmdHelp.allowExcessArguments === 'function') {
|
|
160
|
+
cmdHelp.allowExcessArguments(true);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// spaps docs
|
|
165
|
+
const cmdDocs = program
|
|
166
|
+
.command('docs')
|
|
167
|
+
.description('Browse SDK documentation')
|
|
168
|
+
.option('-i, --interactive', 'Interactive documentation browser')
|
|
169
|
+
.option('-s, --search <query>', 'Search documentation')
|
|
170
|
+
.option('--json', 'Output in JSON format')
|
|
171
|
+
.action(
|
|
172
|
+
makeAction('docs', (opts, _cmd, isJson) => ({ interactive: Boolean(opts.interactive), search: opts.search || null, json: isJson }))
|
|
173
|
+
);
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
cmdDocs.allowUnknownOption(true);
|
|
176
|
+
if (typeof cmdDocs.allowExcessArguments === 'function') {
|
|
177
|
+
cmdDocs.allowExcessArguments(true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// spaps tools
|
|
182
|
+
const cmdTools = program
|
|
183
|
+
.command('tools')
|
|
184
|
+
.description('Output AI tool spec (OpenAI-style)')
|
|
185
|
+
.option('-p, --port <port>', 'Port to use for base_url', String(DEFAULT_PORT))
|
|
186
|
+
.option('-f, --format <format>', 'Spec format (openai)', 'openai')
|
|
187
|
+
.option('--json', 'Output in JSON format')
|
|
188
|
+
.action(
|
|
189
|
+
makeAction('tools', (opts, _cmd, isJson) => ({ port: Number(opts.port), format: String(opts.format || 'openai'), json: isJson }))
|
|
190
|
+
);
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
cmdTools.allowUnknownOption(true);
|
|
193
|
+
if (typeof cmdTools.allowExcessArguments === 'function') {
|
|
194
|
+
cmdTools.allowExcessArguments(true);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// spaps doctor
|
|
199
|
+
const cmdDoctor = program
|
|
200
|
+
.command('doctor')
|
|
201
|
+
.description('Diagnose local environment and config')
|
|
202
|
+
.option('-p, --port <port>', 'Port to check', String(DEFAULT_PORT))
|
|
203
|
+
.option('-s, --stripe <mode>', 'Stripe mode: mock|real')
|
|
204
|
+
.option('--json', 'Output in JSON format')
|
|
205
|
+
.action(
|
|
206
|
+
makeAction('doctor', (opts, _cmd, isJson) => ({ port: Number(opts.port), stripe: opts.stripe || null, json: isJson }))
|
|
207
|
+
);
|
|
208
|
+
if (dryRun) {
|
|
209
|
+
cmdDoctor.allowUnknownOption(true);
|
|
210
|
+
if (typeof cmdDoctor.allowExcessArguments === 'function') {
|
|
211
|
+
cmdDoctor.allowExcessArguments(true);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { program, getIntents: () => intents };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildProgram(config = {}) {
|
|
219
|
+
return defineProgram(config).program;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseArgv(argv, config = {}) {
|
|
223
|
+
const { program, getIntents } = defineProgram({ ...config, dryRun: true });
|
|
224
|
+
program.exitOverride(() => { /* swallow exit in dry-run */ });
|
|
225
|
+
try {
|
|
226
|
+
program.parse(argv, { from: 'user' });
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// Commander throws for help/version; we ignore in parse mode
|
|
229
|
+
}
|
|
230
|
+
return getIntents();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { buildProgram, parseArgv };
|
package/src/config.js
ADDED
package/src/docs-html.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generates comprehensive documentation page
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function generateDocsHTML(port = 3300) {
|
|
6
|
+
function generateDocsHTML(port = 3300, notice) {
|
|
7
7
|
return `<!DOCTYPE html>
|
|
8
8
|
<html lang="en">
|
|
9
9
|
<head>
|
|
@@ -277,6 +277,7 @@ function generateDocsHTML(port = 3300) {
|
|
|
277
277
|
<p class="subtitle">Sweet Potato Authentication & Payment Service</p>
|
|
278
278
|
<span class="status-badge">✅ Local Mode Active - Port ${port}</span>
|
|
279
279
|
</div>
|
|
280
|
+
${notice ? `<div class="alert">${notice}</div>` : ''}
|
|
280
281
|
|
|
281
282
|
<nav class="nav">
|
|
282
283
|
<ul>
|
|
@@ -807,4 +808,4 @@ test().catch(console.error);</code></pre>
|
|
|
807
808
|
</html>`;
|
|
808
809
|
}
|
|
809
810
|
|
|
810
|
-
module.exports = { generateDocsHTML };
|
|
811
|
+
module.exports = { generateDocsHTML };
|