@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,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
|
+
}
|