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.
@@ -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
@@ -0,0 +1,5 @@
1
+ // Shared configuration for SPAPS CLI
2
+ module.exports = {
3
+ DEFAULT_PORT: 3300
4
+ };
5
+
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 };