@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,302 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander'
3
+ import chalk from 'chalk'
4
+ import { createRequire } from 'module'
5
+ import { SecretsManager } from '../cli/secrets/SecretsManager.js'
6
+ import { SecretInjector } from '../cli/secrets/SecretInjector.js'
7
+ import { SecretsConfig } from '../cli/secrets/SecretsConfig.js'
8
+ import { SecretsDoctor } from '../cli/secrets/SecretsDoctor.js'
9
+ import { BiometricAuth } from '../cli/secrets/BiometricAuth.js'
10
+
11
+ const require = createRequire(import.meta.url)
12
+ const pkg = require('../package.json')
13
+
14
+ async function createManager (options = {}) {
15
+ const manager = await SecretsManager.create({ projectDir: process.cwd() })
16
+ if (manager.projectName && !options.silent) {
17
+ console.log(chalk.blue(' \u2139'), `project detected: ${manager.projectName}`)
18
+ }
19
+ return manager
20
+ }
21
+
22
+ const program = new Command()
23
+
24
+ program
25
+ .name('goki-secrets')
26
+ .description('Goki secrets manager \u2014 macOS Keychain-backed credentials')
27
+ .version(pkg.version)
28
+ .enablePositionalOptions()
29
+
30
+ // --- CRUD ---
31
+
32
+ program
33
+ .command('set <key> [value]')
34
+ .description('Set a secret (value prompted if omitted)')
35
+ .option('--global', 'Force global scope')
36
+ .option('--description <desc>', 'Optional metadata description')
37
+ .action(async (key, value, options) => {
38
+ try {
39
+ if (!value) {
40
+ const inquirer = (await import('inquirer')).default
41
+ const answers = await inquirer.prompt([{
42
+ type: 'password',
43
+ name: 'value',
44
+ message: `Enter value for ${key}:`,
45
+ mask: '\u25CF'
46
+ }])
47
+ value = answers.value
48
+ }
49
+ const manager = await createManager()
50
+ await manager.unlock()
51
+ const scope = options.global ? 'global' : undefined
52
+ await manager.set(key, value, { scope, description: options.description })
53
+ const resolvedScope = options.global ? 'global' : (manager.projectName ? `project: ${manager.projectName}` : 'global')
54
+ console.log(chalk.green(` \u2713 set ${key} (${resolvedScope})`))
55
+ } catch (error) {
56
+ console.error(chalk.red(` \u2717 ${error.message}`))
57
+ process.exit(1)
58
+ }
59
+ })
60
+
61
+ program
62
+ .command('get <key>')
63
+ .description('Print secret value to stdout')
64
+ .option('--global', 'Force global scope')
65
+ .action(async (key, options) => {
66
+ try {
67
+ const manager = await createManager({ silent: true })
68
+ await manager.unlock()
69
+ let value
70
+ if (options.global) {
71
+ const globalSecrets = manager.getGlobalSecrets()
72
+ value = globalSecrets[key]
73
+ } else {
74
+ value = manager.get(key)
75
+ }
76
+ if (value === undefined) {
77
+ console.error(chalk.red(` \u2717 secret not found: ${key}`))
78
+ process.exit(1)
79
+ }
80
+ process.stdout.write(value)
81
+ } catch (error) {
82
+ console.error(chalk.red(` \u2717 ${error.message}`))
83
+ process.exit(1)
84
+ }
85
+ })
86
+
87
+ program
88
+ .command('delete <key>')
89
+ .description('Remove secret from keychain and index')
90
+ .option('--global', 'Force global scope')
91
+ .action(async (key, options) => {
92
+ try {
93
+ const manager = await createManager()
94
+ await manager.unlock()
95
+ const scope = options.global ? 'global' : undefined
96
+ await manager.delete(key, { scope })
97
+ console.log(chalk.green(` \u2713 deleted ${key}`))
98
+ } catch (error) {
99
+ console.error(chalk.red(` \u2717 ${error.message}`))
100
+ process.exit(1)
101
+ }
102
+ })
103
+
104
+ program
105
+ .command('list')
106
+ .description('List keys and metadata (never values)')
107
+ .option('--format <format>', 'Output format: table or json', 'table')
108
+ .action(async (options) => {
109
+ try {
110
+ const manager = await createManager()
111
+ const entries = manager.list()
112
+ if (options.format === 'json') {
113
+ console.log(JSON.stringify(entries, null, 2))
114
+ return
115
+ }
116
+ if (entries.length === 0) {
117
+ console.log(chalk.dim(' No secrets found'))
118
+ return
119
+ }
120
+ const globalEntries = entries.filter(e => e.scope === 'global')
121
+ const projectEntries = entries.filter(e => e.scope === 'project')
122
+ if (globalEntries.length > 0) {
123
+ console.log()
124
+ console.log(chalk.bold(' Global'))
125
+ console.log(chalk.dim(' ' + '-'.repeat(60)))
126
+ printTable(globalEntries)
127
+ }
128
+ if (projectEntries.length > 0) {
129
+ const projectName = projectEntries[0].project || manager.projectName
130
+ console.log()
131
+ console.log(chalk.bold(` Project: ${projectName}`))
132
+ console.log(chalk.dim(' ' + '-'.repeat(60)))
133
+ printTable(projectEntries)
134
+ }
135
+ console.log()
136
+ } catch (error) {
137
+ console.error(chalk.red(` \u2717 ${error.message}`))
138
+ process.exit(1)
139
+ }
140
+ })
141
+
142
+ function printTable (entries) {
143
+ const keyWidth = Math.max(12, ...entries.map(e => e.key.length)) + 2
144
+ const descWidth = Math.max(14, ...entries.map(e => (e.description || '').length)) + 2
145
+ console.log(
146
+ ' ' +
147
+ 'KEY'.padEnd(keyWidth) +
148
+ 'DESCRIPTION'.padEnd(descWidth) +
149
+ 'UPDATED'
150
+ )
151
+ for (const entry of entries) {
152
+ const updated = entry.updatedAt
153
+ ? new Date(entry.updatedAt).toLocaleDateString()
154
+ : '-'
155
+ console.log(
156
+ ' ' +
157
+ entry.key.padEnd(keyWidth) +
158
+ (entry.description || '-').padEnd(descWidth) +
159
+ updated
160
+ )
161
+ }
162
+ }
163
+
164
+ // --- Injection ---
165
+
166
+ program
167
+ .command('run')
168
+ .description('Run command with secrets injected as env vars')
169
+ .option('--verbose', 'Show injected key names')
170
+ .passThroughOptions()
171
+ .allowUnknownOption(true)
172
+ .argument('<command...>', 'Command and arguments to run')
173
+ .action(async (commandArgs, options) => {
174
+ try {
175
+ const manager = await createManager()
176
+ await manager.unlock()
177
+ const injector = new SecretInjector(manager)
178
+ const [cmd, ...args] = commandArgs
179
+ await injector.run(cmd, args, { verbose: options.verbose })
180
+ } catch (error) {
181
+ console.error(chalk.red(` \u2717 ${error.message}`))
182
+ process.exit(1)
183
+ }
184
+ })
185
+
186
+ program
187
+ .command('env')
188
+ .description('Print secrets as export statements')
189
+ .option('--format <format>', 'Output format: shell or json', 'shell')
190
+ .option('--keys <keys>', 'Filter to specific keys (comma-separated)')
191
+ .action(async (options) => {
192
+ try {
193
+ const manager = await createManager({ silent: true })
194
+ await manager.unlock()
195
+ const injector = new SecretInjector(manager)
196
+ const output = injector.formatEnv({
197
+ format: options.format,
198
+ keys: options.keys
199
+ })
200
+ console.log(output)
201
+ } catch (error) {
202
+ console.error(chalk.red(` \u2717 ${error.message}`))
203
+ process.exit(1)
204
+ }
205
+ })
206
+
207
+ // --- Project onboarding ---
208
+
209
+ program
210
+ .command('setup')
211
+ .description('Interactive: prompt for missing secrets')
212
+ .action(async () => {
213
+ try {
214
+ const manager = await createManager()
215
+ await manager.unlock()
216
+ const config = new SecretsConfig(manager)
217
+ await config.setup()
218
+ } catch (error) {
219
+ console.error(chalk.red(` \u2717 ${error.message}`))
220
+ process.exit(1)
221
+ }
222
+ })
223
+
224
+ program
225
+ .command('check')
226
+ .description('Non-interactive: verify required secrets exist')
227
+ .action(async () => {
228
+ try {
229
+ const manager = await createManager()
230
+ await manager.unlock()
231
+ const config = new SecretsConfig(manager)
232
+ const ok = await config.check()
233
+ if (!ok) process.exit(1)
234
+ } catch (error) {
235
+ console.error(chalk.red(` \u2717 ${error.message}`))
236
+ process.exit(1)
237
+ }
238
+ })
239
+
240
+ // --- Audit ---
241
+
242
+ program
243
+ .command('doctor')
244
+ .description('Scan for plaintext credentials')
245
+ .option('--fix', 'Interactively migrate plaintext credentials to keychain')
246
+ .action(async (options) => {
247
+ try {
248
+ const doctor = new SecretsDoctor(process.cwd())
249
+ const findings = await doctor.scan()
250
+ if (!options.fix) {
251
+ doctor.report(findings)
252
+ if (findings.length > 0) process.exit(1)
253
+ return
254
+ }
255
+ doctor.report(findings)
256
+ if (findings.length === 0) return
257
+ const manager = await createManager()
258
+ await manager.unlock()
259
+ await doctor.interactiveFix(findings, manager)
260
+ } catch (error) {
261
+ console.error(chalk.red(` \u2717 ${error.message}`))
262
+ process.exit(1)
263
+ }
264
+ })
265
+
266
+ // --- Config ---
267
+
268
+ program
269
+ .command('config [setting] [value]')
270
+ .description('Show or update preferences')
271
+ .action(async (setting, value) => {
272
+ try {
273
+ if (!setting) {
274
+ const prefs = BiometricAuth.loadPreferences()
275
+ console.log()
276
+ console.log(chalk.bold(' Preferences'))
277
+ console.log(chalk.dim(' ' + '-'.repeat(40)))
278
+ console.log(` biometric: ${prefs.biometric !== false ? chalk.green('on') : chalk.red('off')}`)
279
+ console.log(` passphrase: ${prefs.passphraseHash ? chalk.green('set') : chalk.dim('not set')}`)
280
+ console.log()
281
+ return
282
+ }
283
+ if (setting === 'biometric') {
284
+ if (value !== 'on' && value !== 'off') {
285
+ console.error(chalk.red(' \u2717 usage: goki-secrets config biometric on|off'))
286
+ process.exit(1)
287
+ }
288
+ const prefs = BiometricAuth.loadPreferences()
289
+ prefs.biometric = value === 'on'
290
+ BiometricAuth.savePreferences(prefs)
291
+ console.log(chalk.green(` \u2713 biometric ${value}`))
292
+ return
293
+ }
294
+ console.error(chalk.red(` \u2717 unknown setting: ${setting}`))
295
+ process.exit(1)
296
+ } catch (error) {
297
+ console.error(chalk.red(` \u2717 ${error.message}`))
298
+ process.exit(1)
299
+ }
300
+ })
301
+
302
+ program.parse()
@@ -0,0 +1,226 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { parseComposeFile } from './ComposeParser.js'
5
+ import { ensureDir, getPath } from './DevToolsDir.js'
6
+
7
+ const OVERRIDE_FILE = 'docker-compose.override.yml'
8
+
9
+ /**
10
+ * Fixed environment variable overrides per service type
11
+ * Applied automatically when a service is enabled in config.services
12
+ */
13
+ const SERVICE_ENV_OVERRIDES = {
14
+ redis: {
15
+ REDIS_HOST: 'goki-redis',
16
+ REDIS_PORT: '6379',
17
+ RED_LOCK_HOST: 'goki-redis',
18
+ RED_LOCK_PORT: '6379'
19
+ },
20
+ pubsub: {
21
+ PUBSUB_EMULATOR_HOST: 'goki-pubsub-emulator:8085',
22
+ PUB_SUB_KEY_FILE_NAME: '',
23
+ PUB_SUB_PROJECT_ID: 'tipi-development'
24
+ },
25
+ firestore: {
26
+ FIRESTORE_EMULATOR_HOST: 'goki-firestore-emulator:8080',
27
+ FIRESTORE_KEY_FILE_NAME: '',
28
+ FIRESTORE_PROJECT_ID: 'tipi-development'
29
+ },
30
+ postgres: {
31
+ POSTGRES_HOST: 'goki-postgres'
32
+ },
33
+ elasticsearch: {
34
+ ELASTIC_CLOUD_ID: '',
35
+ ELASTIC_NODE: 'http://goki-elasticsearch:9200'
36
+ },
37
+ _always: {
38
+ DEV_TOOLS_URL: 'http://goki-dev-tools-backend:9000'
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get the path to the override file
44
+ */
45
+ export function getOverridePath (projectDir) {
46
+ return getPath(projectDir, OVERRIDE_FILE)
47
+ }
48
+
49
+ /**
50
+ * Build environment overrides based on enabled services
51
+ */
52
+ function buildServiceEnvOverrides (services) {
53
+ const envOverrides = { ...SERVICE_ENV_OVERRIDES._always }
54
+ if (!services) return envOverrides
55
+ for (const [service, enabled] of Object.entries(services)) {
56
+ if (enabled && SERVICE_ENV_OVERRIDES[service]) {
57
+ Object.assign(envOverrides, SERVICE_ENV_OVERRIDES[service])
58
+ }
59
+ }
60
+ return envOverrides
61
+ }
62
+
63
+ /**
64
+ * Auto-detect Dockerfile.dev in project directory
65
+ */
66
+ function detectDockerfileDev (projectDir) {
67
+ const devDockerfile = path.join(projectDir, 'Dockerfile.dev')
68
+ if (fs.existsSync(devDockerfile)) return 'Dockerfile.dev'
69
+ return null
70
+ }
71
+
72
+ /**
73
+ * Resolve NPM_REGISTRY value for build args
74
+ * Checks: env var > secretsManager > .npmrc auth token > NPM_TOKEN env var
75
+ */
76
+ function resolveNpmRegistry (projectDir, secretsManager) {
77
+ if (process.env.NPM_REGISTRY) return process.env.NPM_REGISTRY
78
+ // Check keychain via secretsManager (if available)
79
+ if (secretsManager) {
80
+ const npmToken = secretsManager.get('NPM_TOKEN')
81
+ if (npmToken) {
82
+ return `//registry.npmjs.org/:_authToken=${npmToken}`
83
+ }
84
+ }
85
+ const npmrcPaths = [
86
+ path.join(projectDir, '.npmrc'),
87
+ path.join(os.homedir(), '.npmrc')
88
+ ]
89
+ for (const npmrcPath of npmrcPaths) {
90
+ if (!fs.existsSync(npmrcPath)) continue
91
+ const content = fs.readFileSync(npmrcPath, 'utf-8')
92
+ for (const line of content.split('\n')) {
93
+ const trimmed = line.trim()
94
+ if (!trimmed || trimmed.startsWith('#')) continue
95
+ const match = trimmed.match(/^(\/\/[^:]+\/:_authToken)=(.+)$/)
96
+ if (match) {
97
+ const token = match[2].startsWith('${') ? null : match[2]
98
+ if (token) return `${match[1]}=${token}`
99
+ }
100
+ }
101
+ }
102
+ if (process.env.NPM_TOKEN) {
103
+ return `//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}`
104
+ }
105
+ return null
106
+ }
107
+
108
+ /**
109
+ * Check if a Dockerfile has ARG NPM_REGISTRY
110
+ */
111
+ function dockerfileNeedsNpmRegistry (projectDir, dockerfile) {
112
+ const dockerfilePath = path.resolve(projectDir, dockerfile)
113
+ if (!fs.existsSync(dockerfilePath)) return false
114
+ const content = fs.readFileSync(dockerfilePath, 'utf-8')
115
+ return content.split('\n').some(l => /^ARG\s+NPM_REGISTRY\s*$/.test(l))
116
+ }
117
+
118
+ /**
119
+ * Generate the combined docker-compose override file
120
+ *
121
+ * @param {object} config - The .dev-tools/config.js config object
122
+ * @param {string} projectDir - Absolute path to the project root
123
+ * @param {object} options - Additional override data
124
+ * @param {Array} options.proxyRewrites - HTTP proxy URL rewrites [{key, proxied}]
125
+ * @param {Array} options.webhookRewrites - Webhook URL rewrites [{key, substituted}]
126
+ * @param {Array} options.secretKeys - Secret key names to include as key-only entries (values from process env)
127
+ * @returns {{ overridePath, serviceName, envOverrides, secretKeys }} or null on failure
128
+ */
129
+ export function generateComposeOverride (config, projectDir, options = {}) {
130
+ const docker = config.docker || {}
131
+ const composeFile = docker.composeFile || 'docker-compose.yaml'
132
+ const composePath = path.join(projectDir, composeFile)
133
+ // Parse base compose to get service name
134
+ const { serviceName } = parseComposeFile(composePath)
135
+ if (!serviceName) {
136
+ return null
137
+ }
138
+ // Determine dockerfile
139
+ const dockerfile = docker.dockerfile || detectDockerfileDev(projectDir)
140
+ // Build environment overrides (layered)
141
+ const envOverrides = {}
142
+ // 1. Fixed service overrides
143
+ Object.assign(envOverrides, buildServiceEnvOverrides(config.services))
144
+ // 2. HTTP proxy rewrites
145
+ if (options.proxyRewrites?.length) {
146
+ for (const r of options.proxyRewrites) {
147
+ envOverrides[r.key] = r.proxied
148
+ }
149
+ }
150
+ // 3. Webhook URL rewrites
151
+ if (options.webhookRewrites?.length) {
152
+ for (const r of options.webhookRewrites) {
153
+ envOverrides[r.key] = r.substituted
154
+ }
155
+ }
156
+ // 4. Project-specific env overrides from config (highest priority)
157
+ if (docker.environment) {
158
+ Object.assign(envOverrides, docker.environment)
159
+ }
160
+ // Build YAML sections
161
+ const sections = []
162
+ // Build section (dockerfile + build args)
163
+ const buildLines = []
164
+ if (dockerfile) {
165
+ buildLines.push(' build:')
166
+ buildLines.push(' context: .')
167
+ buildLines.push(` dockerfile: ${dockerfile}`)
168
+ // Check if we need NPM_REGISTRY build arg
169
+ const npmRegistry = resolveNpmRegistry(projectDir, options.secretsManager)
170
+ const needsNpmRegistry = dockerfile && dockerfileNeedsNpmRegistry(projectDir, dockerfile)
171
+ if (npmRegistry && needsNpmRegistry) {
172
+ buildLines.push(' args:')
173
+ buildLines.push(` - NPM_REGISTRY=${npmRegistry}`)
174
+ }
175
+ }
176
+ if (buildLines.length) sections.push(buildLines.join('\n'))
177
+ // Container name
178
+ if (docker.containerName) {
179
+ sections.push(` container_name: ${docker.containerName}`)
180
+ }
181
+ // Ports
182
+ if (docker.ports?.http) {
183
+ sections.push(' ports:\n - "${port}:3000"'.replace('${port}', docker.ports.http))
184
+ }
185
+ // Volumes
186
+ if (docker.volumes?.length) {
187
+ const volumeLines = docker.volumes.map(v => ` - ${v}`)
188
+ sections.push(' volumes:\n' + volumeLines.join('\n'))
189
+ }
190
+ // Network
191
+ sections.push(' networks:\n - goki-network')
192
+ // Environment
193
+ const envLines = Object.entries(envOverrides).map(([k, v]) => ` - ${k}=${v}`)
194
+ // Secret keys — key-only entries (values injected via process env at runtime)
195
+ const secretKeys = options.secretKeys || []
196
+ const secretLines = secretKeys
197
+ .filter(k => !(k in envOverrides))
198
+ .map(k => ` - ${k}`)
199
+ const allEnvLines = [...envLines, ...secretLines]
200
+ if (allEnvLines.length) {
201
+ sections.push(' environment:\n' + allEnvLines.join('\n'))
202
+ }
203
+ // Restart policy
204
+ sections.push(' restart: unless-stopped')
205
+ // Compose the full override file
206
+ const overrideContent = `# Auto-generated by goki-dev CLI — DO NOT EDIT
207
+ # Overrides ${composeFile} for local development with dev-tools
208
+ # Regenerated on every start
209
+
210
+ services:
211
+ ${serviceName}:
212
+ ${sections.join('\n')}
213
+
214
+ networks:
215
+ goki-network:
216
+ name: goki-network
217
+ external: true
218
+ `
219
+ // Ensure .dev-tools/ dir exists and write
220
+ ensureDir(projectDir)
221
+ const overridePath = getOverridePath(projectDir)
222
+ fs.writeFileSync(overridePath, overrideContent)
223
+ return { overridePath, serviceName, envOverrides, secretKeys }
224
+ }
225
+
226
+ export { SERVICE_ENV_OVERRIDES }
@@ -0,0 +1,73 @@
1
+ import fs from 'fs'
2
+
3
+ /**
4
+ * Parse a dotenv-style file into key-value pairs
5
+ * Handles comments, empty lines, and quoted values
6
+ */
7
+ export function parseEnvFile (filePath) {
8
+ const vars = {}
9
+ if (!fs.existsSync(filePath)) return vars
10
+ const content = fs.readFileSync(filePath, 'utf-8')
11
+ for (const line of content.split('\n')) {
12
+ const trimmed = line.trim()
13
+ if (!trimmed || trimmed.startsWith('#')) continue
14
+ const eqIndex = trimmed.indexOf('=')
15
+ if (eqIndex === -1) continue
16
+ const key = trimmed.substring(0, eqIndex).trim()
17
+ const value = trimmed.substring(eqIndex + 1).trim()
18
+ vars[key] = value
19
+ }
20
+ return vars
21
+ }
22
+
23
+ /**
24
+ * Parse a docker-compose YAML file to extract the first service name
25
+ * and its environment variables (from environment: section)
26
+ */
27
+ export function parseComposeFile (composeFilePath) {
28
+ const result = { serviceName: null, envVars: {} }
29
+ if (!fs.existsSync(composeFilePath)) return result
30
+ const content = fs.readFileSync(composeFilePath, 'utf-8')
31
+ const lines = content.split('\n')
32
+ let inEnvironment = false
33
+ let inServices = false
34
+ for (const line of lines) {
35
+ const trimmed = line.trim()
36
+ if (trimmed === 'services:') {
37
+ inServices = true
38
+ continue
39
+ }
40
+ if (inServices && !result.serviceName && trimmed.endsWith(':') && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
41
+ result.serviceName = trimmed.replace(':', '').trim()
42
+ }
43
+ if (trimmed === 'environment:') {
44
+ inEnvironment = true
45
+ continue
46
+ }
47
+ if (inEnvironment) {
48
+ if (trimmed.startsWith('- ') && trimmed.includes('=')) {
49
+ const envLine = trimmed.substring(2).trim()
50
+ const eqIndex = envLine.indexOf('=')
51
+ const key = envLine.substring(0, eqIndex)
52
+ const value = envLine.substring(eqIndex + 1)
53
+ result.envVars[key] = value
54
+ } else if (!trimmed.startsWith('-') && !trimmed.startsWith('#') && trimmed.length > 0) {
55
+ inEnvironment = false
56
+ }
57
+ }
58
+ }
59
+ return result
60
+ }
61
+
62
+ /**
63
+ * Collect all environment variables from both env_file and docker-compose environment section
64
+ * Compose environment overrides env_file values
65
+ */
66
+ export function collectAllEnvVars (envFilePath, composeFilePath) {
67
+ const allVars = {}
68
+ const envFileVars = parseEnvFile(envFilePath)
69
+ Object.assign(allVars, envFileVars)
70
+ const { serviceName, envVars: composeVars } = parseComposeFile(composeFilePath)
71
+ Object.assign(allVars, composeVars)
72
+ return { allVars, serviceName }
73
+ }