@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,304 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { execa } from 'execa'
4
+ import { Logger } from './Logger.js'
5
+ import { ensureDir } from './DevToolsDir.js'
6
+
7
+ /**
8
+ * Check if the Claude CLI is available on the system
9
+ */
10
+ async function isClaudeAvailable () {
11
+ try {
12
+ await execa('claude', ['--version'])
13
+ return true
14
+ } catch {
15
+ return false
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Read a file's content, return null if not found
21
+ */
22
+ function readFileOrNull (filePath) {
23
+ try {
24
+ return fs.readFileSync(filePath, 'utf-8')
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Build the analysis prompt for Claude CLI.
32
+ * Embeds all project files directly so Claude needs 0 tool calls (single-shot).
33
+ */
34
+ function buildPrompt () {
35
+ const projectDir = process.cwd()
36
+ const projectName = path.basename(projectDir)
37
+ const packageJson = readFileOrNull(path.join(projectDir, 'package.json'))
38
+ const configDev = readFileOrNull(path.join(projectDir, 'config.development'))
39
+ const composeFile = readFileOrNull(path.join(projectDir, 'docker-compose.yaml'))
40
+ || readFileOrNull(path.join(projectDir, 'docker-compose.yml'))
41
+ const dockerfileDev = fs.existsSync(path.join(projectDir, 'Dockerfile.dev'))
42
+ let schemaFiles = ''
43
+ const postgresDir = path.join(projectDir, 'setup', 'postgres')
44
+ if (fs.existsSync(postgresDir)) {
45
+ const sqlFiles = fs.readdirSync(postgresDir).filter(f => f.endsWith('.sql'))
46
+ for (const f of sqlFiles) {
47
+ schemaFiles += `\n--- ${f} ---\n`
48
+ schemaFiles += readFileOrNull(path.join(postgresDir, f)) || ''
49
+ }
50
+ }
51
+ return `Generate a .dev-tools/config.js file for the goki-dev CLI based on the project files below.
52
+
53
+ Project name: ${projectName}
54
+ Has Dockerfile.dev: ${dockerfileDev ? 'yes' : 'no'}
55
+
56
+ ## Project Files
57
+
58
+ ### package.json
59
+ \`\`\`json
60
+ ${packageJson || '(not found)'}
61
+ \`\`\`
62
+
63
+ ### config.development
64
+ \`\`\`
65
+ ${configDev || '(not found)'}
66
+ \`\`\`
67
+
68
+ ### docker-compose.yaml
69
+ ${composeFile
70
+ ? `\`\`\`yaml\n${composeFile}\`\`\``
71
+ : '(not found — the project needs a docker-compose.yaml)'}
72
+
73
+ ${schemaFiles
74
+ ? `### SQL Schema Files\n\`\`\`sql${schemaFiles}\n\`\`\``
75
+ : ''}
76
+
77
+ ## Config Format
78
+
79
+ The .dev-tools/config.js uses ESM (export default {}) with these fields:
80
+
81
+ | Field | Required | Type | Description |
82
+ |-------|----------|------|-------------|
83
+ | name | YES | string | Project name (use folder name) |
84
+ | services | YES | object | Which dev-tools services to enable |
85
+ | docker | YES | object | Docker compose override properties |
86
+ | docker.composeFile | YES | string | Path to base docker-compose file (usually 'docker-compose.yaml') |
87
+ | docker.dockerfile | no | string | Override Dockerfile (e.g. 'Dockerfile.dev' if it exists) |
88
+ | docker.containerName | no | string | Override container_name |
89
+ | docker.ports | no | object | { http: number } — external port mapping |
90
+ | docker.volumes | no | array | Volume mounts for hot-reload (e.g. ['./src:/app/src']) |
91
+ | docker.environment | no | object | Extra project-specific env var overrides |
92
+ | database | no | string | PostgreSQL database name (only if project uses pg/knex) |
93
+ | schemaPath | no | string | Path to SQL schema (required if database is set) |
94
+ | httpProxy | no | object | { enabled: true } for inter-service traffic monitoring |
95
+ | webhookProxy | no | object | { enabled: true, routes: {} } for webhook proxy |
96
+ | pubsub | no | object | Pub/Sub emulator config with topics |
97
+
98
+ ## Environment Override Strategy
99
+
100
+ The CLI auto-applies these env overrides based on enabled services:
101
+ - redis: true → REDIS_HOST=goki-redis, RED_LOCK_HOST=goki-redis
102
+ - pubsub: true → PUBSUB_EMULATOR_HOST=goki-pubsub-emulator:8085, PUB_SUB_KEY_FILE_NAME= (cleared)
103
+ - firestore: true → FIRESTORE_EMULATOR_HOST=goki-firestore-emulator:8080, FIRESTORE_KEY_FILE_NAME= (cleared)
104
+ - postgres: true → POSTGRES_HOST=goki-postgres
105
+ - elasticsearch: true → ELASTIC_NODE=http://goki-elasticsearch:9200
106
+
107
+ Only add docker.environment for vars NOT covered by the auto-mapping (e.g. MQTT_HOST, JWT_POSTGRES_REPOSITORY_HOST).
108
+
109
+ ## Dependency → Service Mapping
110
+
111
+ From package.json dependencies:
112
+ - pg, knex, sequelize → postgres: true, set database field
113
+ - firebase-admin, @google-cloud/firestore → firestore: true
114
+ - ioredis, redis, bull → redis: true
115
+ - @google-cloud/pubsub → pubsub: true
116
+ - @elastic/elasticsearch → elasticsearch: true
117
+
118
+ ## Pub/Sub Topic Detection
119
+
120
+ In config.development, look for PUB_SUB_SUBSCRIPTION_* env vars.
121
+ The value is the subscription name. The topic name = subscription minus "-{service-name}-sub" suffix.
122
+
123
+ Topics with subscriptions use object format: { name: 'topicName', subscription: 'topicName-service-sub' }
124
+ Topics without subscriptions (publish-only) use string format: 'topicName'
125
+
126
+ ## Port Convention
127
+
128
+ Each service maps internal port 3000 to a unique external port:
129
+ device-native:3000, device-simulator:3001, integration-apaleo:3002, core-pms:3003, core-key:3004, integration-mews:3005, integration-opera:3006
130
+
131
+ ## Example: Firestore project
132
+
133
+ \`\`\`javascript
134
+ export default {
135
+ name: 'core-key',
136
+ services: {
137
+ redis: true,
138
+ pubsub: true,
139
+ firestore: true
140
+ },
141
+ docker: {
142
+ composeFile: 'docker-compose.yaml',
143
+ dockerfile: 'Dockerfile.dev',
144
+ containerName: 'core-key-app',
145
+ ports: { http: 3004 },
146
+ volumes: ['./src:/app/src']
147
+ },
148
+ httpProxy: {
149
+ enabled: true
150
+ },
151
+ pubsub: {
152
+ projectId: 'tipi-development',
153
+ host: 'localhost',
154
+ port: 8085,
155
+ topics: [
156
+ 'connectionCreated',
157
+ { name: 'keyActionRequested', subscription: 'keyActionRequested-core-key-sub' }
158
+ ]
159
+ }
160
+ }
161
+ \`\`\`
162
+
163
+ ## Example: PostgreSQL project with extra env
164
+
165
+ \`\`\`javascript
166
+ export default {
167
+ name: 'device-simulator',
168
+ database: 'device_simulator_dev',
169
+ schemaPath: './setup/postgres/device-simulator.sql',
170
+ services: {
171
+ redis: true,
172
+ pubsub: true,
173
+ postgres: true
174
+ },
175
+ docker: {
176
+ composeFile: 'docker-compose.yaml',
177
+ containerName: 'device-simulator-app',
178
+ ports: { http: 3001 },
179
+ volumes: ['./src:/app/src'],
180
+ environment: {
181
+ POSTGRES_DATABASE: 'device_simulator_dev',
182
+ MQTT_HOST: 'goki-dev-tools-backend',
183
+ MQTT_PORT: '8883'
184
+ }
185
+ },
186
+ pubsub: {
187
+ projectId: 'tipi-development',
188
+ host: 'localhost',
189
+ port: 8085,
190
+ topics: [
191
+ { name: 'systemOneMinuteTick', subscription: 'systemOneMinuteTick-device-simulator-sub' }
192
+ ]
193
+ }
194
+ }
195
+ \`\`\`
196
+
197
+ ## Output
198
+
199
+ Output ONLY in this exact format with markers. No explanations, no markdown wrapping.
200
+
201
+ ---FILE:.dev-tools/config.js---
202
+ (file content)
203
+ ---END---`
204
+ }
205
+
206
+ /**
207
+ * Parse Claude's output to extract file contents
208
+ */
209
+ function parseGeneratedFiles (output) {
210
+ const files = {}
211
+ const filePattern = /---FILE:(.+?)---\n([\s\S]*?)---END---/g
212
+ let match
213
+ while ((match = filePattern.exec(output)) !== null) {
214
+ const filename = match[1].trim()
215
+ const content = match[2].trim()
216
+ files[filename] = content
217
+ }
218
+ return files
219
+ }
220
+
221
+ /**
222
+ * Offer to generate .dev-tools/config.js using Claude CLI.
223
+ * Returns true if config was generated successfully.
224
+ */
225
+ export async function offerConfigGeneration () {
226
+ const projectDir = process.cwd()
227
+ const configPath = path.join(projectDir, '.dev-tools', 'config.js')
228
+ if (fs.existsSync(configPath)) {
229
+ Logger.error('.dev-tools/config.js exists but has errors. Please fix it manually.')
230
+ return false
231
+ }
232
+ const claudeAvailable = await isClaudeAvailable()
233
+ if (!claudeAvailable) {
234
+ Logger.warn('Claude CLI not found. Install it to auto-generate config.')
235
+ console.log('\n To install: npm install -g @anthropic-ai/claude-code')
236
+ console.log(' Or create .dev-tools/config.js manually (see dev-tools/CLAUDE.md)\n')
237
+ return false
238
+ }
239
+ const inquirer = await import('inquirer')
240
+ const { shouldGenerate } = await inquirer.default.prompt([{
241
+ type: 'confirm',
242
+ name: 'shouldGenerate',
243
+ message: 'No .dev-tools/config.js found. Generate one using Claude AI?',
244
+ default: true
245
+ }])
246
+ if (!shouldGenerate) return false
247
+ console.log('\n Running Claude CLI to analyze project...\n')
248
+ try {
249
+ const prompt = buildPrompt()
250
+ const subprocess = execa('claude', [
251
+ '-p', prompt,
252
+ '--output-format', 'text',
253
+ '--max-turns', '1'
254
+ ], {
255
+ cwd: projectDir,
256
+ stdin: 'ignore',
257
+ timeout: 120000
258
+ })
259
+ if (subprocess.stderr) {
260
+ subprocess.stderr.pipe(process.stderr)
261
+ }
262
+ const result = await subprocess
263
+ const files = parseGeneratedFiles(result.stdout)
264
+ if (!files['.dev-tools/config.js']) {
265
+ Logger.error('Claude did not generate a valid config')
266
+ console.log('\nRaw output:')
267
+ console.log(result.stdout)
268
+ return false
269
+ }
270
+ // Ensure .dev-tools/ directory exists
271
+ ensureDir(projectDir)
272
+ // Write generated files
273
+ for (const [filename, content] of Object.entries(files)) {
274
+ const filePath = path.join(projectDir, filename)
275
+ const dir = path.dirname(filePath)
276
+ if (!fs.existsSync(dir)) {
277
+ fs.mkdirSync(dir, { recursive: true })
278
+ }
279
+ fs.writeFileSync(filePath, content + '\n')
280
+ }
281
+ Logger.success(`Generated ${Object.keys(files).length} file(s)`)
282
+ console.log('\n Generated files:')
283
+ for (const filename of Object.keys(files)) {
284
+ console.log(` - ${filename}`)
285
+ }
286
+ console.log('\n --- .dev-tools/config.js ---')
287
+ console.log(files['.dev-tools/config.js'].split('\n').map(l => ' ' + l).join('\n'))
288
+ console.log(' ---\n')
289
+ const { proceed } = await inquirer.default.prompt([{
290
+ type: 'confirm',
291
+ name: 'proceed',
292
+ message: 'Config looks good? Continue with start?',
293
+ default: true
294
+ }])
295
+ if (!proceed) {
296
+ console.log('\n Edit .dev-tools/config.js and run goki-dev start again.\n')
297
+ return false
298
+ }
299
+ return true
300
+ } catch (error) {
301
+ Logger.error(`Failed to generate config: ${error.message}`)
302
+ return false
303
+ }
304
+ }
@@ -0,0 +1,46 @@
1
+ import fs from 'fs'
2
+ import { ensureDir, getPath } from './DevToolsDir.js'
3
+
4
+ const LOCAL_FILE = 'local.json'
5
+
6
+ export class ConfigManager {
7
+ static getConfigPath (projectDir) {
8
+ return getPath(projectDir || process.cwd(), LOCAL_FILE)
9
+ }
10
+
11
+ static load (projectDir) {
12
+ const dir = projectDir || process.cwd()
13
+ const configPath = this.getConfigPath(dir)
14
+ if (!fs.existsSync(configPath)) {
15
+ return {}
16
+ }
17
+ try {
18
+ const content = fs.readFileSync(configPath, 'utf-8')
19
+ return JSON.parse(content)
20
+ } catch {
21
+ return {}
22
+ }
23
+ }
24
+
25
+ static save (config, projectDir) {
26
+ const dir = projectDir || process.cwd()
27
+ ensureDir(dir)
28
+ const configPath = this.getConfigPath(dir)
29
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
30
+ }
31
+
32
+ static getMcpPreference (projectDir) {
33
+ const config = this.load(projectDir)
34
+ return config.mcpPermissions?.prompted ? config.mcpPermissions : null
35
+ }
36
+
37
+ static saveMcpPreference (allowed, projectDir) {
38
+ const config = this.load(projectDir)
39
+ config.mcpPermissions = {
40
+ prompted: true,
41
+ allowed,
42
+ promptedAt: new Date().toISOString()
43
+ }
44
+ this.save(config, projectDir)
45
+ }
46
+ }
@@ -0,0 +1,94 @@
1
+ import pg from 'pg'
2
+ import fs from 'fs/promises'
3
+
4
+ const { Client } = pg
5
+
6
+ export class DatabaseManager {
7
+ constructor ({ host, port, user, password, database }) {
8
+ this.config = { host, port, user, password }
9
+ this.database = database
10
+ }
11
+
12
+ async isReachable (maxRetries = 5, delayMs = 2000) {
13
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
14
+ try {
15
+ const client = new Client({ ...this.config, database: 'postgres' })
16
+ await client.connect()
17
+ await client.end()
18
+ return true
19
+ } catch (error) {
20
+ if (attempt === maxRetries) {
21
+ // Log the actual error on final attempt
22
+ console.error(`PostgreSQL connection failed after ${maxRetries} attempts:`, error.message)
23
+ return false
24
+ }
25
+ // Wait before retrying
26
+ await new Promise(resolve => setTimeout(resolve, delayMs))
27
+ }
28
+ }
29
+ return false
30
+ }
31
+
32
+ async databaseExists () {
33
+ const client = new Client({ ...this.config, database: 'postgres' })
34
+ await client.connect()
35
+ const result = await client.query(
36
+ 'SELECT 1 FROM pg_database WHERE datname = $1',
37
+ [this.database]
38
+ )
39
+ await client.end()
40
+ return result.rows.length > 0
41
+ }
42
+
43
+ async createDatabase () {
44
+ const client = new Client({ ...this.config, database: 'postgres' })
45
+ await client.connect()
46
+ await client.query(`CREATE DATABASE "${this.database}"`)
47
+ await client.end()
48
+ }
49
+
50
+ async dropDatabase () {
51
+ const client = new Client({ ...this.config, database: 'postgres' })
52
+ await client.connect()
53
+ await client.query(`DROP DATABASE IF EXISTS "${this.database}"`)
54
+ await client.end()
55
+ }
56
+
57
+ async runSchema (schemaPath) {
58
+ const sql = await fs.readFile(schemaPath, 'utf8')
59
+ const client = new Client({ ...this.config, database: this.database })
60
+ await client.connect()
61
+ await client.query(sql)
62
+ await client.end()
63
+ }
64
+
65
+ async verify () {
66
+ const client = new Client({ ...this.config, database: this.database })
67
+ await client.connect()
68
+ const result = await client.query(`
69
+ SELECT
70
+ (SELECT COUNT(*) FROM information_schema.tables
71
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE') as tables,
72
+ (SELECT COUNT(*) FROM information_schema.views
73
+ WHERE table_schema = 'public') as views,
74
+ (SELECT COUNT(*) FROM information_schema.routines
75
+ WHERE routine_schema = 'public' AND routine_type = 'FUNCTION') as functions
76
+ `)
77
+ await client.end()
78
+ const { tables, views, functions } = result.rows[0]
79
+ return { tables: parseInt(tables), views: parseInt(views), functions: parseInt(functions) }
80
+ }
81
+
82
+ async clear () {
83
+ const client = new Client({ ...this.config, database: this.database })
84
+ await client.connect()
85
+ const result = await client.query(`
86
+ SELECT tablename FROM pg_tables
87
+ WHERE schemaname = 'public'
88
+ `)
89
+ for (const row of result.rows) {
90
+ await client.query(`TRUNCATE TABLE "${row.tablename}" CASCADE`)
91
+ }
92
+ await client.end()
93
+ }
94
+ }
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk'
2
+
3
+ export class DevToolsChecker {
4
+ static async isRunning () {
5
+ try {
6
+ const response = await fetch('http://localhost:9000/v1/health/readiness')
7
+ return response.ok
8
+ } catch {
9
+ return false
10
+ }
11
+ }
12
+
13
+ static showStartInstructions () {
14
+ console.log('\n📦 Dev-tools is not running.\n')
15
+ console.log('To start dev-tools (Pub/Sub, MQTT, Redis, PostgreSQL):')
16
+ console.log(chalk.cyan('\n cd ../dev-tools'))
17
+ console.log(chalk.cyan(' docker-compose -f docker-compose.services.yml up -d'))
18
+ console.log(chalk.cyan(' docker-compose -f docker-compose.dev.yml up -d\n'))
19
+ console.log('Then try again.\n')
20
+ }
21
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ const DIR_NAME = '.dev-tools'
5
+ const CONFIG_FILE = 'config.js'
6
+
7
+ /**
8
+ * Ensure the .dev-tools/ directory exists in the project
9
+ */
10
+ export function ensureDir (projectDir) {
11
+ const dirPath = path.join(projectDir, DIR_NAME)
12
+ if (!fs.existsSync(dirPath)) {
13
+ fs.mkdirSync(dirPath, { recursive: true })
14
+ }
15
+ return dirPath
16
+ }
17
+
18
+ /**
19
+ * Get the full path to a file inside .dev-tools/
20
+ */
21
+ export function getPath (projectDir, filename) {
22
+ return path.join(projectDir, DIR_NAME, filename)
23
+ }
24
+
25
+ /**
26
+ * Check if .dev-tools/config.js exists
27
+ */
28
+ export function configExists (projectDir) {
29
+ return fs.existsSync(path.join(projectDir, DIR_NAME, CONFIG_FILE))
30
+ }
31
+
32
+ /**
33
+ * Load the config from .dev-tools/config.js via dynamic import
34
+ * Returns the default export
35
+ */
36
+ export async function loadConfig (projectDir) {
37
+ const configPath = path.join(projectDir, DIR_NAME, CONFIG_FILE)
38
+ if (!fs.existsSync(configPath)) return null
39
+ const config = await import(configPath)
40
+ return config.default
41
+ }
42
+
43
+ /**
44
+ * Ensure .dev-tools/ is in the project's .gitignore
45
+ */
46
+ export function ensureGitignore (projectDir) {
47
+ const gitignorePath = path.join(projectDir, '.gitignore')
48
+ if (!fs.existsSync(gitignorePath)) return
49
+ const content = fs.readFileSync(gitignorePath, 'utf-8')
50
+ const lines = content.split('\n')
51
+ const patterns = [DIR_NAME + '/', '.mcp.json']
52
+ const toAdd = []
53
+ for (const pattern of patterns) {
54
+ const alreadyIgnored = lines.some(line => {
55
+ const trimmed = line.trim()
56
+ return trimmed === pattern || trimmed === `/${pattern}`
57
+ })
58
+ if (!alreadyIgnored) {
59
+ toAdd.push(pattern)
60
+ }
61
+ }
62
+ if (toAdd.length === 0) return
63
+ const endsWithNewline = content.endsWith('\n')
64
+ const separator = endsWithNewline ? '' : '\n'
65
+ fs.appendFileSync(gitignorePath, `${separator}${toAdd.join('\n')}\n`)
66
+ }