@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.
- package/README.md +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- 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
|
+
}
|