@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.
Files changed (136) hide show
  1. package/bin/skill.mjs +21 -0
  2. package/dist/chunk-2NCCVTEE.js +22342 -0
  3. package/dist/chunk-2NCCVTEE.js.map +1 -0
  4. package/dist/chunk-3E3GYSZR.js +7071 -0
  5. package/dist/chunk-3E3GYSZR.js.map +1 -0
  6. package/dist/chunk-F4EM72IH.js +86 -0
  7. package/dist/chunk-F4EM72IH.js.map +1 -0
  8. package/dist/chunk-FGP7KUQW.js +432 -0
  9. package/dist/chunk-FGP7KUQW.js.map +1 -0
  10. package/dist/chunk-H3D6VCME.js +55 -0
  11. package/dist/chunk-H3D6VCME.js.map +1 -0
  12. package/dist/chunk-HK3PEWFD.js +208 -0
  13. package/dist/chunk-HK3PEWFD.js.map +1 -0
  14. package/dist/chunk-KEV3QKXP.js +4495 -0
  15. package/dist/chunk-KEV3QKXP.js.map +1 -0
  16. package/dist/chunk-MG37YDAK.js +882 -0
  17. package/dist/chunk-MG37YDAK.js.map +1 -0
  18. package/dist/chunk-MLNDSBZ4.js +482 -0
  19. package/dist/chunk-MLNDSBZ4.js.map +1 -0
  20. package/dist/chunk-N2WIV2JV.js +22 -0
  21. package/dist/chunk-N2WIV2JV.js.map +1 -0
  22. package/dist/chunk-PWWRCN5W.js +2067 -0
  23. package/dist/chunk-PWWRCN5W.js.map +1 -0
  24. package/dist/chunk-SKHBM3XP.js +7746 -0
  25. package/dist/chunk-SKHBM3XP.js.map +1 -0
  26. package/dist/chunk-WFANXVQG.js +64 -0
  27. package/dist/chunk-WFANXVQG.js.map +1 -0
  28. package/dist/chunk-WYKL32C3.js +275 -0
  29. package/dist/chunk-WYKL32C3.js.map +1 -0
  30. package/dist/chunk-ZNF7XD2S.js +134 -0
  31. package/dist/chunk-ZNF7XD2S.js.map +1 -0
  32. package/dist/config-AUAIYDSI.js +20 -0
  33. package/dist/config-AUAIYDSI.js.map +1 -0
  34. package/dist/fileFromPath-XN7LXIBI.js +134 -0
  35. package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
  36. package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
  37. package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
  38. package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
  39. package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
  40. package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
  41. package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
  42. package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
  43. package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
  44. package/dist/getMachineId-win-IIF36LEJ.js +44 -0
  45. package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
  46. package/dist/index.js +112703 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib-R6DEEJCP.js +7623 -0
  49. package/dist/lib-R6DEEJCP.js.map +1 -0
  50. package/dist/pipeline-IAVVAKTU.js +120 -0
  51. package/dist/pipeline-IAVVAKTU.js.map +1 -0
  52. package/dist/query-NTP5NVXN.js +25 -0
  53. package/dist/query-NTP5NVXN.js.map +1 -0
  54. package/dist/routing-BAEPFB7V.js +390 -0
  55. package/dist/routing-BAEPFB7V.js.map +1 -0
  56. package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
  57. package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
  58. package/dist/stripe-payment-history-SJPKA63N.js +67 -0
  59. package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
  60. package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
  61. package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
  62. package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
  63. package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
  64. package/dist/support-memory-WSG7SDKG.js +10 -0
  65. package/dist/support-memory-WSG7SDKG.js.map +1 -0
  66. package/package.json +10 -7
  67. package/.env.encrypted +0 -0
  68. package/CHANGELOG.md +0 -35
  69. package/data/tt-archive-dataset.json +0 -1
  70. package/data/validate-test-dataset.json +0 -97
  71. package/docs/CLI-AUTH.md +0 -504
  72. package/preload.ts +0 -18
  73. package/src/__tests__/init.test.ts +0 -74
  74. package/src/alignment-test.ts +0 -64
  75. package/src/check-apps.ts +0 -16
  76. package/src/commands/auth/decrypt.ts +0 -123
  77. package/src/commands/auth/encrypt.ts +0 -81
  78. package/src/commands/auth/index.ts +0 -50
  79. package/src/commands/auth/keygen.ts +0 -41
  80. package/src/commands/auth/status.ts +0 -164
  81. package/src/commands/axiom/forensic.ts +0 -868
  82. package/src/commands/axiom/index.ts +0 -697
  83. package/src/commands/build-dataset.ts +0 -311
  84. package/src/commands/db-status.ts +0 -47
  85. package/src/commands/deploys.ts +0 -219
  86. package/src/commands/eval-local/compare.ts +0 -171
  87. package/src/commands/eval-local/health.ts +0 -212
  88. package/src/commands/eval-local/index.ts +0 -76
  89. package/src/commands/eval-local/real-tools.ts +0 -416
  90. package/src/commands/eval-local/run.ts +0 -1168
  91. package/src/commands/eval-local/score-production.ts +0 -256
  92. package/src/commands/eval-local/seed.ts +0 -276
  93. package/src/commands/eval-pipeline/index.ts +0 -53
  94. package/src/commands/eval-pipeline/real-tools.ts +0 -492
  95. package/src/commands/eval-pipeline/run.ts +0 -1316
  96. package/src/commands/eval-pipeline/seed.ts +0 -395
  97. package/src/commands/eval-prompt.ts +0 -496
  98. package/src/commands/eval.test.ts +0 -253
  99. package/src/commands/eval.ts +0 -108
  100. package/src/commands/faq-classify.ts +0 -460
  101. package/src/commands/faq-cluster.ts +0 -135
  102. package/src/commands/faq-extract.ts +0 -249
  103. package/src/commands/faq-mine.ts +0 -432
  104. package/src/commands/faq-review.ts +0 -426
  105. package/src/commands/front/index.ts +0 -351
  106. package/src/commands/front/pull-conversations.ts +0 -275
  107. package/src/commands/front/tags.ts +0 -825
  108. package/src/commands/front-cache.ts +0 -1277
  109. package/src/commands/front-stats.ts +0 -75
  110. package/src/commands/health.test.ts +0 -82
  111. package/src/commands/health.ts +0 -362
  112. package/src/commands/init.test.ts +0 -89
  113. package/src/commands/init.ts +0 -106
  114. package/src/commands/inngest/client.ts +0 -294
  115. package/src/commands/inngest/events.ts +0 -296
  116. package/src/commands/inngest/investigate.ts +0 -382
  117. package/src/commands/inngest/runs.ts +0 -149
  118. package/src/commands/inngest/signal.ts +0 -143
  119. package/src/commands/kb-sync.ts +0 -498
  120. package/src/commands/memory/find.ts +0 -135
  121. package/src/commands/memory/get.ts +0 -87
  122. package/src/commands/memory/index.ts +0 -97
  123. package/src/commands/memory/stats.ts +0 -163
  124. package/src/commands/memory/store.ts +0 -49
  125. package/src/commands/memory/vote.ts +0 -159
  126. package/src/commands/pipeline.ts +0 -127
  127. package/src/commands/responses.ts +0 -856
  128. package/src/commands/tools.ts +0 -293
  129. package/src/commands/wizard.ts +0 -319
  130. package/src/index.ts +0 -172
  131. package/src/lib/crypto.ts +0 -56
  132. package/src/lib/env-loader.ts +0 -206
  133. package/src/lib/onepassword.ts +0 -137
  134. package/src/test-agent-local.ts +0 -115
  135. package/tsconfig.json +0 -11
  136. 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
- })
@@ -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
- })
@@ -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
- }