@skillrecordings/cli 0.1.0 → 0.2.1
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/bin/skill.mjs +21 -0
- package/dist/chunk-2NCCVTEE.js +22342 -0
- package/dist/chunk-2NCCVTEE.js.map +1 -0
- package/dist/chunk-3E3GYSZR.js +7071 -0
- package/dist/chunk-3E3GYSZR.js.map +1 -0
- package/dist/chunk-F4EM72IH.js +86 -0
- package/dist/chunk-F4EM72IH.js.map +1 -0
- package/dist/chunk-FGP7KUQW.js +432 -0
- package/dist/chunk-FGP7KUQW.js.map +1 -0
- package/dist/chunk-H3D6VCME.js +55 -0
- package/dist/chunk-H3D6VCME.js.map +1 -0
- package/dist/chunk-HK3PEWFD.js +208 -0
- package/dist/chunk-HK3PEWFD.js.map +1 -0
- package/dist/chunk-KEV3QKXP.js +4495 -0
- package/dist/chunk-KEV3QKXP.js.map +1 -0
- package/dist/chunk-MG37YDAK.js +882 -0
- package/dist/chunk-MG37YDAK.js.map +1 -0
- package/dist/chunk-MLNDSBZ4.js +482 -0
- package/dist/chunk-MLNDSBZ4.js.map +1 -0
- package/dist/chunk-N2WIV2JV.js +22 -0
- package/dist/chunk-N2WIV2JV.js.map +1 -0
- package/dist/chunk-PWWRCN5W.js +2067 -0
- package/dist/chunk-PWWRCN5W.js.map +1 -0
- package/dist/chunk-SKHBM3XP.js +7746 -0
- package/dist/chunk-SKHBM3XP.js.map +1 -0
- package/dist/chunk-WFANXVQG.js +64 -0
- package/dist/chunk-WFANXVQG.js.map +1 -0
- package/dist/chunk-WYKL32C3.js +275 -0
- package/dist/chunk-WYKL32C3.js.map +1 -0
- package/dist/chunk-ZNF7XD2S.js +134 -0
- package/dist/chunk-ZNF7XD2S.js.map +1 -0
- package/dist/config-AUAIYDSI.js +20 -0
- package/dist/config-AUAIYDSI.js.map +1 -0
- package/dist/fileFromPath-XN7LXIBI.js +134 -0
- package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
- package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
- package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
- package/dist/getMachineId-win-IIF36LEJ.js +44 -0
- package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
- package/dist/index.js +112703 -0
- package/dist/index.js.map +1 -0
- package/dist/lib-R6DEEJCP.js +7623 -0
- package/dist/lib-R6DEEJCP.js.map +1 -0
- package/dist/pipeline-IAVVAKTU.js +120 -0
- package/dist/pipeline-IAVVAKTU.js.map +1 -0
- package/dist/query-NTP5NVXN.js +25 -0
- package/dist/query-NTP5NVXN.js.map +1 -0
- package/dist/routing-BAEPFB7V.js +390 -0
- package/dist/routing-BAEPFB7V.js.map +1 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
- package/dist/stripe-payment-history-SJPKA63N.js +67 -0
- package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
- package/dist/support-memory-WSG7SDKG.js +10 -0
- package/dist/support-memory-WSG7SDKG.js.map +1 -0
- package/package.json +10 -7
- package/.env.encrypted +0 -0
- package/CHANGELOG.md +0 -35
- package/data/tt-archive-dataset.json +0 -1
- package/data/validate-test-dataset.json +0 -97
- package/docs/CLI-AUTH.md +0 -504
- package/preload.ts +0 -18
- package/src/__tests__/init.test.ts +0 -74
- package/src/alignment-test.ts +0 -64
- package/src/check-apps.ts +0 -16
- package/src/commands/auth/decrypt.ts +0 -123
- package/src/commands/auth/encrypt.ts +0 -81
- package/src/commands/auth/index.ts +0 -50
- package/src/commands/auth/keygen.ts +0 -41
- package/src/commands/auth/status.ts +0 -164
- package/src/commands/axiom/forensic.ts +0 -868
- package/src/commands/axiom/index.ts +0 -697
- package/src/commands/build-dataset.ts +0 -311
- package/src/commands/db-status.ts +0 -47
- package/src/commands/deploys.ts +0 -219
- package/src/commands/eval-local/compare.ts +0 -171
- package/src/commands/eval-local/health.ts +0 -212
- package/src/commands/eval-local/index.ts +0 -76
- package/src/commands/eval-local/real-tools.ts +0 -416
- package/src/commands/eval-local/run.ts +0 -1168
- package/src/commands/eval-local/score-production.ts +0 -256
- package/src/commands/eval-local/seed.ts +0 -276
- package/src/commands/eval-pipeline/index.ts +0 -53
- package/src/commands/eval-pipeline/real-tools.ts +0 -492
- package/src/commands/eval-pipeline/run.ts +0 -1316
- package/src/commands/eval-pipeline/seed.ts +0 -395
- package/src/commands/eval-prompt.ts +0 -496
- package/src/commands/eval.test.ts +0 -253
- package/src/commands/eval.ts +0 -108
- package/src/commands/faq-classify.ts +0 -460
- package/src/commands/faq-cluster.ts +0 -135
- package/src/commands/faq-extract.ts +0 -249
- package/src/commands/faq-mine.ts +0 -432
- package/src/commands/faq-review.ts +0 -426
- package/src/commands/front/index.ts +0 -351
- package/src/commands/front/pull-conversations.ts +0 -275
- package/src/commands/front/tags.ts +0 -825
- package/src/commands/front-cache.ts +0 -1277
- package/src/commands/front-stats.ts +0 -75
- package/src/commands/health.test.ts +0 -82
- package/src/commands/health.ts +0 -362
- package/src/commands/init.test.ts +0 -89
- package/src/commands/init.ts +0 -106
- package/src/commands/inngest/client.ts +0 -294
- package/src/commands/inngest/events.ts +0 -296
- package/src/commands/inngest/investigate.ts +0 -382
- package/src/commands/inngest/runs.ts +0 -149
- package/src/commands/inngest/signal.ts +0 -143
- package/src/commands/kb-sync.ts +0 -498
- package/src/commands/memory/find.ts +0 -135
- package/src/commands/memory/get.ts +0 -87
- package/src/commands/memory/index.ts +0 -97
- package/src/commands/memory/stats.ts +0 -163
- package/src/commands/memory/store.ts +0 -49
- package/src/commands/memory/vote.ts +0 -159
- package/src/commands/pipeline.ts +0 -127
- package/src/commands/responses.ts +0 -856
- package/src/commands/tools.ts +0 -293
- package/src/commands/wizard.ts +0 -319
- package/src/index.ts +0 -172
- package/src/lib/crypto.ts +0 -56
- package/src/lib/env-loader.ts +0 -206
- package/src/lib/onepassword.ts +0 -137
- package/src/test-agent-local.ts +0 -115
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -10
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { createInstrumentedFrontClient } from '@skillrecordings/core/front/instrumented-client'
|
|
2
|
-
|
|
3
|
-
export async function frontStats() {
|
|
4
|
-
const front = createInstrumentedFrontClient({
|
|
5
|
-
apiToken: process.env.FRONT_API_TOKEN!,
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
// List all inboxes
|
|
9
|
-
const inboxes = await front.inboxes.list()
|
|
10
|
-
const inboxList = (inboxes as any)?._results ?? []
|
|
11
|
-
|
|
12
|
-
console.log(`Found ${inboxList.length} inboxes\n`)
|
|
13
|
-
console.log('Counting conversations (this may take a minute)...\n')
|
|
14
|
-
|
|
15
|
-
let grandTotal = 0
|
|
16
|
-
const results: { name: string; id: string; count: number }[] = []
|
|
17
|
-
|
|
18
|
-
// Key inboxes to check
|
|
19
|
-
const keyInboxes = [
|
|
20
|
-
'Total TypeScript',
|
|
21
|
-
'Epic Web',
|
|
22
|
-
'AI Hero',
|
|
23
|
-
'Pro Tailwind',
|
|
24
|
-
'Epic AI',
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
for (const inbox of inboxList) {
|
|
28
|
-
// Only count key inboxes for speed
|
|
29
|
-
if (!keyInboxes.some((k) => inbox.name.includes(k))) continue
|
|
30
|
-
|
|
31
|
-
process.stdout.write(`📬 ${inbox.name}... `)
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// Count by paginating
|
|
35
|
-
let count = 0
|
|
36
|
-
let hasMore = true
|
|
37
|
-
let pageToken: string | undefined
|
|
38
|
-
|
|
39
|
-
while (hasMore && count < 10000) {
|
|
40
|
-
// Cap at 10k
|
|
41
|
-
const response = (await front.inboxes.listConversations(inbox.id, {
|
|
42
|
-
limit: 100,
|
|
43
|
-
...(pageToken ? { page_token: pageToken } : {}),
|
|
44
|
-
})) as any
|
|
45
|
-
|
|
46
|
-
const convos = response?._results ?? []
|
|
47
|
-
count += convos.length
|
|
48
|
-
|
|
49
|
-
pageToken = response?._pagination?.next
|
|
50
|
-
hasMore = !!pageToken && convos.length === 100
|
|
51
|
-
|
|
52
|
-
// Rate limit
|
|
53
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log(`${count}${count >= 10000 ? '+' : ''} conversations`)
|
|
57
|
-
results.push({ name: inbox.name, id: inbox.id, count })
|
|
58
|
-
grandTotal += count
|
|
59
|
-
} catch (e: any) {
|
|
60
|
-
console.log(`Error: ${e.message}`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log(`\n📊 Key inboxes total: ${grandTotal} conversations`)
|
|
65
|
-
console.log(
|
|
66
|
-
` (${inboxList.length} total inboxes, only counted ${results.length} key ones)`
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
frontStats()
|
|
71
|
-
.then(() => process.exit(0))
|
|
72
|
-
.catch((e) => {
|
|
73
|
-
console.error(e)
|
|
74
|
-
process.exit(1)
|
|
75
|
-
})
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { createHmac } from 'node:crypto'
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
// Mock fetch globally
|
|
5
|
-
const mockFetch = vi.fn()
|
|
6
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
7
|
-
|
|
8
|
-
describe('health command', () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
vi.clearAllMocks()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
describe('signRequest', () => {
|
|
14
|
-
it('creates valid HMAC signature', async () => {
|
|
15
|
-
// Import the module to test signature format
|
|
16
|
-
const body = '{"action":"lookupUser","email":"[EMAIL]"}'
|
|
17
|
-
const secret = 'test-secret'
|
|
18
|
-
const timestamp = Math.floor(Date.now() / 1000)
|
|
19
|
-
const payload = `${timestamp}.${body}`
|
|
20
|
-
const expectedSignature = createHmac('sha256', secret)
|
|
21
|
-
.update(payload)
|
|
22
|
-
.digest('hex')
|
|
23
|
-
|
|
24
|
-
// The signature format should be: timestamp=X,v1=Y
|
|
25
|
-
expect(expectedSignature).toMatch(/^[a-f0-9]{64}$/)
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe('testAction', () => {
|
|
30
|
-
it('returns ok for successful response', async () => {
|
|
31
|
-
mockFetch.mockResolvedValueOnce({
|
|
32
|
-
ok: true,
|
|
33
|
-
status: 200,
|
|
34
|
-
json: async () => ({ id: 'user-123', email: '[EMAIL]' }),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
// Test that fetch was called with correct headers
|
|
38
|
-
const response = await mockFetch('http://localhost:3016/api/support', {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'x-support-signature': 'timestamp=123,v1=abc',
|
|
43
|
-
},
|
|
44
|
-
body: '{"action":"lookupUser","email":"[EMAIL]"}',
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
expect(response.ok).toBe(true)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('returns not_implemented for 501 response', async () => {
|
|
51
|
-
mockFetch.mockResolvedValueOnce({
|
|
52
|
-
ok: false,
|
|
53
|
-
status: 501,
|
|
54
|
-
json: async () => ({ error: 'Method not implemented' }),
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const response = await mockFetch('http://localhost:3016/api/support', {
|
|
58
|
-
method: 'POST',
|
|
59
|
-
body: '{"action":"getSubscriptions"}',
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
expect(response.status).toBe(501)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('returns error for failed response', async () => {
|
|
66
|
-
mockFetch.mockResolvedValueOnce({
|
|
67
|
-
ok: false,
|
|
68
|
-
status: 401,
|
|
69
|
-
statusText: 'Unauthorized',
|
|
70
|
-
json: async () => ({ error: 'Invalid signature' }),
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const response = await mockFetch('http://localhost:3016/api/support', {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
body: '{"action":"lookupUser"}',
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(response.ok).toBe(false)
|
|
79
|
-
expect(response.status).toBe(401)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
})
|
package/src/commands/health.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { createHmac } from 'node:crypto'
|
|
2
|
-
import { AppsTable, closeDb, eq, getDb } from '@skillrecordings/database'
|
|
3
|
-
|
|
4
|
-
interface HealthCheckResult {
|
|
5
|
-
endpoint: string
|
|
6
|
-
status: 'ok' | 'error'
|
|
7
|
-
responseTime: number
|
|
8
|
-
actions: {
|
|
9
|
-
name: string
|
|
10
|
-
status: 'ok' | 'error' | 'not_implemented'
|
|
11
|
-
error?: string
|
|
12
|
-
}[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Sign a request payload with HMAC-SHA256
|
|
17
|
-
*/
|
|
18
|
-
function signRequest(body: string, secret: string): string {
|
|
19
|
-
const timestamp = Math.floor(Date.now() / 1000)
|
|
20
|
-
const payload = `${timestamp}.${body}`
|
|
21
|
-
const signature = createHmac('sha256', secret).update(payload).digest('hex')
|
|
22
|
-
return `timestamp=${timestamp},v1=${signature}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Test a single action against the integration endpoint
|
|
27
|
-
*/
|
|
28
|
-
async function testAction(
|
|
29
|
-
baseUrl: string,
|
|
30
|
-
secret: string,
|
|
31
|
-
action: string,
|
|
32
|
-
params: Record<string, unknown>
|
|
33
|
-
): Promise<{ status: 'ok' | 'error' | 'not_implemented'; error?: string }> {
|
|
34
|
-
const body = JSON.stringify({ action, ...params })
|
|
35
|
-
const signature = signRequest(body, secret)
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const response = await fetch(baseUrl, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'x-support-signature': signature,
|
|
43
|
-
},
|
|
44
|
-
body,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
if (response.status === 501) {
|
|
48
|
-
return { status: 'not_implemented' }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
const data = (await response.json().catch(() => ({}))) as {
|
|
53
|
-
error?: string
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
status: 'error',
|
|
57
|
-
error: `${response.status}: ${data.error || response.statusText}`,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { status: 'ok' }
|
|
62
|
-
} catch (err) {
|
|
63
|
-
return {
|
|
64
|
-
status: 'error',
|
|
65
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Look up an app by slug from the database
|
|
72
|
-
*/
|
|
73
|
-
async function lookupApp(
|
|
74
|
-
slugOrUrl: string
|
|
75
|
-
): Promise<{ baseUrl: string; secret: string } | null> {
|
|
76
|
-
// If it looks like a URL, return null (use direct mode)
|
|
77
|
-
if (slugOrUrl.startsWith('http://') || slugOrUrl.startsWith('https://')) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const db = getDb()
|
|
83
|
-
const app = await db
|
|
84
|
-
.select({
|
|
85
|
-
integration_base_url: AppsTable.integration_base_url,
|
|
86
|
-
webhook_secret: AppsTable.webhook_secret,
|
|
87
|
-
})
|
|
88
|
-
.from(AppsTable)
|
|
89
|
-
.where(eq(AppsTable.slug, slugOrUrl))
|
|
90
|
-
.limit(1)
|
|
91
|
-
|
|
92
|
-
const record = app[0]
|
|
93
|
-
if (!record) {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
baseUrl: record.integration_base_url,
|
|
99
|
-
secret: record.webhook_secret,
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error('Database lookup failed:', err)
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* List all registered apps
|
|
109
|
-
*/
|
|
110
|
-
async function listApps(): Promise<void> {
|
|
111
|
-
try {
|
|
112
|
-
const db = getDb()
|
|
113
|
-
const apps = await db
|
|
114
|
-
.select({
|
|
115
|
-
slug: AppsTable.slug,
|
|
116
|
-
name: AppsTable.name,
|
|
117
|
-
integration_base_url: AppsTable.integration_base_url,
|
|
118
|
-
})
|
|
119
|
-
.from(AppsTable)
|
|
120
|
-
|
|
121
|
-
if (!apps.length) {
|
|
122
|
-
console.log('No apps registered.')
|
|
123
|
-
await closeDb()
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
console.log('\nRegistered apps:\n')
|
|
128
|
-
for (const app of apps) {
|
|
129
|
-
console.log(` ${app.slug}`)
|
|
130
|
-
console.log(` Name: ${app.name}`)
|
|
131
|
-
console.log(` URL: ${app.integration_base_url}\n`)
|
|
132
|
-
}
|
|
133
|
-
await closeDb()
|
|
134
|
-
} catch (err) {
|
|
135
|
-
console.error('Failed to list apps:', err)
|
|
136
|
-
await closeDb()
|
|
137
|
-
process.exit(1)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Health check command - tests integration endpoint connectivity and capabilities
|
|
143
|
-
*
|
|
144
|
-
* Agent-friendly: all options are non-interactive.
|
|
145
|
-
* Use --json for machine-readable output.
|
|
146
|
-
*
|
|
147
|
-
* Usage:
|
|
148
|
-
* skill health <slug> - Look up app by slug from database
|
|
149
|
-
* skill health <url> --secret xxx - Direct URL mode
|
|
150
|
-
* skill health --list - List all registered apps
|
|
151
|
-
* skill health <slug> --json - Output as JSON
|
|
152
|
-
*/
|
|
153
|
-
export async function health(
|
|
154
|
-
slugOrUrl: string | undefined,
|
|
155
|
-
options: { secret?: string; list?: boolean; json?: boolean }
|
|
156
|
-
): Promise<void> {
|
|
157
|
-
const { json = false } = options
|
|
158
|
-
|
|
159
|
-
// Handle --list flag
|
|
160
|
-
if (options.list) {
|
|
161
|
-
await listApps()
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!slugOrUrl) {
|
|
166
|
-
const error = {
|
|
167
|
-
success: false,
|
|
168
|
-
error:
|
|
169
|
-
'App slug or URL required. Usage: skill health <slug|url> [--secret <secret>]',
|
|
170
|
-
}
|
|
171
|
-
if (json) {
|
|
172
|
-
console.log(JSON.stringify(error, null, 2))
|
|
173
|
-
} else {
|
|
174
|
-
console.error('Error: App slug or URL required')
|
|
175
|
-
console.error('Usage: skill health <slug|url> [--secret <secret>]')
|
|
176
|
-
console.error(' skill health --list')
|
|
177
|
-
}
|
|
178
|
-
await closeDb()
|
|
179
|
-
process.exit(1)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let baseUrl: string
|
|
183
|
-
let secret: string
|
|
184
|
-
|
|
185
|
-
// Try database lookup first
|
|
186
|
-
const appConfig = await lookupApp(slugOrUrl)
|
|
187
|
-
|
|
188
|
-
if (appConfig) {
|
|
189
|
-
// Found in database
|
|
190
|
-
baseUrl = appConfig.baseUrl.endsWith('/api/support')
|
|
191
|
-
? appConfig.baseUrl
|
|
192
|
-
: appConfig.baseUrl.replace(/\/$/, '') + '/api/support'
|
|
193
|
-
secret = appConfig.secret
|
|
194
|
-
if (!json) {
|
|
195
|
-
console.log(`\nUsing app configuration for: ${slugOrUrl}`)
|
|
196
|
-
}
|
|
197
|
-
} else if (
|
|
198
|
-
slugOrUrl.startsWith('http://') ||
|
|
199
|
-
slugOrUrl.startsWith('https://')
|
|
200
|
-
) {
|
|
201
|
-
// Direct URL mode
|
|
202
|
-
const secretValue = options.secret || process.env.SUPPORT_WEBHOOK_SECRET
|
|
203
|
-
if (!secretValue) {
|
|
204
|
-
const error = {
|
|
205
|
-
success: false,
|
|
206
|
-
error:
|
|
207
|
-
'Webhook secret required for direct URL mode. Use --secret or set SUPPORT_WEBHOOK_SECRET',
|
|
208
|
-
}
|
|
209
|
-
if (json) {
|
|
210
|
-
console.log(JSON.stringify(error, null, 2))
|
|
211
|
-
} else {
|
|
212
|
-
console.error(`Error: ${error.error}`)
|
|
213
|
-
}
|
|
214
|
-
await closeDb()
|
|
215
|
-
process.exit(1)
|
|
216
|
-
}
|
|
217
|
-
baseUrl = slugOrUrl.endsWith('/api/support')
|
|
218
|
-
? slugOrUrl
|
|
219
|
-
: slugOrUrl.replace(/\/$/, '') + '/api/support'
|
|
220
|
-
secret = secretValue
|
|
221
|
-
} else {
|
|
222
|
-
// Slug not found in database
|
|
223
|
-
const error = {
|
|
224
|
-
success: false,
|
|
225
|
-
error: `App "${slugOrUrl}" not found in database. Use --list to see registered apps, or provide a full URL.`,
|
|
226
|
-
}
|
|
227
|
-
if (json) {
|
|
228
|
-
console.log(JSON.stringify(error, null, 2))
|
|
229
|
-
} else {
|
|
230
|
-
console.error(`Error: App "${slugOrUrl}" not found in database`)
|
|
231
|
-
console.error('Use --list to see registered apps, or provide a full URL')
|
|
232
|
-
}
|
|
233
|
-
await closeDb()
|
|
234
|
-
process.exit(1)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!json) {
|
|
238
|
-
console.log(`\nHealth check: ${baseUrl}\n`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const start = Date.now()
|
|
242
|
-
const results: HealthCheckResult = {
|
|
243
|
-
endpoint: baseUrl,
|
|
244
|
-
status: 'ok',
|
|
245
|
-
responseTime: 0,
|
|
246
|
-
actions: [],
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Test required actions
|
|
250
|
-
const requiredActions = [
|
|
251
|
-
{ name: 'lookupUser', params: { email: '[EMAIL]' } },
|
|
252
|
-
{ name: 'getPurchases', params: { userId: 'health-check-user-id' } },
|
|
253
|
-
]
|
|
254
|
-
|
|
255
|
-
// Test optional actions
|
|
256
|
-
const optionalActions = [
|
|
257
|
-
{ name: 'getSubscriptions', params: { userId: 'health-check-user-id' } },
|
|
258
|
-
{
|
|
259
|
-
name: 'generateMagicLink',
|
|
260
|
-
params: { email: '[EMAIL]', expiresIn: 60 },
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: 'getClaimedSeats',
|
|
264
|
-
params: { bulkCouponId: 'health-check-coupon-id' },
|
|
265
|
-
},
|
|
266
|
-
]
|
|
267
|
-
|
|
268
|
-
if (!json) {
|
|
269
|
-
console.log('Testing required actions...')
|
|
270
|
-
}
|
|
271
|
-
for (const { name, params } of requiredActions) {
|
|
272
|
-
const result = await testAction(baseUrl, secret, name, params)
|
|
273
|
-
results.actions.push({ name, ...result })
|
|
274
|
-
|
|
275
|
-
if (!json) {
|
|
276
|
-
const icon =
|
|
277
|
-
result.status === 'ok'
|
|
278
|
-
? '✓'
|
|
279
|
-
: result.status === 'not_implemented'
|
|
280
|
-
? '○'
|
|
281
|
-
: '✗'
|
|
282
|
-
const color =
|
|
283
|
-
result.status === 'ok'
|
|
284
|
-
? '\x1b[32m'
|
|
285
|
-
: result.status === 'not_implemented'
|
|
286
|
-
? '\x1b[33m'
|
|
287
|
-
: '\x1b[31m'
|
|
288
|
-
console.log(
|
|
289
|
-
` ${color}${icon}\x1b[0m ${name}${result.error ? ` - ${result.error}` : ''}`
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (result.status === 'error') {
|
|
294
|
-
results.status = 'error'
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!json) {
|
|
299
|
-
console.log('\nTesting optional actions...')
|
|
300
|
-
}
|
|
301
|
-
for (const { name, params } of optionalActions) {
|
|
302
|
-
const result = await testAction(baseUrl, secret, name, params)
|
|
303
|
-
results.actions.push({ name, ...result })
|
|
304
|
-
|
|
305
|
-
if (!json) {
|
|
306
|
-
const icon =
|
|
307
|
-
result.status === 'ok'
|
|
308
|
-
? '✓'
|
|
309
|
-
: result.status === 'not_implemented'
|
|
310
|
-
? '○'
|
|
311
|
-
: '✗'
|
|
312
|
-
const color =
|
|
313
|
-
result.status === 'ok'
|
|
314
|
-
? '\x1b[32m'
|
|
315
|
-
: result.status === 'not_implemented'
|
|
316
|
-
? '\x1b[33m'
|
|
317
|
-
: '\x1b[31m'
|
|
318
|
-
console.log(
|
|
319
|
-
` ${color}${icon}\x1b[0m ${name}${result.error ? ` - ${result.error}` : ''}`
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
results.responseTime = Date.now() - start
|
|
325
|
-
|
|
326
|
-
// Summary
|
|
327
|
-
const okCount = results.actions.filter((a) => a.status === 'ok').length
|
|
328
|
-
const errorCount = results.actions.filter((a) => a.status === 'error').length
|
|
329
|
-
const notImplementedCount = results.actions.filter(
|
|
330
|
-
(a) => a.status === 'not_implemented'
|
|
331
|
-
).length
|
|
332
|
-
|
|
333
|
-
// JSON output includes success flag
|
|
334
|
-
const jsonResult = {
|
|
335
|
-
success: results.status !== 'error',
|
|
336
|
-
...results,
|
|
337
|
-
summary: {
|
|
338
|
-
ok: okCount,
|
|
339
|
-
notImplemented: notImplementedCount,
|
|
340
|
-
errors: errorCount,
|
|
341
|
-
},
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (json) {
|
|
345
|
-
console.log(JSON.stringify(jsonResult, null, 2))
|
|
346
|
-
} else {
|
|
347
|
-
console.log(`\n─────────────────────────────`)
|
|
348
|
-
console.log(`Total time: ${results.responseTime}ms`)
|
|
349
|
-
console.log(
|
|
350
|
-
`Actions: \x1b[32m${okCount} ok\x1b[0m, \x1b[33m${notImplementedCount} not implemented\x1b[0m, \x1b[31m${errorCount} errors\x1b[0m`
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
if (results.status === 'error') {
|
|
354
|
-
console.log(`\n\x1b[31m✗ Health check failed\x1b[0m\n`)
|
|
355
|
-
} else {
|
|
356
|
-
console.log(`\n\x1b[32m✓ Health check passed\x1b[0m\n`)
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
await closeDb()
|
|
361
|
-
process.exit(results.status === 'error' ? 1 : 0)
|
|
362
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { init } from './init'
|
|
3
|
-
|
|
4
|
-
// Mock process.exit to prevent test termination
|
|
5
|
-
const mockExit = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
6
|
-
throw new Error(`process.exit(${code})`)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
describe('init command', () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
vi.clearAllMocks()
|
|
12
|
-
mockExit.mockClear()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
mockExit.mockClear()
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('should generate a secure 64-character hex webhook secret', async () => {
|
|
20
|
-
const consoleSpy = vi.spyOn(console, 'log')
|
|
21
|
-
|
|
22
|
-
// Expect process.exit(0) to be called
|
|
23
|
-
await expect(init('test-app')).rejects.toThrow('process.exit(0)')
|
|
24
|
-
|
|
25
|
-
const output = consoleSpy.mock.calls.flat().join('\n')
|
|
26
|
-
const secretMatch = output.match(/Webhook Secret: ([a-f0-9]+)/)
|
|
27
|
-
|
|
28
|
-
expect(secretMatch).toBeTruthy()
|
|
29
|
-
expect(secretMatch?.[1]).toHaveLength(64) // 32 bytes = 64 hex chars
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should use provided app name', async () => {
|
|
33
|
-
const consoleSpy = vi.spyOn(console, 'log')
|
|
34
|
-
|
|
35
|
-
await expect(init('my-custom-app')).rejects.toThrow('process.exit(0)')
|
|
36
|
-
|
|
37
|
-
const output = consoleSpy.mock.calls.flat().join('\n')
|
|
38
|
-
expect(output).toContain('my-custom-app')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('should default to "my-app" when name not provided and no input given', async () => {
|
|
42
|
-
// Skip this test in CI - requires user interaction
|
|
43
|
-
// In real usage, promptForName() waits for user input
|
|
44
|
-
// Testing the happy path with explicit name is sufficient
|
|
45
|
-
expect(true).toBe(true)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('should output webhook URL and .env format', async () => {
|
|
49
|
-
const consoleSpy = vi.spyOn(console, 'log')
|
|
50
|
-
|
|
51
|
-
await expect(init('test-app')).rejects.toThrow('process.exit(0)')
|
|
52
|
-
|
|
53
|
-
const output = consoleSpy.mock.calls.flat().join('\n')
|
|
54
|
-
expect(output).toContain('Webhook URL:')
|
|
55
|
-
expect(output).toContain('Add to your .env:')
|
|
56
|
-
expect(output).toContain('FRONT_WEBHOOK_SECRET=')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should output JSON when --json flag is used', async () => {
|
|
60
|
-
const consoleSpy = vi.spyOn(console, 'log')
|
|
61
|
-
|
|
62
|
-
await expect(init('json-test-app', { json: true })).rejects.toThrow(
|
|
63
|
-
'process.exit(0)'
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
const output = consoleSpy.mock.calls.flat().join('\n')
|
|
67
|
-
const parsed = JSON.parse(output)
|
|
68
|
-
|
|
69
|
-
expect(parsed.success).toBe(true)
|
|
70
|
-
expect(parsed.appName).toBe('json-test-app')
|
|
71
|
-
expect(parsed.webhookSecret).toHaveLength(64)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should error in non-interactive mode without name', async () => {
|
|
75
|
-
// Mock non-interactive by checking TTY
|
|
76
|
-
const originalIsTTY = process.stdin.isTTY
|
|
77
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
78
|
-
value: false,
|
|
79
|
-
writable: true,
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
await expect(init(undefined)).rejects.toThrow('process.exit(1)')
|
|
83
|
-
|
|
84
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
85
|
-
value: originalIsTTY,
|
|
86
|
-
writable: true,
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
})
|
package/src/commands/init.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
2
|
-
import { createInterface } from 'node:readline'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Check if stdin is a TTY (interactive terminal)
|
|
6
|
-
*/
|
|
7
|
-
function isInteractive(): boolean {
|
|
8
|
-
return process.stdin.isTTY === true
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Prompt user for app name interactively
|
|
13
|
-
*/
|
|
14
|
-
async function promptForName(): Promise<string> {
|
|
15
|
-
const rl = createInterface({
|
|
16
|
-
input: process.stdin,
|
|
17
|
-
output: process.stdout,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
rl.question('App name: ', (answer) => {
|
|
22
|
-
rl.close()
|
|
23
|
-
resolve(answer.trim() || 'my-app')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface InitOptions {
|
|
29
|
-
json?: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface InitResult {
|
|
33
|
-
success: boolean
|
|
34
|
-
appName?: string
|
|
35
|
-
webhookSecret?: string
|
|
36
|
-
error?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Initialize command for registering new app with webhook secret
|
|
41
|
-
*
|
|
42
|
-
* Agent-friendly: requires name argument in non-interactive mode.
|
|
43
|
-
* Use --json for machine-readable output.
|
|
44
|
-
*
|
|
45
|
-
* @param name - Name for the app (required in non-interactive mode)
|
|
46
|
-
* @param options - Command options
|
|
47
|
-
*/
|
|
48
|
-
export async function init(
|
|
49
|
-
name: string | undefined,
|
|
50
|
-
options: InitOptions = {}
|
|
51
|
-
): Promise<void> {
|
|
52
|
-
const { json = false } = options
|
|
53
|
-
|
|
54
|
-
// In non-interactive mode, name is required
|
|
55
|
-
if (!name && !isInteractive()) {
|
|
56
|
-
const result: InitResult = {
|
|
57
|
-
success: false,
|
|
58
|
-
error:
|
|
59
|
-
'App name is required in non-interactive mode. Usage: skill init <name>',
|
|
60
|
-
}
|
|
61
|
-
if (json) {
|
|
62
|
-
console.log(JSON.stringify(result, null, 2))
|
|
63
|
-
} else {
|
|
64
|
-
console.error(`Error: ${result.error}`)
|
|
65
|
-
}
|
|
66
|
-
process.exit(1)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const appName = name || (await promptForName())
|
|
70
|
-
|
|
71
|
-
if (!appName || appName.trim() === '') {
|
|
72
|
-
const result: InitResult = {
|
|
73
|
-
success: false,
|
|
74
|
-
error: 'App name cannot be empty',
|
|
75
|
-
}
|
|
76
|
-
if (json) {
|
|
77
|
-
console.log(JSON.stringify(result, null, 2))
|
|
78
|
-
} else {
|
|
79
|
-
console.error(`Error: ${result.error}`)
|
|
80
|
-
}
|
|
81
|
-
process.exit(1)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const webhookSecret = randomBytes(32).toString('hex')
|
|
85
|
-
|
|
86
|
-
// TODO: Save to DB when database connection is configured
|
|
87
|
-
// For now, output the values for manual configuration
|
|
88
|
-
|
|
89
|
-
const result: InitResult = {
|
|
90
|
-
success: true,
|
|
91
|
-
appName,
|
|
92
|
-
webhookSecret,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (json) {
|
|
96
|
-
console.log(JSON.stringify(result, null, 2))
|
|
97
|
-
} else {
|
|
98
|
-
console.log(`\n✓ App "${appName}" initialized\n`)
|
|
99
|
-
console.log(`Webhook URL: https://your-domain.com/api/webhooks/front`)
|
|
100
|
-
console.log(`Webhook Secret: ${webhookSecret}`)
|
|
101
|
-
console.log(`\nAdd to your .env:`)
|
|
102
|
-
console.log(`FRONT_WEBHOOK_SECRET=${webhookSecret}`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
process.exit(0)
|
|
106
|
-
}
|