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
package/src/docs-system.js
CHANGED
|
@@ -16,21 +16,21 @@ ${chalk.green('Installation:')}
|
|
|
16
16
|
yarn add spaps-sdk
|
|
17
17
|
|
|
18
18
|
${chalk.green('Basic Usage:')}
|
|
19
|
-
${chalk.gray('//
|
|
20
|
-
import {
|
|
19
|
+
${chalk.gray('// ES Module')}
|
|
20
|
+
import { SweetPotatoSDK } from 'spaps-sdk'
|
|
21
21
|
|
|
22
22
|
${chalk.gray('// CommonJS')}
|
|
23
|
-
const {
|
|
23
|
+
const { SweetPotatoSDK } = require('spaps-sdk')
|
|
24
24
|
|
|
25
25
|
${chalk.gray('// Create client (auto-detects local mode)')}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
${chalk.gray('//
|
|
29
|
-
const spaps = new SPAPSClient({
|
|
30
|
-
apiUrl: 'http://localhost:3300',
|
|
31
|
-
apiKey: 'your-api-key', ${chalk.gray('// Not needed for localhost')}
|
|
32
|
-
timeout: 10000
|
|
26
|
+
const sdk = new SweetPotatoSDK({
|
|
27
|
+
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3300',
|
|
28
|
+
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Not required in local mode')}
|
|
33
29
|
})
|
|
30
|
+
|
|
31
|
+
${chalk.gray('// Sign in with email/password (local mode accepts any credentials)')}
|
|
32
|
+
const auth = await sdk.auth.signInWithPassword({ email: 'user@example.com', password: 'password' })
|
|
33
|
+
console.log('User:', auth.user)
|
|
34
34
|
`
|
|
35
35
|
},
|
|
36
36
|
|
|
@@ -39,48 +39,30 @@ ${chalk.green('Basic Usage:')}
|
|
|
39
39
|
content: `
|
|
40
40
|
${chalk.green('Email/Password Authentication:')}
|
|
41
41
|
${chalk.gray('// Register new user')}
|
|
42
|
-
const
|
|
43
|
-
console.log('User:',
|
|
44
|
-
console.log('Token:',
|
|
42
|
+
const registered = await sdk.auth.register({ email, password })
|
|
43
|
+
console.log('User:', registered.user)
|
|
44
|
+
console.log('Token:', registered.access_token)
|
|
45
45
|
|
|
46
46
|
${chalk.gray('// Login existing user')}
|
|
47
|
-
const
|
|
47
|
+
const auth = await sdk.auth.signInWithPassword({ email, password })
|
|
48
48
|
|
|
49
49
|
${chalk.gray('// Check authentication status')}
|
|
50
|
-
if (
|
|
51
|
-
const user = await
|
|
52
|
-
console.log('Current user:', user
|
|
50
|
+
if (sdk.auth.isAuthenticated()) {
|
|
51
|
+
const user = await sdk.auth.getCurrentUser()
|
|
52
|
+
console.log('Current user:', user)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
${chalk.gray('// Logout')}
|
|
56
|
-
await
|
|
56
|
+
await sdk.auth.logout()
|
|
57
57
|
|
|
58
58
|
${chalk.green('Wallet Authentication:')}
|
|
59
|
-
${chalk.gray('//
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
signature,
|
|
63
|
-
message,
|
|
64
|
-
'solana'
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
${chalk.gray('// Ethereum wallet')}
|
|
68
|
-
await spaps.walletSignIn(
|
|
69
|
-
walletAddress,
|
|
70
|
-
signature,
|
|
71
|
-
message,
|
|
72
|
-
'ethereum'
|
|
73
|
-
)
|
|
59
|
+
${chalk.gray('// One-call helper: authenticateWallet')}
|
|
60
|
+
const resp = await sdk.auth.authenticateWallet(walletAddress, signMessage, 'ethereum')
|
|
61
|
+
console.log('User:', resp.user)
|
|
74
62
|
|
|
75
63
|
${chalk.green('Token Management:')}
|
|
76
|
-
${chalk.gray('//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
${chalk.gray('// Set token manually')}
|
|
80
|
-
spaps.setAccessToken(token)
|
|
81
|
-
|
|
82
|
-
${chalk.gray('// Refresh token')}
|
|
83
|
-
await spaps.refresh()
|
|
64
|
+
${chalk.gray('// Access token is managed internally; you can also set it manually')}
|
|
65
|
+
sdk.setAccessToken('jwt-token')
|
|
84
66
|
`
|
|
85
67
|
},
|
|
86
68
|
|
|
@@ -89,33 +71,25 @@ ${chalk.green('Token Management:')}
|
|
|
89
71
|
content: `
|
|
90
72
|
${chalk.green('Stripe Checkout:')}
|
|
91
73
|
${chalk.gray('// Create checkout session')}
|
|
92
|
-
const session = await
|
|
93
|
-
'price_123abc',
|
|
94
|
-
'http://localhost:3000/success',
|
|
95
|
-
'http://localhost:3000/cancel'
|
|
96
|
-
)
|
|
74
|
+
const session = await sdk.payments.createCheckoutSession({
|
|
75
|
+
price_id: 'price_123abc',
|
|
76
|
+
success_url: 'http://localhost:3000/success',
|
|
77
|
+
cancel_url: 'http://localhost:3000/cancel'
|
|
78
|
+
})
|
|
97
79
|
|
|
98
80
|
${chalk.gray('// Redirect to Stripe')}
|
|
99
|
-
window.location.href = session.
|
|
81
|
+
window.location.href = session.url
|
|
100
82
|
|
|
101
83
|
${chalk.green('Subscription Management:')}
|
|
102
84
|
${chalk.gray('// Get current subscription')}
|
|
103
|
-
|
|
104
|
-
console.log('Status:', subscription.data.status)
|
|
105
|
-
console.log('Plan:', subscription.data.plan)
|
|
106
|
-
console.log('Renews:', subscription.data.current_period_end)
|
|
85
|
+
// Example subscription helpers would go here if enabled
|
|
107
86
|
|
|
108
87
|
${chalk.gray('// Cancel subscription')}
|
|
109
88
|
await spaps.cancelSubscription()
|
|
110
89
|
|
|
111
90
|
${chalk.green('Usage Tracking:')}
|
|
112
91
|
${chalk.gray('// Check balance')}
|
|
113
|
-
|
|
114
|
-
console.log('Credits:', balance.data.balance)
|
|
115
|
-
|
|
116
|
-
${chalk.gray('// Record usage')}
|
|
117
|
-
await spaps.recordUsage('api-call', 1)
|
|
118
|
-
await spaps.recordUsage('image-generation', 10)
|
|
92
|
+
// Usage APIs depend on your server config
|
|
119
93
|
`
|
|
120
94
|
},
|
|
121
95
|
|
|
@@ -132,18 +106,9 @@ ${chalk.green('Environment Variables:')}
|
|
|
132
106
|
NEXT_PUBLIC_SPAPS_API_KEY=spaps_live_abc123...
|
|
133
107
|
|
|
134
108
|
${chalk.green('Configuration Options:')}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
${chalk.gray('// API key (not needed for localhost)')}
|
|
140
|
-
apiKey: 'spaps_live_abc123...',
|
|
141
|
-
|
|
142
|
-
${chalk.gray('// Request timeout in milliseconds')}
|
|
143
|
-
timeout: 10000,
|
|
144
|
-
|
|
145
|
-
${chalk.gray('// Auto-detect local mode (default: true)')}
|
|
146
|
-
autoDetect: true
|
|
109
|
+
const sdk = new SweetPotatoSDK({
|
|
110
|
+
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3300',
|
|
111
|
+
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Omit in local dev')}
|
|
147
112
|
})
|
|
148
113
|
|
|
149
114
|
${chalk.green('Local Mode Detection:')}
|
|
@@ -153,7 +118,7 @@ ${chalk.green('Local Mode Detection:')}
|
|
|
153
118
|
- No API URL is provided
|
|
154
119
|
|
|
155
120
|
${chalk.gray('// Check if in local mode')}
|
|
156
|
-
if (
|
|
121
|
+
if (sdk.isLocalMode) {
|
|
157
122
|
console.log('Running in local mode - no API key needed!')
|
|
158
123
|
}
|
|
159
124
|
`
|
|
@@ -165,14 +130,14 @@ ${chalk.green('Local Mode Detection:')}
|
|
|
165
130
|
${chalk.green('React Context Setup:')}
|
|
166
131
|
${chalk.gray('// contexts/SpapsContext.tsx')}
|
|
167
132
|
import { createContext, useContext } from 'react'
|
|
168
|
-
import {
|
|
133
|
+
import { SweetPotatoSDK } from 'spaps-sdk'
|
|
169
134
|
|
|
170
|
-
const
|
|
171
|
-
const SpapsContext = createContext(
|
|
135
|
+
const sdk = new SweetPotatoSDK({ apiUrl: process.env.NEXT_PUBLIC_SPAPS_API_URL || 'http://localhost:3300' })
|
|
136
|
+
const SpapsContext = createContext(sdk)
|
|
172
137
|
|
|
173
138
|
export function SpapsProvider({ children }) {
|
|
174
139
|
return (
|
|
175
|
-
<SpapsContext.Provider value={
|
|
140
|
+
<SpapsContext.Provider value={sdk}>
|
|
176
141
|
{children}
|
|
177
142
|
</SpapsContext.Provider>
|
|
178
143
|
)
|
|
@@ -273,26 +238,23 @@ ${chalk.green('Server Actions:')}
|
|
|
273
238
|
${chalk.gray('// app/actions/auth.ts')}
|
|
274
239
|
'use server'
|
|
275
240
|
|
|
276
|
-
import {
|
|
241
|
+
import { SweetPotatoSDK } from 'spaps-sdk'
|
|
277
242
|
import { cookies } from 'next/headers'
|
|
278
243
|
|
|
279
|
-
const
|
|
280
|
-
apiUrl: process.env.SPAPS_API_URL,
|
|
281
|
-
apiKey: process.env.SPAPS_API_KEY
|
|
282
|
-
})
|
|
244
|
+
const sdk = new SweetPotatoSDK({ apiUrl: process.env.SPAPS_API_URL, apiKey: process.env.SPAPS_API_KEY })
|
|
283
245
|
|
|
284
246
|
export async function loginAction(email: string, password: string) {
|
|
285
|
-
const
|
|
247
|
+
const auth = await sdk.auth.signInWithPassword({ email, password })
|
|
286
248
|
|
|
287
249
|
// Store token in cookie
|
|
288
|
-
cookies().set('spaps_token',
|
|
250
|
+
cookies().set('spaps_token', auth.access_token, {
|
|
289
251
|
httpOnly: true,
|
|
290
252
|
secure: process.env.NODE_ENV === 'production',
|
|
291
253
|
sameSite: 'lax',
|
|
292
254
|
maxAge: 60 * 60 * 24 * 7 // 1 week
|
|
293
255
|
})
|
|
294
256
|
|
|
295
|
-
return { success: true, user:
|
|
257
|
+
return { success: true, user: auth.user }
|
|
296
258
|
}
|
|
297
259
|
|
|
298
260
|
${chalk.green('Middleware Protection:')}
|
|
@@ -322,17 +284,14 @@ ${chalk.green('Middleware Protection:')}
|
|
|
322
284
|
${chalk.green('Express Middleware:')}
|
|
323
285
|
${chalk.gray('// server.js')}
|
|
324
286
|
const express = require('express')
|
|
325
|
-
const {
|
|
287
|
+
const { SweetPotatoSDK } = require('spaps-sdk')
|
|
326
288
|
|
|
327
289
|
const app = express()
|
|
328
|
-
const
|
|
329
|
-
apiUrl: process.env.SPAPS_API_URL,
|
|
330
|
-
apiKey: process.env.SPAPS_API_KEY
|
|
331
|
-
})
|
|
290
|
+
const sdk = new SweetPotatoSDK({ apiUrl: process.env.SPAPS_API_URL, apiKey: process.env.SPAPS_API_KEY })
|
|
332
291
|
|
|
333
292
|
${chalk.gray('// Add SPAPS to request')}
|
|
334
293
|
app.use((req, res, next) => {
|
|
335
|
-
req.spaps =
|
|
294
|
+
req.spaps = sdk
|
|
336
295
|
next()
|
|
337
296
|
})
|
|
338
297
|
|
|
@@ -346,8 +305,8 @@ ${chalk.green('Express Middleware:')}
|
|
|
346
305
|
|
|
347
306
|
try {
|
|
348
307
|
req.spaps.setAccessToken(token)
|
|
349
|
-
const
|
|
350
|
-
req.user =
|
|
308
|
+
const user = await req.spaps.auth.getCurrentUser()
|
|
309
|
+
req.user = user
|
|
351
310
|
next()
|
|
352
311
|
} catch (error) {
|
|
353
312
|
res.status(401).json({ error: 'Invalid token' })
|
|
@@ -360,8 +319,8 @@ ${chalk.green('Route Examples:')}
|
|
|
360
319
|
const { email, password } = req.body
|
|
361
320
|
|
|
362
321
|
try {
|
|
363
|
-
const
|
|
364
|
-
res.json(
|
|
322
|
+
const auth = await req.spaps.auth.signInWithPassword({ email, password })
|
|
323
|
+
res.json(auth)
|
|
365
324
|
} catch (error) {
|
|
366
325
|
res.status(401).json({ error: 'Invalid credentials' })
|
|
367
326
|
}
|
|
@@ -567,17 +526,17 @@ ${chalk.green('E2E Testing with Cypress:')}
|
|
|
567
526
|
${chalk.green('Mocking for Tests:')}
|
|
568
527
|
${chalk.gray('// Mock SPAPS client')}
|
|
569
528
|
jest.mock('spaps-sdk', () => ({
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
529
|
+
SweetPotatoSDK: jest.fn().mockImplementation(() => ({
|
|
530
|
+
auth: {
|
|
531
|
+
signInWithPassword: jest.fn().mockResolvedValue({
|
|
573
532
|
user: { id: '123', email: 'test@example.com' },
|
|
574
|
-
access_token: 'mock-token'
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
533
|
+
access_token: 'mock-token',
|
|
534
|
+
refresh_token: 'mock-refresh'
|
|
535
|
+
}),
|
|
536
|
+
isAuthenticated: jest.fn().mockReturnValue(true),
|
|
537
|
+
getCurrentUser: jest.fn().mockResolvedValue({ id: '123', email: 'test@example.com' })
|
|
538
|
+
},
|
|
539
|
+
setAccessToken: jest.fn()
|
|
581
540
|
}))
|
|
582
541
|
}))
|
|
583
542
|
`
|
|
@@ -587,39 +546,29 @@ ${chalk.green('Mocking for Tests:')}
|
|
|
587
546
|
title: 'API Reference',
|
|
588
547
|
content: `
|
|
589
548
|
${chalk.green('Authentication Methods:')}
|
|
590
|
-
|
|
591
|
-
register(email
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
logout(): Promise<void>
|
|
595
|
-
|
|
549
|
+
sdk.auth.signInWithPassword({ email, password }): Promise<AuthResponse>
|
|
550
|
+
sdk.auth.register({ email, password, username? }): Promise<AuthResponse>
|
|
551
|
+
sdk.auth.authenticateWallet(address, signFn, chain?): Promise<AuthResponse>
|
|
552
|
+
sdk.auth.refreshToken(refreshToken): Promise<AuthResponse>
|
|
553
|
+
sdk.auth.logout(): Promise<void>
|
|
554
|
+
sdk.auth.getCurrentUser(): Promise<User>
|
|
596
555
|
|
|
597
556
|
${chalk.green('Payment Methods:')}
|
|
598
|
-
createCheckoutSession(
|
|
599
|
-
|
|
600
|
-
|
|
557
|
+
sdk.payments.createCheckoutSession({ price_id, success_url, cancel_url }): Promise<CheckoutSession>
|
|
558
|
+
sdk.payments.getCheckoutSession(id): Promise<CheckoutSession>
|
|
559
|
+
sdk.payments.listProducts({ category?, active?, limit? }): Promise<ProductsListResponse>
|
|
601
560
|
|
|
602
561
|
${chalk.green('Usage Methods:')}
|
|
603
|
-
|
|
604
|
-
recordUsage(feature: string, amount: number): Promise<void>
|
|
562
|
+
See server docs if enabled
|
|
605
563
|
|
|
606
564
|
${chalk.green('Utility Methods:')}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
setAccessToken(token: string): void
|
|
610
|
-
isLocalMode(): boolean
|
|
611
|
-
health(): Promise<{data: any}>
|
|
612
|
-
|
|
613
|
-
${chalk.green('Properties:')}
|
|
614
|
-
client: AxiosInstance ${chalk.gray('// Direct access to Axios client')}
|
|
565
|
+
sdk.setAccessToken(token: string): void
|
|
566
|
+
sdk.clearAccessToken(): void
|
|
615
567
|
|
|
616
568
|
${chalk.green('Response Types:')}
|
|
617
|
-
${chalk.gray('//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const response = await spaps.login(email, password)
|
|
621
|
-
// response.data contains AuthResponse
|
|
622
|
-
// response.status, response.headers also available
|
|
569
|
+
${chalk.gray('// Methods generally return typed objects directly')}
|
|
570
|
+
const auth = await sdk.auth.signInWithPassword({ email, password })
|
|
571
|
+
console.log(auth.access_token)
|
|
623
572
|
`
|
|
624
573
|
}
|
|
625
574
|
};
|
|
@@ -636,7 +585,7 @@ ${chalk.green('Authentication Endpoints:')}
|
|
|
636
585
|
GET /api/auth/user ${chalk.gray('Get current user')}
|
|
637
586
|
|
|
638
587
|
${chalk.green('Stripe Endpoints:')}
|
|
639
|
-
POST /api/stripe/
|
|
588
|
+
POST /api/stripe/checkout-sessions ${chalk.gray('Create Stripe checkout')}
|
|
640
589
|
GET /api/stripe/subscription ${chalk.gray('Get subscription status')}
|
|
641
590
|
DELETE /api/stripe/subscription ${chalk.gray('Cancel subscription')}
|
|
642
591
|
POST /api/stripe/webhook ${chalk.gray('Stripe webhook handler')}
|
|
@@ -803,4 +752,4 @@ module.exports = {
|
|
|
803
752
|
searchDocs,
|
|
804
753
|
SDK_DOCS,
|
|
805
754
|
API_ENDPOINTS
|
|
806
|
-
};
|
|
755
|
+
};
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { getServerStatus } = require('./ai-helper');
|
|
8
|
+
const { DEFAULT_PORT } = require('./config');
|
|
9
|
+
|
|
10
|
+
function checkNodeVersion() {
|
|
11
|
+
const version = process.versions.node || '0.0.0';
|
|
12
|
+
const major = parseInt(version.split('.')[0], 10) || 0;
|
|
13
|
+
const ok = major >= 16;
|
|
14
|
+
return {
|
|
15
|
+
check: 'node_version',
|
|
16
|
+
success: ok,
|
|
17
|
+
details: { version, requirement: '>=16' },
|
|
18
|
+
fix: ok ? null : 'Upgrade Node.js to v18+ (recommended)'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function checkPort(port) {
|
|
23
|
+
// If server is running, we consider port check OK
|
|
24
|
+
const status = await getServerStatus(port);
|
|
25
|
+
if (status.running) {
|
|
26
|
+
return {
|
|
27
|
+
check: 'port',
|
|
28
|
+
success: true,
|
|
29
|
+
details: { port, running: true, url: status.url },
|
|
30
|
+
fix: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Otherwise ensure port is free to bind
|
|
34
|
+
const free = await new Promise((resolve) => {
|
|
35
|
+
const tester = net.createServer()
|
|
36
|
+
.once('error', () => resolve(false))
|
|
37
|
+
.once('listening', () => tester.once('close', () => resolve(true)).close())
|
|
38
|
+
.listen(port, '127.0.0.1');
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
check: 'port',
|
|
42
|
+
success: free,
|
|
43
|
+
details: { port, running: false, free },
|
|
44
|
+
fix: free ? null : `Use a different port: npx spaps local --port ${port + 1}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkEnvFile() {
|
|
49
|
+
const envPath = path.resolve(process.cwd(), '.env.local');
|
|
50
|
+
const exists = fs.existsSync(envPath);
|
|
51
|
+
let hasApiUrl = false;
|
|
52
|
+
if (exists) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
55
|
+
hasApiUrl = /SPAPS_API_URL\s*=/.test(content);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
check: 'env_file',
|
|
60
|
+
success: exists && hasApiUrl,
|
|
61
|
+
details: { path: envPath, exists, hasApiUrl },
|
|
62
|
+
fix: exists ? (hasApiUrl ? null : 'Add SPAPS_API_URL to .env.local (http://localhost:3300)') : 'Run: npx spaps init'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function checkWritePermissions() {
|
|
67
|
+
const dir = path.resolve(process.cwd(), '.spaps');
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
const tmp = path.join(dir, '_doctor.tmp');
|
|
71
|
+
fs.writeFileSync(tmp, 'ok');
|
|
72
|
+
fs.unlinkSync(tmp);
|
|
73
|
+
return { check: 'write_permissions', success: true, details: { dir }, fix: null };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { check: 'write_permissions', success: false, details: { dir, error: e.message }, fix: `Make directory writable: chmod -R u+rw ${dir}` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkSDKInstalled() {
|
|
80
|
+
try {
|
|
81
|
+
require.resolve('spaps-sdk', { paths: [process.cwd()] });
|
|
82
|
+
return { check: 'sdk_installed', success: true, details: { package: 'spaps-sdk' }, fix: null };
|
|
83
|
+
} catch {
|
|
84
|
+
return { check: 'sdk_installed', success: false, details: { package: 'spaps-sdk' }, fix: 'npm install spaps-sdk' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkStripeMode(stripeModeOpt) {
|
|
89
|
+
const mode = (stripeModeOpt || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real')).toLowerCase();
|
|
90
|
+
const needsKey = mode === 'real';
|
|
91
|
+
const hasKey = Boolean(process.env.STRIPE_SECRET_KEY);
|
|
92
|
+
const ok = mode === 'mock' || (mode === 'real' && hasKey);
|
|
93
|
+
return {
|
|
94
|
+
check: 'stripe_mode',
|
|
95
|
+
success: ok,
|
|
96
|
+
details: { mode, needsKey, hasKey },
|
|
97
|
+
fix: ok ? null : (mode === 'real' ? 'Set STRIPE_SECRET_KEY or run with --stripe mock' : null)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function checkEnvTest() {
|
|
102
|
+
const envPath = path.resolve(process.cwd(), '.env.test');
|
|
103
|
+
if (!fs.existsSync(envPath)) {
|
|
104
|
+
return {
|
|
105
|
+
check: 'env_test',
|
|
106
|
+
success: false,
|
|
107
|
+
details: { path: envPath, exists: false },
|
|
108
|
+
fix: 'Create .env.test with SPAPS_API_URL=http://localhost:3300 (no real network keys)'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
113
|
+
const hasLocalUrl = /SPAPS_API_URL\s*=\s*http:\/\/localhost:\d+/.test(content);
|
|
114
|
+
const hasApiKey = /SPAPS_API_KEY\s*=\s*\S+/.test(content);
|
|
115
|
+
const warns = [];
|
|
116
|
+
if (!hasLocalUrl) warns.push('SPAPS_API_URL should point to localhost');
|
|
117
|
+
if (hasApiKey) warns.push('SPAPS_API_KEY should not be set in tests');
|
|
118
|
+
return {
|
|
119
|
+
check: 'env_test',
|
|
120
|
+
success: hasLocalUrl && !hasApiKey,
|
|
121
|
+
details: { path: envPath, hasLocalUrl, hasApiKey },
|
|
122
|
+
fix: warns.length ? warns.join(' | ') : null
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { check: 'env_test', success: false, details: { error: e.message }, fix: 'Ensure .env.test is readable' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function checkNextJsPort() {
|
|
130
|
+
const defaultNextPort = 3000;
|
|
131
|
+
const inUse = await new Promise((resolve) => {
|
|
132
|
+
const tester = net.createServer()
|
|
133
|
+
.once('error', () => resolve(true))
|
|
134
|
+
.once('listening', () => tester.once('close', () => resolve(false)).close())
|
|
135
|
+
.listen(defaultNextPort, '127.0.0.1');
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
check: 'next_port',
|
|
139
|
+
success: true,
|
|
140
|
+
details: { port: defaultNextPort, inUse, note: inUse ? 'Next.js likely running (good)' : 'Port free' },
|
|
141
|
+
fix: null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function checkWebhook(port) {
|
|
146
|
+
const status = await getServerStatus(port);
|
|
147
|
+
if (!status.running) {
|
|
148
|
+
return {
|
|
149
|
+
check: 'webhook',
|
|
150
|
+
success: false,
|
|
151
|
+
details: { running: false },
|
|
152
|
+
fix: `Start server: npx spaps local --port ${port} --stripe mock`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const http = require('http');
|
|
157
|
+
const payload = JSON.stringify({ id: 'evt_doctor_' + Date.now(), type: 'checkout.session.completed', data: { object: { id: 'cs_doctor_' + Date.now() } } });
|
|
158
|
+
const ok = await new Promise((resolve) => {
|
|
159
|
+
const req = http.request({ hostname: 'localhost', port, path: '/api/stripe/webhooks', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }, (res) => {
|
|
160
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
161
|
+
});
|
|
162
|
+
req.on('error', () => resolve(false));
|
|
163
|
+
req.write(payload);
|
|
164
|
+
req.end();
|
|
165
|
+
});
|
|
166
|
+
return { check: 'webhook', success: ok, details: { path: '/api/stripe/webhooks' }, fix: ok ? null : 'Use --stripe mock or ensure webhook handler is reachable' };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { check: 'webhook', success: false, details: { error: e.message }, fix: 'Use --stripe mock or ensure server is running' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatHuman(results) {
|
|
173
|
+
const ok = results.every(r => r.success);
|
|
174
|
+
console.log(chalk.yellow('\nš SPAPS Doctor\n'));
|
|
175
|
+
results.forEach(r => {
|
|
176
|
+
const icon = r.success ? chalk.green('ā') : chalk.red('ā');
|
|
177
|
+
console.log(`${icon} ${r.check} ${chalk.gray(JSON.stringify(r.details))}`);
|
|
178
|
+
if (!r.success && r.fix) console.log(chalk.cyan(` fix: ${r.fix}`));
|
|
179
|
+
});
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(ok ? chalk.green('All checks passed!') : chalk.red('Some checks failed. See fixes above.'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } = {}) {
|
|
185
|
+
const results = [];
|
|
186
|
+
results.push(checkNodeVersion());
|
|
187
|
+
results.push(await checkPort(port));
|
|
188
|
+
// Warn if using 3000 which often collides with Next.js
|
|
189
|
+
if (port === 3000) {
|
|
190
|
+
results.push({
|
|
191
|
+
check: 'spaps_port_vs_next',
|
|
192
|
+
success: false,
|
|
193
|
+
details: { spaps_port: port, suggestion: 'Use 3300 for SPAPS to avoid Next.js conflicts' },
|
|
194
|
+
fix: 'Run: npx spaps local --port 3300'
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
results.push({ check: 'spaps_port_vs_next', success: true, details: { spaps_port: port }, fix: null });
|
|
198
|
+
}
|
|
199
|
+
results.push(checkEnvFile());
|
|
200
|
+
results.push(checkWritePermissions());
|
|
201
|
+
results.push(checkSDKInstalled());
|
|
202
|
+
results.push(checkStripeMode(stripe));
|
|
203
|
+
results.push(checkEnvTest());
|
|
204
|
+
results.push(await checkNextJsPort());
|
|
205
|
+
results.push(await checkWebhook(port));
|
|
206
|
+
|
|
207
|
+
const ok = results.every(r => r.success);
|
|
208
|
+
const payload = { success: ok, results, next_steps: ok ? [] : ['Apply suggested fixes and re-run: npx spaps doctor --json'] };
|
|
209
|
+
if (json) {
|
|
210
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
211
|
+
} else {
|
|
212
|
+
formatHuman(results);
|
|
213
|
+
}
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { runDoctor };
|