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