@gokiteam/goki-dev 0.2.0

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 (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,129 @@
1
+ import http from 'http'
2
+
3
+ export class PubSubManager {
4
+ constructor (config) {
5
+ this.host = config.host || 'localhost'
6
+ this.port = config.port || 8085
7
+ this.projectId = config.projectId || 'tipi-development'
8
+ }
9
+
10
+ async request (method, path, body = null) {
11
+ return new Promise((resolve, reject) => {
12
+ const options = {
13
+ hostname: this.host,
14
+ port: this.port,
15
+ path: `/v1/projects/${this.projectId}${path}`,
16
+ method,
17
+ headers: { 'Content-Type': 'application/json' }
18
+ }
19
+ const req = http.request(options, (res) => {
20
+ let data = ''
21
+ res.on('data', chunk => { data += chunk })
22
+ res.on('end', () => {
23
+ try {
24
+ resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} })
25
+ } catch {
26
+ resolve({ status: res.statusCode, data })
27
+ }
28
+ })
29
+ })
30
+ req.on('error', reject)
31
+ if (body) req.write(JSON.stringify(body))
32
+ req.end()
33
+ })
34
+ }
35
+
36
+ async isReachable () {
37
+ try {
38
+ const { status } = await this.request('GET', '/topics')
39
+ return status === 200
40
+ } catch {
41
+ return false
42
+ }
43
+ }
44
+
45
+ async topicExists (topicName) {
46
+ const { status } = await this.request('GET', `/topics/${topicName}`)
47
+ return status === 200
48
+ }
49
+
50
+ async subscriptionExists (subscriptionName) {
51
+ const { status } = await this.request('GET', `/subscriptions/${subscriptionName}`)
52
+ return status === 200
53
+ }
54
+
55
+ async createTopic (topicName) {
56
+ const { status, data } = await this.request('PUT', `/topics/${topicName}`)
57
+ if (status !== 200 && status !== 409) {
58
+ throw new Error(`Failed to create topic ${topicName}: ${JSON.stringify(data)}`)
59
+ }
60
+ return status === 200 ? 'created' : 'exists'
61
+ }
62
+
63
+ async createSubscription (subscriptionName, topicName) {
64
+ const { status, data } = await this.request('PUT', `/subscriptions/${subscriptionName}`, {
65
+ topic: `projects/${this.projectId}/topics/${topicName}`
66
+ })
67
+ if (status !== 200 && status !== 409) {
68
+ throw new Error(`Failed to create subscription ${subscriptionName}: ${JSON.stringify(data)}`)
69
+ }
70
+ return status === 200 ? 'created' : 'exists'
71
+ }
72
+
73
+ async initializeAll (topics = []) {
74
+ const results = { topics: [], subscriptions: [] }
75
+ const normalized = topics.map(t => typeof t === 'string' ? { name: t } : t)
76
+ for (const entry of normalized) {
77
+ const result = await this.createTopic(entry.name)
78
+ results.topics.push({ name: entry.name, result })
79
+ }
80
+ for (const entry of normalized) {
81
+ if (!entry.subscription) continue
82
+ const result = await this.createSubscription(entry.subscription, entry.name)
83
+ results.subscriptions.push({ name: entry.subscription, topic: entry.name, result })
84
+ }
85
+ return results
86
+ }
87
+
88
+ /**
89
+ * Register topics with dev-tools persistent registry for auto-recovery.
90
+ * Fire-and-forget — silently fails if dev-tools backend is unavailable.
91
+ */
92
+ async registerWithDevTools (topics, projectName) {
93
+ try {
94
+ const body = JSON.stringify({ projectName, topics })
95
+ return new Promise((resolve) => {
96
+ const options = {
97
+ hostname: 'localhost',
98
+ port: 9000,
99
+ path: '/v1/pubsub/registry/register',
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'Content-Length': Buffer.byteLength(body)
104
+ }
105
+ }
106
+ const req = http.request(options, (res) => {
107
+ let data = ''
108
+ res.on('data', chunk => { data += chunk })
109
+ res.on('end', () => {
110
+ try {
111
+ resolve(JSON.parse(data))
112
+ } catch {
113
+ resolve(null)
114
+ }
115
+ })
116
+ })
117
+ req.on('error', () => resolve(null))
118
+ req.setTimeout(5000, () => {
119
+ req.destroy()
120
+ resolve(null)
121
+ })
122
+ req.write(body)
123
+ req.end()
124
+ })
125
+ } catch {
126
+ return null
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,88 @@
1
+ export class SnapshotManager {
2
+ constructor (config) {
3
+ this.config = config
4
+ this.devToolsUrl = 'http://localhost:9000'
5
+ }
6
+
7
+ async createSnapshot (services = []) {
8
+ const response = await fetch(`${this.devToolsUrl}/v1/snapshots/create`, {
9
+ method: 'POST',
10
+ headers: { 'Content-Type': 'application/json' },
11
+ body: JSON.stringify({ services })
12
+ })
13
+ if (!response.ok) {
14
+ const error = await response.text()
15
+ throw new Error(`Failed to create snapshot: ${error}`)
16
+ }
17
+ const result = await response.json()
18
+ if (result.status === 'error') {
19
+ throw new Error(result.message)
20
+ }
21
+ return result
22
+ }
23
+
24
+ async restoreSnapshot (snapshotId, restartServices = true) {
25
+ const response = await fetch(`${this.devToolsUrl}/v1/snapshots/restore`, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ snapshotId, restartServices })
29
+ })
30
+ if (!response.ok) {
31
+ const error = await response.text()
32
+ throw new Error(`Failed to restore snapshot: ${error}`)
33
+ }
34
+ const result = await response.json()
35
+ if (result.status === 'error') {
36
+ throw new Error(result.message)
37
+ }
38
+ return result
39
+ }
40
+
41
+ async listSnapshots () {
42
+ const response = await fetch(`${this.devToolsUrl}/v1/snapshots/list`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({})
46
+ })
47
+ if (!response.ok) {
48
+ const error = await response.text()
49
+ throw new Error(`Failed to list snapshots: ${error}`)
50
+ }
51
+ const result = await response.json()
52
+ return result
53
+ }
54
+
55
+ async getSnapshotDetails (snapshotId) {
56
+ const response = await fetch(`${this.devToolsUrl}/v1/snapshots/details`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({ snapshotId })
60
+ })
61
+ if (!response.ok) {
62
+ const error = await response.text()
63
+ throw new Error(`Failed to get snapshot details: ${error}`)
64
+ }
65
+ const result = await response.json()
66
+ if (result.status === 'error') {
67
+ throw new Error(result.message)
68
+ }
69
+ return result
70
+ }
71
+
72
+ async cleanupSnapshot (snapshotId = null) {
73
+ const response = await fetch(`${this.devToolsUrl}/v1/snapshots/delete`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify(snapshotId ? { snapshotId } : {})
77
+ })
78
+ if (!response.ok) {
79
+ const error = await response.text()
80
+ throw new Error(`Failed to delete snapshot: ${error}`)
81
+ }
82
+ const result = await response.json()
83
+ if (result.status === 'error') {
84
+ throw new Error(result.message)
85
+ }
86
+ return result
87
+ }
88
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * UI/UX formatting utilities for CLI output
3
+ * Provides color, clickable links, and visual hierarchy
4
+ */
5
+
6
+ // ANSI color codes
7
+ const colors = {
8
+ reset: '\x1b[0m',
9
+ bright: '\x1b[1m',
10
+ dim: '\x1b[2m',
11
+ cyan: '\x1b[36m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ blue: '\x1b[34m',
15
+ magenta: '\x1b[35m',
16
+ gray: '\x1b[90m'
17
+ }
18
+
19
+ /**
20
+ * Creates a clickable link in terminal (supported by most modern terminals)
21
+ * Format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\
22
+ */
23
+ export function link (url, text = url) {
24
+ return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
25
+ }
26
+
27
+ /**
28
+ * Color text
29
+ */
30
+ export function color (text, colorName) {
31
+ return `${colors[colorName] || ''}${text}${colors.reset}`
32
+ }
33
+
34
+ /**
35
+ * Dim/muted text
36
+ */
37
+ export function dim (text) {
38
+ return color(text, 'dim')
39
+ }
40
+
41
+ /**
42
+ * Bright/bold text
43
+ */
44
+ export function bright (text) {
45
+ return color(text, 'bright')
46
+ }
47
+
48
+ /**
49
+ * Format a section header
50
+ */
51
+ export function section (title) {
52
+ return `\n${color('━━━', 'dim')} ${bright(title)}`
53
+ }
54
+
55
+ /**
56
+ * Format a service line with optional link
57
+ */
58
+ export function service (icon, label, url, meta = null) {
59
+ const labelPart = `${icon} ${bright(label)}`
60
+ const urlPart = url ? link(url, color(url, 'cyan')) : ''
61
+ const metaPart = meta ? dim(` ${meta}`) : ''
62
+ return ` ${labelPart}${urlPart ? `\n ${urlPart}` : ''}${metaPart}`
63
+ }
64
+
65
+ /**
66
+ * Format a compact service line (label + URL on same line)
67
+ */
68
+ export function compactService (icon, label, url, meta = null) {
69
+ const labelPart = `${icon} ${label.padEnd(16)}`
70
+ const urlPart = url ? link(url, color(url, 'cyan')) : ''
71
+ const metaPart = meta ? dim(` ${meta}`) : ''
72
+ return ` ${labelPart}${urlPart}${metaPart}`
73
+ }
74
+
75
+ /**
76
+ * Format a sub-item (indented)
77
+ */
78
+ export function subItem (label, value, highlighted = false) {
79
+ const valueColor = highlighted ? 'green' : 'cyan'
80
+ return ` ${dim('•')} ${label}: ${color(value, valueColor)}`
81
+ }
82
+
83
+ /**
84
+ * Format a command
85
+ */
86
+ export function command (cmd, description) {
87
+ return ` ${color(cmd.padEnd(20), 'yellow')} ${dim(description)}`
88
+ }
89
+
90
+ /**
91
+ * Create a visual separator
92
+ */
93
+ export function separator () {
94
+ return color(' ─'.repeat(35), 'dim')
95
+ }
96
+
97
+ /**
98
+ * Format a tip/hint
99
+ */
100
+ export function tip (text) {
101
+ return `\n ${dim('💡 Tip:')} ${text}\n`
102
+ }
103
+
104
+ /**
105
+ * Format a warning
106
+ */
107
+ export function warning (text) {
108
+ return ` ${color('⚠️ Warning:', 'yellow')} ${text}`
109
+ }
110
+
111
+ /**
112
+ * Format success message
113
+ */
114
+ export function success (text) {
115
+ return `\n${color('✨ ' + text, 'green')}\n`
116
+ }
117
+
118
+ /**
119
+ * Create a box around text
120
+ */
121
+ export function box (lines) {
122
+ const maxLen = Math.max(...lines.map(l => stripAnsi(l).length))
123
+ const top = `┌${'─'.repeat(maxLen + 2)}┐`
124
+ const bottom = `└${'─'.repeat(maxLen + 2)}┘`
125
+ const content = lines.map(line => {
126
+ const padding = maxLen - stripAnsi(line).length
127
+ return `│ ${line}${' '.repeat(padding)} │`
128
+ })
129
+ return [top, ...content, bottom].join('\n')
130
+ }
131
+
132
+ /**
133
+ * Strip ANSI codes for length calculation
134
+ */
135
+ function stripAnsi (str) {
136
+ return str.replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b]8;;.*?\x1b\\/g, '')
137
+ }
138
+
139
+ /**
140
+ * Create a simple table with aligned columns
141
+ */
142
+ export function table (rows, options = {}) {
143
+ if (rows.length === 0) return ''
144
+ const { indent = ' ', separator = ' ' } = options
145
+ // Calculate column widths
146
+ const colCount = rows[0].length
147
+ const widths = Array(colCount).fill(0)
148
+ for (const row of rows) {
149
+ for (let i = 0; i < row.length; i++) {
150
+ const len = stripAnsi(row[i]).length
151
+ if (len > widths[i]) widths[i] = len
152
+ }
153
+ }
154
+ // Format rows
155
+ const lines = []
156
+ for (let i = 0; i < rows.length; i++) {
157
+ const row = rows[i]
158
+ const cells = row.map((cell, j) => {
159
+ const padding = widths[j] - stripAnsi(cell).length
160
+ return cell + ' '.repeat(padding)
161
+ })
162
+ const line = indent + cells.join(separator)
163
+ lines.push(line)
164
+ // Add separator after header (first row)
165
+ if (i === 0 && rows.length > 1) {
166
+ const sepLine = indent + widths.map(w => dim('─'.repeat(w))).join(separator)
167
+ lines.push(sepLine)
168
+ }
169
+ }
170
+ return lines.join('\n')
171
+ }
172
+
173
+ /**
174
+ * Format the startup success output with all service URLs and info
175
+ */
176
+ export function formatStartupOutput (options) {
177
+ const {
178
+ projectName,
179
+ appPort,
180
+ webhookResult,
181
+ webhookUrlResult,
182
+ proxyResult,
183
+ overrideResult,
184
+ mcpResult
185
+ } = options
186
+
187
+ const lines = []
188
+
189
+ // Success header
190
+ lines.push(success(`${projectName} is ready!`))
191
+
192
+ // Main services section
193
+ lines.push(section('Services'))
194
+ const serviceRows = [
195
+ [dim('Service'), dim('URL')]
196
+ ]
197
+ if (appPort) {
198
+ serviceRows.push([
199
+ '🌐 Application',
200
+ link(`http://localhost:${appPort}`, color(`http://localhost:${appPort}`, 'cyan'))
201
+ ])
202
+ }
203
+ serviceRows.push([
204
+ '🎛️ Dev Tools',
205
+ link('http://localhost:9001', color('http://localhost:9001', 'cyan'))
206
+ ])
207
+ lines.push(table(serviceRows, { indent: ' ' }))
208
+
209
+ // Webhook tunnel section
210
+ if (webhookResult?.tunnelUrl) {
211
+ lines.push(section('Webhook Tunnel'))
212
+ const pidInfo = webhookResult.pid ? dim(` (PID ${webhookResult.pid})`) : ''
213
+ const tunnelUrl = link(webhookResult.tunnelUrl, color(webhookResult.tunnelUrl, 'cyan'))
214
+ lines.push(` 🔗 ${tunnelUrl}${pidInfo}`)
215
+
216
+ if (webhookResult.routes?.length > 0) {
217
+ lines.push('')
218
+ const routeRows = [
219
+ [dim('Route'), dim('Webhook URL')]
220
+ ]
221
+ for (const prefix of webhookResult.routes) {
222
+ const url = `${webhookResult.tunnelUrl}/v1/webhooks/proxy/${prefix}`
223
+ routeRows.push([
224
+ color(prefix, 'yellow'),
225
+ link(url, color(url, 'cyan'))
226
+ ])
227
+ }
228
+ lines.push(table(routeRows, { indent: ' ' }))
229
+ lines.push(` ${dim('Stop with:')} ${color('goki-dev ngrok stop', 'yellow')}`)
230
+ }
231
+ } else if (webhookResult?.routes?.length > 0) {
232
+ lines.push(section('Webhook Tunnel'))
233
+ lines.push(warning('Tunnel not available — install ngrok for external access'))
234
+ }
235
+
236
+ // HTTP proxy section
237
+ if (proxyResult && proxyResult.rewrites?.length > 0) {
238
+ lines.push(section('HTTP Traffic Monitoring'))
239
+ lines.push(` ${color(proxyResult.rewrites.length, 'green')} URL(s) proxied through dev-tools`)
240
+
241
+ // Build table rows
242
+ const tableRows = [
243
+ [dim('Variable'), dim('Original URL'), dim('Status')]
244
+ ]
245
+ for (const r of proxyResult.rewrites) {
246
+ tableRows.push([
247
+ color(r.key, 'yellow'),
248
+ dim(r.original),
249
+ color('✓ proxied', 'green')
250
+ ])
251
+ }
252
+ if (proxyResult.skipped?.length > 0) {
253
+ for (const s of proxyResult.skipped) {
254
+ tableRows.push([
255
+ color(s.key, 'gray'),
256
+ dim(s.original),
257
+ dim('○ skipped')
258
+ ])
259
+ }
260
+ }
261
+
262
+ lines.push(table(tableRows, { indent: ' ' }))
263
+ }
264
+
265
+ // Webhook URL Substitution section
266
+ if (webhookUrlResult && webhookUrlResult.rewrites?.length > 0) {
267
+ lines.push(section('Webhook URL Substitution'))
268
+ lines.push(` ${color(webhookUrlResult.rewrites.length, 'green')} placeholder(s) replaced`)
269
+ lines.push('')
270
+
271
+ const tableRows = [
272
+ [dim('Variable'), dim('Substituted URL')]
273
+ ]
274
+ for (const r of webhookUrlResult.rewrites) {
275
+ tableRows.push([
276
+ color(r.key, 'yellow'),
277
+ dim(r.substituted)
278
+ ])
279
+ }
280
+
281
+ lines.push(table(tableRows, { indent: ' ' }))
282
+ }
283
+
284
+ // MCP config info
285
+ if (mcpResult && !mcpResult.skipped) {
286
+ const action = mcpResult.created ? 'Created' : 'Updated'
287
+ lines.push(` ${dim('🤖')} ${action} ${color('.mcp.json', 'cyan')} ${dim('— Claude Code ready')}`)
288
+ }
289
+
290
+ lines.push('')
291
+ return lines.join('\n')
292
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'path'
2
+ import { collectAllEnvVars } from './ComposeParser.js'
3
+
4
+ const WEBHOOK_PLACEHOLDER = '{DEV_TOOLS_WEBHOOK_BASE_URL}'
5
+
6
+ /**
7
+ * Compute webhook URL rewrites as pure data (no file writing)
8
+ *
9
+ * @param {object} config - The config object
10
+ * @param {string} projectDir - Absolute path to project root
11
+ * @param {string} tunnelUrl - The ngrok tunnel URL
12
+ * @returns {{ rewrites: Array<{key, original, substituted}>, serviceName: string }} or null
13
+ */
14
+ export function computeWebhookRewrites (config, projectDir, tunnelUrl) {
15
+ if (!tunnelUrl) return null
16
+ const docker = config.docker || {}
17
+ const composeFile = docker.composeFile || 'docker-compose.yaml'
18
+ const envFilePath = path.join(projectDir, 'config.development')
19
+ const composePath = path.join(projectDir, composeFile)
20
+ const { allVars, serviceName } = collectAllEnvVars(envFilePath, composePath)
21
+ if (!serviceName) return null
22
+ const rewrites = []
23
+ const webhookBaseUrl = `${tunnelUrl}/v1/webhooks/proxy`
24
+ for (const [key, value] of Object.entries(allVars)) {
25
+ if (value.includes(WEBHOOK_PLACEHOLDER)) {
26
+ const substituted = value.replace(WEBHOOK_PLACEHOLDER, webhookBaseUrl)
27
+ rewrites.push({ key, original: value, substituted })
28
+ }
29
+ }
30
+ if (rewrites.length === 0) return null
31
+ return { rewrites, serviceName }
32
+ }
@@ -0,0 +1,125 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import crypto from 'crypto'
5
+ import chalk from 'chalk'
6
+ import inquirer from 'inquirer'
7
+
8
+ const CONFIG_DIR = '.goki-dev'
9
+ const PREFS_FILE = 'preferences.json'
10
+
11
+ export class BiometricAuth {
12
+ constructor (preferences = {}) {
13
+ this._enabled = preferences.biometric !== false
14
+ this._passphraseHash = preferences.passphraseHash || null
15
+ }
16
+
17
+ async isAvailable () {
18
+ if (process.platform !== 'darwin') return false
19
+ try {
20
+ const touchid = await import('macos-touchid')
21
+ return !!touchid.default
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ async authenticate (reason = 'access secure credentials') {
28
+ if (this._enabled && await this.isAvailable()) {
29
+ try {
30
+ const touchid = (await import('macos-touchid')).default
31
+ await new Promise((resolve, reject) => {
32
+ touchid.authenticate(reason, (err) => {
33
+ if (err) reject(err)
34
+ else resolve()
35
+ })
36
+ })
37
+ return true
38
+ } catch (err) {
39
+ console.log(chalk.yellow('Touch ID failed, falling back to passphrase...'))
40
+ }
41
+ }
42
+ return this._authenticateWithPassphrase()
43
+ }
44
+
45
+ get enabled () {
46
+ return this._enabled
47
+ }
48
+
49
+ set enabled (value) {
50
+ this._enabled = value
51
+ }
52
+
53
+ async _authenticateWithPassphrase () {
54
+ if (!this._passphraseHash) {
55
+ return this._setupPassphrase()
56
+ }
57
+ const { passphrase } = await inquirer.prompt([{
58
+ type: 'password',
59
+ name: 'passphrase',
60
+ message: 'Enter your passphrase:',
61
+ mask: '*'
62
+ }])
63
+ const hash = crypto.createHash('sha256').update(passphrase).digest('hex')
64
+ if (hash !== this._passphraseHash) {
65
+ throw new Error('Authentication failed: incorrect passphrase')
66
+ }
67
+ return true
68
+ }
69
+
70
+ async _setupPassphrase () {
71
+ console.log(chalk.blue('Setting up passphrase for the first time...'))
72
+ const { passphrase } = await inquirer.prompt([{
73
+ type: 'password',
74
+ name: 'passphrase',
75
+ message: 'Create a passphrase:',
76
+ mask: '*',
77
+ validate: (input) => input.length >= 4 || 'Passphrase must be at least 4 characters'
78
+ }])
79
+ const { confirm } = await inquirer.prompt([{
80
+ type: 'password',
81
+ name: 'confirm',
82
+ message: 'Confirm passphrase:',
83
+ mask: '*'
84
+ }])
85
+ if (passphrase !== confirm) {
86
+ throw new Error('Passphrases do not match')
87
+ }
88
+ this._passphraseHash = crypto.createHash('sha256').update(passphrase).digest('hex')
89
+ const prefs = BiometricAuth.loadPreferences()
90
+ prefs.passphraseHash = this._passphraseHash
91
+ BiometricAuth.savePreferences(prefs)
92
+ console.log(chalk.green('Passphrase saved.'))
93
+ return true
94
+ }
95
+
96
+ static loadPreferences () {
97
+ const prefsPath = path.join(BiometricAuth.getConfigDir(), PREFS_FILE)
98
+ if (!fs.existsSync(prefsPath)) {
99
+ return { biometric: true }
100
+ }
101
+ try {
102
+ const content = fs.readFileSync(prefsPath, 'utf-8')
103
+ return JSON.parse(content)
104
+ } catch {
105
+ return { biometric: true }
106
+ }
107
+ }
108
+
109
+ static savePreferences (prefs) {
110
+ const configDir = BiometricAuth.getConfigDir()
111
+ if (!fs.existsSync(configDir)) {
112
+ fs.mkdirSync(configDir, { recursive: true })
113
+ }
114
+ const prefsPath = path.join(configDir, PREFS_FILE)
115
+ fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2) + '\n')
116
+ }
117
+
118
+ static getConfigDir () {
119
+ const dir = path.join(os.homedir(), CONFIG_DIR)
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true })
122
+ }
123
+ return dir
124
+ }
125
+ }