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,174 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const { DEFAULT_PORT } = require('./config');
4
+ const { handleError } = require('./error-handler');
5
+ const { showInteractiveHelp, showQuickHelp } = require('./help-system');
6
+ const { showInteractiveDocs, showQuickReference, searchDocs } = require('./docs-system');
7
+ const { getQuickStartInstructions, getServerStatus, runQuickTest } = require('./ai-helper');
8
+ const { buildToolSpec } = require('./ai-tool-spec');
9
+ const { runDoctor } = require('./doctor');
10
+
11
+ function createHandlers(version, logo) {
12
+ return {
13
+ local: async ({ options }) => {
14
+ const isJson = options.json;
15
+ if (!isJson) console.log(logo);
16
+ try {
17
+ const LocalServer = require('./local-server.js');
18
+ const server = new LocalServer({ port: options.port, json: isJson, stripeMode: options.stripe, seedMode: options.seed });
19
+ if (isJson) {
20
+ await server.start();
21
+ console.log(JSON.stringify({
22
+ success: true,
23
+ command: 'local',
24
+ server: {
25
+ url: `http://localhost:${options.port}`,
26
+ docs: `http://localhost:${options.port}/docs`,
27
+ mode: 'local-development',
28
+ port: Number(options.port),
29
+ features: { autoAuth: true, corsEnabled: true, testUsers: ['user', 'admin', 'premium'], apiKeyRequired: false, stripeMode: options.stripe, seeded: options.seed }
30
+ }
31
+ }));
32
+ } else {
33
+ await server.start();
34
+ if (options.open) {
35
+ const { exec } = require('child_process');
36
+ const url = `http://localhost:${options.port}/docs`;
37
+ const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
38
+ exec(`${start} ${url}`);
39
+ }
40
+ }
41
+ process.on('SIGINT', () => {
42
+ if (!isJson) console.log(chalk.yellow('\nšŸ‘‹ Shutting down SPAPS local server...'));
43
+ process.exit(0);
44
+ });
45
+ } catch (error) {
46
+ handleError(error, { port: options.port, command: 'local' }, { json: isJson });
47
+ }
48
+ },
49
+ quickstart: async ({ options }) => {
50
+ const instructions = getQuickStartInstructions(options.port);
51
+ if (options.json) {
52
+ console.log(JSON.stringify(instructions, null, 2));
53
+ } else {
54
+ console.log(chalk.yellow('\nšŸ  SPAPS Quick Start Instructions\n'));
55
+ console.log('1. Install SDK: npm install spaps-sdk');
56
+ console.log('2. Create test file with the code above');
57
+ console.log('3. Run: node test-spaps.js');
58
+ console.log('\nFor JSON output: npx spaps quickstart --json');
59
+ }
60
+ },
61
+ status: async ({ options }) => {
62
+ const status = await getServerStatus(options.port);
63
+ if (options.json) {
64
+ console.log(JSON.stringify(status));
65
+ } else {
66
+ if (!status.running) {
67
+ console.log(chalk.red('\nāŒ SPAPS server is not running'));
68
+ console.log('Start it with:');
69
+ console.log(chalk.cyan(` npx spaps local --port ${options.port}`));
70
+ console.log();
71
+ } else {
72
+ console.log(chalk.green('\nāœ… SPAPS server is running!\n'));
73
+ console.log(' URL:', chalk.cyan(status.url));
74
+ console.log(' Docs:', chalk.cyan(status.docs));
75
+ console.log();
76
+ }
77
+ }
78
+ },
79
+ test: async ({ options }) => {
80
+ const result = await runQuickTest(options.port);
81
+ console.log(JSON.stringify(result, null, 2));
82
+ },
83
+ init: async ({ options }) => {
84
+ const isJson = options.json;
85
+ const envContent = `# SPAPS Local Development\nSPAPS_API_URL=http://localhost:${DEFAULT_PORT}\n# SPAPS_API_KEY=your-api-key-here\n`;
86
+ const result = { success: true, command: 'init', files_created: [], files_skipped: [], next_steps: ['npx spaps local', 'npm install @spaps/sdk', 'Start coding!'] };
87
+ if (!fs.existsSync('.env.local')) {
88
+ fs.writeFileSync('.env.local', envContent);
89
+ result.files_created.push('.env.local');
90
+ if (!isJson) console.log(chalk.green('āœ… Created .env.local'));
91
+ } else {
92
+ result.files_skipped.push('.env.local');
93
+ result.message = '.env.local already exists';
94
+ if (!isJson) console.log(chalk.yellow('āš ļø .env.local already exists'));
95
+ }
96
+ if (isJson) {
97
+ console.log(JSON.stringify(result));
98
+ } else {
99
+ console.log();
100
+ console.log(chalk.green('✨ SPAPS initialized!'));
101
+ console.log();
102
+ console.log('Next steps:');
103
+ console.log(chalk.cyan(' 1. Run: npx spaps local'));
104
+ console.log(chalk.cyan(' 2. Install SDK: npm install @spaps/sdk'));
105
+ console.log(chalk.cyan(' 3. Start coding!'));
106
+ }
107
+ },
108
+ create: () => {
109
+ console.log(chalk.yellow('šŸ  SPAPS'));
110
+ console.log(chalk.yellow(`🚧 'spaps create' coming in v0.3.0!`));
111
+ console.log();
112
+ console.log('For now, check out our examples:');
113
+ console.log(chalk.cyan(' https://github.com/yourusername/sweet-potato/tree/main/examples'));
114
+ },
115
+ types: () => {
116
+ console.log(chalk.yellow('šŸ  SPAPS'));
117
+ console.log(chalk.yellow(`🚧 'spaps types' coming in v0.4.0!`));
118
+ },
119
+ help: async ({ options }) => {
120
+ if (options.interactive) {
121
+ await showInteractiveHelp();
122
+ } else if (options.quick) {
123
+ showQuickHelp();
124
+ } else {
125
+ showQuickHelp();
126
+ }
127
+ },
128
+ docs: async ({ options }) => {
129
+ if (options.search) {
130
+ const results = searchDocs(options.search);
131
+ if (options.json) {
132
+ console.log(JSON.stringify({ results }, null, 2));
133
+ } else {
134
+ console.log(chalk.yellow(`\nšŸ” Search results for "${options.search}":\n`));
135
+ if (results.length === 0) {
136
+ console.log(chalk.gray(' No results found'));
137
+ } else {
138
+ results.forEach((result, i) => {
139
+ console.log(chalk.green(` ${i + 1}. ${result.title}`));
140
+ console.log(chalk.gray(` ${result.preview}`));
141
+ console.log();
142
+ });
143
+ }
144
+ console.log(chalk.blue(' Run: npx spaps docs --interactive'));
145
+ console.log(chalk.blue(' to browse full documentation\n'));
146
+ }
147
+ } else if (options.interactive) {
148
+ await showInteractiveDocs();
149
+ } else {
150
+ showQuickReference();
151
+ }
152
+ },
153
+ tools: async ({ options }) => {
154
+ const spec = buildToolSpec({ format: options.format || 'openai', port: options.port });
155
+ if (options.json) {
156
+ console.log(JSON.stringify(spec, null, 2));
157
+ } else {
158
+ console.log(chalk.yellow('\nšŸ  SPAPS AI Tool Spec (OpenAI-style)\n'));
159
+ console.log('Base URL:', spec.base_url);
160
+ console.log('Tools:');
161
+ spec.tools.forEach((t, i) => {
162
+ console.log(chalk.green(` ${i + 1}. ${t.name}`), '-', t.description);
163
+ console.log(chalk.gray(` ${t.method} ${t.path}`));
164
+ });
165
+ console.log('\nTip: npx spaps tools --json > spaps-tools.json');
166
+ }
167
+ },
168
+ doctor: async ({ options }) => {
169
+ await runDoctor({ port: options.port || DEFAULT_PORT, stripe: options.stripe || null, json: options.json });
170
+ }
171
+ };
172
+ }
173
+
174
+ module.exports = { createHandlers };
@@ -6,6 +6,8 @@
6
6
  const chalk = require('chalk');
7
7
  const prompts = require('prompts');
8
8
 
9
+ const { DEFAULT_PORT } = require('./config');
10
+
9
11
  const HELP_TREE = {
10
12
  root: {
11
13
  question: 'What would you like to do?',
@@ -45,12 +47,12 @@ const HELP_TREE = {
45
47
  title: 'Quick start (default settings)',
46
48
  value: 'quick-start',
47
49
  command: 'npx spaps local',
48
- description: 'Start on port 3300'
50
+ description: `Start on port ${DEFAULT_PORT}`
49
51
  },
50
52
  {
51
53
  title: 'Custom port',
52
54
  value: 'custom-port',
53
- command: 'npx spaps local --port 3001',
55
+ command: `npx spaps local --port ${DEFAULT_PORT + 1}`,
54
56
  description: 'Choose your own port'
55
57
  },
56
58
  {
@@ -484,4 +486,4 @@ module.exports = {
484
486
  showInteractiveHelp,
485
487
  showQuickHelp,
486
488
  HELP_TREE
487
- };
489
+ };
@@ -8,19 +8,24 @@
8
8
  const express = require('express');
9
9
  const cors = require('cors');
10
10
  const chalk = require('chalk');
11
+ const fs = require('fs');
12
+ const path = require('path');
11
13
  const { generateDocsHTML } = require('./docs-html');
14
+ let swaggerUiDist = null;
15
+ try { swaggerUiDist = require('swagger-ui-dist'); } catch {}
12
16
  const StripeLocalManager = require('./stripe-local');
13
17
  const LocalAdminManager = require('./admin-local');
14
18
 
15
19
  // Stripe configuration for test mode
16
20
  const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY || 'sk_test_51S1WOy2HT0E1dOewiHvzt7T96PDwjocSDDUuc2ur569AVA5fDj4UpNM66lujrda1tTYrgooG0Z1dNFZfwEZuZdcA00nuVLJW67');
17
21
  const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY || 'pk_test_51S1WOy2HT0E1dOewb2EkxZIaPkz7v3zMM9VxuBoxgNILYMmS85I4zrAWTkevyUQcaWlWUoC2NYnB8X5ZKd5e7Ifc005IzIW6H2';
18
- const USE_REAL_STRIPE = process.env.USE_REAL_STRIPE !== 'false'; // Default to true
19
22
 
20
23
  class LocalServer {
21
24
  constructor(options = {}) {
22
25
  this.port = options.port || process.env.PORT || 3456;
23
26
  this.json = options.json || false;
27
+ this.stripeMode = options.stripeMode || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real');
28
+ this.seedMode = options.seedMode || 'none';
24
29
  this.app = express();
25
30
  this.stripeManager = null;
26
31
  this.adminManager = new LocalAdminManager();
@@ -29,6 +34,15 @@ class LocalServer {
29
34
  this.setupStripeRoutes();
30
35
  this.setupAdminRoutes();
31
36
  this.setupCatchAll();
37
+
38
+ // Optional demo seeding (idempotent)
39
+ if (this.seedMode === 'demo') {
40
+ try {
41
+ this.seedDemoData();
42
+ } catch (e) {
43
+ if (!this.json) console.warn(chalk.yellow(`āš ļø Seed failed: ${e.message}`));
44
+ }
45
+ }
32
46
  }
33
47
 
34
48
  setupMiddleware() {
@@ -44,6 +58,12 @@ class LocalServer {
44
58
  this.app.use(express.json());
45
59
  this.app.use(express.urlencoded({ extended: true }));
46
60
 
61
+ // Serve Swagger UI assets locally if available
62
+ if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
63
+ const uiPath = swaggerUiDist.getAbsoluteFSPath();
64
+ this.app.use('/swagger-ui', express.static(uiPath));
65
+ }
66
+
47
67
  // Local mode indicator
48
68
  this.app.use((req, res, next) => {
49
69
  res.setHeader('X-SPAPS-Mode', 'local-development');
@@ -67,6 +87,16 @@ class LocalServer {
67
87
  }
68
88
 
69
89
  setupRoutes() {
90
+ // OpenAPI JSON - try to serve repo spec; fallback to manifest; else minimal stub
91
+ this.app.get('/openapi.json', async (_req, res) => {
92
+ try {
93
+ const spec = await this.loadOpenApiSpec();
94
+ return res.json(spec);
95
+ } catch (e) {
96
+ return res.status(500).json({ error: 'Failed to generate OpenAPI', message: e.message });
97
+ }
98
+ });
99
+
70
100
  // Health check
71
101
  this.app.get('/health', (req, res) => {
72
102
  res.json({
@@ -167,7 +197,7 @@ class LocalServer {
167
197
  // Stripe checkout sessions endpoint - REAL or MOCK based on config
168
198
  this.app.post('/api/stripe/checkout-sessions', async (req, res) => {
169
199
  try {
170
- if (USE_REAL_STRIPE) {
200
+ if (this.stripeMode === 'real') {
171
201
  // Real Stripe checkout session
172
202
  const { product_name, amount, currency = 'usd', success_url, cancel_url, price_id } = req.body;
173
203
 
@@ -254,7 +284,7 @@ class LocalServer {
254
284
  // Stripe products endpoint - REAL or MOCK based on config
255
285
  this.app.get('/api/stripe/products', async (req, res) => {
256
286
  try {
257
- if (USE_REAL_STRIPE) {
287
+ if (this.stripeMode === 'real') {
258
288
  // Fetch real Stripe products
259
289
  const products = await stripe.products.list({
260
290
  active: req.query.active !== undefined ? req.query.active === 'true' : undefined,
@@ -418,7 +448,7 @@ class LocalServer {
418
448
  // Admin product sync endpoint - REAL or MOCK based on config
419
449
  this.app.post('/api/v1/admin/products/sync', async (req, res) => {
420
450
  try {
421
- if (USE_REAL_STRIPE) {
451
+ if (this.stripeMode === 'real') {
422
452
  // Get local products from admin manager
423
453
  const localProducts = this.adminManager.listProducts();
424
454
  const syncResults = [];
@@ -532,19 +562,148 @@ class LocalServer {
532
562
  });
533
563
  });
534
564
 
535
- // Documentation endpoint
565
+ // Documentation endpoint: prefer Swagger UI bound to /openapi.json
536
566
  this.app.get('/docs', (req, res) => {
537
- res.send(generateDocsHTML(this.port));
567
+ if (swaggerUiDist && typeof swaggerUiDist.getAbsoluteFSPath === 'function') {
568
+ res.type('html').send(`
569
+ <!DOCTYPE html>
570
+ <html>
571
+ <head>
572
+ <meta charset="utf-8" />
573
+ <title>SPAPS API Docs</title>
574
+ <link rel="stylesheet" href="/swagger-ui/swagger-ui.css" />
575
+ <style>body { margin: 0; } #swagger-ui { max-width: 100%; }</style>
576
+ </head>
577
+ <body>
578
+ <div id="swagger-ui"></div>
579
+ <script src="/swagger-ui/swagger-ui-bundle.js"></script>
580
+ <script src="/swagger-ui/swagger-ui-standalone-preset.js"></script>
581
+ <script>
582
+ window.onload = () => {
583
+ window.ui = SwaggerUIBundle({
584
+ url: '/openapi.json',
585
+ dom_id: '#swagger-ui',
586
+ deepLinking: true,
587
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
588
+ layout: 'BaseLayout'
589
+ });
590
+ };
591
+ </script>
592
+ </body>
593
+ </html>`);
594
+ } else {
595
+ // Fallback to the existing docs page if Swagger UI is not available
596
+ const msg = 'Using fallback docs page. Install Swagger UI assets for the full API explorer: npm install swagger-ui-dist';
597
+ res.send(generateDocsHTML(this.port, msg));
598
+ }
538
599
  });
539
600
  }
540
601
 
602
+ async loadOpenApiSpec() {
603
+ // Try YAML OpenAPI
604
+ const yamlCandidates = [
605
+ path.resolve(process.cwd(), 'docs/api-reference.yaml'),
606
+ path.resolve(__dirname, '../../../docs/api-reference.yaml')
607
+ ];
608
+ for (const p of yamlCandidates) {
609
+ try {
610
+ if (fs.existsSync(p)) {
611
+ let yaml;
612
+ try {
613
+ yaml = require('js-yaml');
614
+ } catch {}
615
+ if (yaml) {
616
+ const content = fs.readFileSync(p, 'utf8');
617
+ const parsed = yaml.load(content);
618
+ // ensure servers list points to local
619
+ parsed.servers = [{ url: `http://localhost:${this.port}` }];
620
+ return parsed;
621
+ }
622
+ }
623
+ } catch {}
624
+ }
625
+
626
+ // Fallback: build from manifest
627
+ const manifestCandidates = [
628
+ path.resolve(process.cwd(), 'docs/manifest.json'),
629
+ path.resolve(__dirname, '../../../docs/manifest.json')
630
+ ];
631
+ for (const p of manifestCandidates) {
632
+ try {
633
+ if (fs.existsSync(p)) {
634
+ const manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
635
+ return this.buildOpenApiFromManifest(manifest);
636
+ }
637
+ } catch {}
638
+ }
639
+
640
+ // Last resort: minimal stub
641
+ return {
642
+ openapi: '3.0.0',
643
+ info: { title: 'SPAPS Local API', version: '0.0.0' },
644
+ servers: [{ url: `http://localhost:${this.port}` }],
645
+ paths: {
646
+ '/health': { get: { summary: 'Health', responses: { '200': { description: 'OK' } } } }
647
+ }
648
+ };
649
+ }
650
+
651
+ buildOpenApiFromManifest(manifest) {
652
+ const spec = {
653
+ openapi: '3.0.0',
654
+ info: { title: 'SPAPS API', version: String(manifest.version || '1.0.0') },
655
+ servers: [{ url: `http://localhost:${this.port}` }],
656
+ paths: {}
657
+ };
658
+ const toPath = (p) => p.replace(/:(\w+)/g, '{$1}');
659
+ for (const ep of manifest.endpoints || []) {
660
+ const pathKey = toPath(ep.path);
661
+ if (!spec.paths[pathKey]) spec.paths[pathKey] = {};
662
+ spec.paths[pathKey][String(ep.method || 'GET').toLowerCase()] = {
663
+ summary: ep.description || `${ep.method} ${ep.path}`,
664
+ tags: ep.tags || [],
665
+ responses: { '200': { description: 'OK' } }
666
+ };
667
+ }
668
+ return spec;
669
+ }
670
+
671
+ seedDemoData() {
672
+ // Add a couple of customers and a completed + pending order if none exist
673
+ const existingCustomers = this.adminManager.listCustomers();
674
+ const products = this.adminManager.listProducts();
675
+ if (existingCustomers.length === 0 && products.length > 0) {
676
+ const alice = this.adminManager.createCustomer({ email: 'alice@example.com', name: 'Alice' });
677
+ const bob = this.adminManager.createCustomer({ email: 'bob@example.com', name: 'Bob' });
678
+ const p = products[0];
679
+ const order1 = this.adminManager.createOrder({
680
+ customer_id: alice.id,
681
+ customer_email: alice.email,
682
+ product_id: p.id,
683
+ price_id: p.price_id,
684
+ amount: p.price,
685
+ currency: p.currency
686
+ });
687
+ this.adminManager.updateOrderStatus(order1.id, 'completed');
688
+ this.adminManager.createOrder({
689
+ customer_id: bob.id,
690
+ customer_email: bob.email,
691
+ product_id: p.id,
692
+ price_id: p.price_id,
693
+ amount: p.price,
694
+ currency: p.currency
695
+ });
696
+ if (!this.json) console.log(chalk.gray('🌱 Seeded demo customers and orders'));
697
+ }
698
+ }
699
+
541
700
  setupStripeRoutes() {
542
701
  // Enhanced Stripe Product Management - Full CRUD
543
702
 
544
703
  // GET /api/stripe/products - List all products with filtering
545
704
  this.app.get('/api/stripe/products', async (req, res) => {
546
705
  try {
547
- if (USE_REAL_STRIPE) {
706
+ if (this.stripeMode === 'real') {
548
707
  const { active, category, limit = '100' } = req.query;
549
708
 
550
709
  const products = await stripe.products.list({
@@ -620,7 +779,7 @@ class LocalServer {
620
779
  });
621
780
  }
622
781
 
623
- if (USE_REAL_STRIPE) {
782
+ if (this.stripeMode === 'real') {
624
783
  // Create product in Stripe
625
784
  const stripeProduct = await stripe.products.create({
626
785
  name,
@@ -718,7 +877,7 @@ class LocalServer {
718
877
  const { productId } = req.params;
719
878
  const { name, description, images, metadata, active } = req.body;
720
879
 
721
- if (USE_REAL_STRIPE) {
880
+ if (this.stripeMode === 'real') {
722
881
  // Update in Stripe
723
882
  const updateData = {};
724
883
  if (name !== undefined) updateData.name = name;
@@ -780,7 +939,7 @@ class LocalServer {
780
939
  });
781
940
  }
782
941
 
783
- if (USE_REAL_STRIPE) {
942
+ if (this.stripeMode === 'real') {
784
943
  const priceData = {
785
944
  product: product_id,
786
945
  unit_amount: parseInt(unit_amount),
@@ -852,7 +1011,7 @@ class LocalServer {
852
1011
  });
853
1012
  }
854
1013
 
855
- if (USE_REAL_STRIPE) {
1014
+ if (this.stripeMode === 'real') {
856
1015
  const stripeProduct = await stripe.products.update(productId, {
857
1016
  default_price: price_id
858
1017
  });
@@ -895,7 +1054,7 @@ class LocalServer {
895
1054
  try {
896
1055
  const { productId } = req.params;
897
1056
 
898
- if (USE_REAL_STRIPE) {
1057
+ if (this.stripeMode === 'real') {
899
1058
  // Archive in Stripe (can't truly delete)
900
1059
  const stripeProduct = await stripe.products.update(productId, {
901
1060
  active: false
@@ -1058,7 +1217,7 @@ class LocalServer {
1058
1217
  try {
1059
1218
  let event;
1060
1219
 
1061
- if (USE_REAL_STRIPE) {
1220
+ if (this.stripeMode === 'real') {
1062
1221
  // Real Stripe webhook verification
1063
1222
  const sig = req.headers['stripe-signature'];
1064
1223
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
@@ -1076,7 +1235,13 @@ class LocalServer {
1076
1235
  }
1077
1236
  } else {
1078
1237
  // Mock mode - accept all webhooks
1079
- event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
1238
+ if (Buffer.isBuffer(req.body)) {
1239
+ event = JSON.parse(req.body.toString());
1240
+ } else if (typeof req.body === 'string') {
1241
+ event = JSON.parse(req.body);
1242
+ } else {
1243
+ event = req.body;
1244
+ }
1080
1245
  }
1081
1246
 
1082
1247
  if (!this.json) {
@@ -1494,7 +1659,7 @@ class LocalServer {
1494
1659
  console.log(chalk.yellow('šŸ  SPAPS Local Development Server'));
1495
1660
  console.log(chalk.green(`✨ Running at: http://localhost:${this.port}`));
1496
1661
  console.log(chalk.blue(`šŸ“ Documentation: http://localhost:${this.port}/docs`));
1497
- if (USE_REAL_STRIPE) {
1662
+ if (this.stripeMode === 'real') {
1498
1663
  console.log(chalk.magenta('šŸ’³ Stripe: Real test mode (live API calls)'));
1499
1664
  } else {
1500
1665
  console.log(chalk.gray('šŸ’³ Stripe: Mock mode (simulated responses)'));
@@ -1535,4 +1700,4 @@ if (require.main === module) {
1535
1700
  console.log(chalk.yellow('\nšŸ‘‹ Shutting down...'));
1536
1701
  process.exit(0);
1537
1702
  });
1538
- }
1703
+ }