@gokiteam/goki-dev 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,188 @@
1
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
2
+ import { Application } from '../../configs/Application.js'
3
+ import { RedisClient } from '../../singletons/RedisClient.js'
4
+ import {
5
+ PUBSUB_TOPICS,
6
+ PUBSUB_SUBSCRIPTIONS,
7
+ PUBSUB_MESSAGES,
8
+ PUBSUB_MESSAGE_HISTORY,
9
+ LOGGING_ENTRIES,
10
+ MQTT_CLIENTS,
11
+ MQTT_MESSAGES,
12
+ FIRESTORE_METADATA
13
+ } from '../../db/Tables.js'
14
+
15
+ const FIRESTORE_API = `http://localhost:${Application.ports.firestore}`
16
+ const PROJECT_ID = Application.firestore.projectId
17
+ const DATABASE = '(default)'
18
+
19
+ async function clearFirestoreEmulator () {
20
+ try {
21
+ // Use Firebase emulator's clear endpoint to delete all data
22
+ const clearUrl = `${FIRESTORE_API}/emulator/v1/projects/${PROJECT_ID}/databases/${DATABASE}/documents`
23
+ await fetch(clearUrl, { method: 'DELETE' })
24
+ } catch {
25
+ // Ignore errors when clearing Firestore (emulator might not be running)
26
+ }
27
+ }
28
+
29
+ export const Logic = {
30
+ async export (params) {
31
+ const { traceId } = params
32
+ const topics = SqliteStore.list(PUBSUB_TOPICS)
33
+ const subscriptions = SqliteStore.list(PUBSUB_SUBSCRIPTIONS)
34
+ const messages = SqliteStore.list(PUBSUB_MESSAGES)
35
+ const logEntries = SqliteStore.list(LOGGING_ENTRIES)
36
+ const mqttClients = SqliteStore.list(MQTT_CLIENTS)
37
+ const mqttMessages = SqliteStore.list(MQTT_MESSAGES)
38
+ return {
39
+ data: {
40
+ pubsub: {
41
+ topics: topics.data,
42
+ subscriptions: subscriptions.data,
43
+ messages: messages.data
44
+ },
45
+ logging: {
46
+ entries: logEntries.data
47
+ },
48
+ mqtt: {
49
+ clients: mqttClients.data,
50
+ messages: mqttMessages.data
51
+ },
52
+ exportedAt: new Date().toISOString()
53
+ },
54
+ traceId
55
+ }
56
+ },
57
+ async import (params) {
58
+ const { data, traceId } = params
59
+ let imported = {
60
+ topics: 0,
61
+ subscriptions: 0,
62
+ messages: 0,
63
+ logEntries: 0,
64
+ mqttClients: 0,
65
+ mqttMessages: 0
66
+ }
67
+ if (data.pubsub) {
68
+ if (data.pubsub.topics) {
69
+ for (const topic of data.pubsub.topics) {
70
+ SqliteStore.create(PUBSUB_TOPICS, topic)
71
+ imported.topics++
72
+ }
73
+ }
74
+ if (data.pubsub.subscriptions) {
75
+ for (const subscription of data.pubsub.subscriptions) {
76
+ SqliteStore.create(PUBSUB_SUBSCRIPTIONS, subscription)
77
+ imported.subscriptions++
78
+ }
79
+ }
80
+ if (data.pubsub.messages) {
81
+ for (const message of data.pubsub.messages) {
82
+ SqliteStore.create(PUBSUB_MESSAGES, message)
83
+ imported.messages++
84
+ }
85
+ }
86
+ }
87
+ if (data.logging && data.logging.entries) {
88
+ for (const entry of data.logging.entries) {
89
+ SqliteStore.create(LOGGING_ENTRIES, entry)
90
+ imported.logEntries++
91
+ }
92
+ }
93
+ if (data.mqtt) {
94
+ if (data.mqtt.clients) {
95
+ for (const client of data.mqtt.clients) {
96
+ SqliteStore.create(MQTT_CLIENTS, client)
97
+ imported.mqttClients++
98
+ }
99
+ }
100
+ if (data.mqtt.messages) {
101
+ for (const message of data.mqtt.messages) {
102
+ SqliteStore.create(MQTT_MESSAGES, message)
103
+ imported.mqttMessages++
104
+ }
105
+ }
106
+ }
107
+ SqliteStore.flushAll()
108
+ return {
109
+ message: 'Data imported successfully',
110
+ imported,
111
+ traceId
112
+ }
113
+ },
114
+ async clear (params) {
115
+ const { traceId } = params
116
+ SqliteStore.clear(PUBSUB_TOPICS)
117
+ SqliteStore.clear(PUBSUB_SUBSCRIPTIONS)
118
+ SqliteStore.clear(PUBSUB_MESSAGES)
119
+ SqliteStore.clear(LOGGING_ENTRIES)
120
+ SqliteStore.clear(MQTT_CLIENTS)
121
+ SqliteStore.clear(MQTT_MESSAGES)
122
+ // Clear Firestore emulator data
123
+ await clearFirestoreEmulator()
124
+ SqliteStore.clear(FIRESTORE_METADATA)
125
+ return {
126
+ message: 'All data cleared successfully',
127
+ traceId
128
+ }
129
+ },
130
+
131
+ async clearServices (params) {
132
+ const { services, keepSystemData = true, traceId } = params
133
+ const cleared = {}
134
+ try {
135
+ for (const service of services) {
136
+ if (service === 'pubsub') {
137
+ if (keepSystemData) {
138
+ const systemTopics = ['systemOneMinuteTick']
139
+ const allMessages = SqliteStore.list(PUBSUB_MESSAGE_HISTORY).data
140
+ const toDelete = allMessages.filter(msg => !systemTopics.includes(msg.topic))
141
+ for (const msg of toDelete) {
142
+ SqliteStore.delete(PUBSUB_MESSAGE_HISTORY, msg.id)
143
+ }
144
+ cleared.pubsub = toDelete.length
145
+ } else {
146
+ SqliteStore.clear(PUBSUB_MESSAGE_HISTORY)
147
+ cleared.pubsub = 'all'
148
+ }
149
+ } else if (service === 'logging') {
150
+ if (keepSystemData) {
151
+ const systemServices = ['dev-tools']
152
+ const allLogs = SqliteStore.list(LOGGING_ENTRIES).data
153
+ const toDelete = allLogs.filter(log => !systemServices.includes(log.service_name))
154
+ for (const log of toDelete) {
155
+ SqliteStore.delete(LOGGING_ENTRIES, log.id)
156
+ }
157
+ cleared.logging = toDelete.length
158
+ } else {
159
+ SqliteStore.clear(LOGGING_ENTRIES)
160
+ SqliteStore.clear('logging-logs')
161
+ cleared.logging = 'all'
162
+ }
163
+ } else if (service === 'redis') {
164
+ await RedisClient.connect()
165
+ await RedisClient.deleteAll()
166
+ cleared.redis = 'all'
167
+ } else if (service === 'postgres') {
168
+ cleared.postgres = 'not implemented'
169
+ } else if (service === 'firestore') {
170
+ cleared.firestore = 'not implemented'
171
+ } else if (service === 'mqtt') {
172
+ cleared.mqtt = 'not applicable'
173
+ }
174
+ }
175
+ return {
176
+ cleared,
177
+ traceId
178
+ }
179
+ } catch (error) {
180
+ return {
181
+ status: 'error',
182
+ message: error.message,
183
+ cleared,
184
+ traceId
185
+ }
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,16 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ const Router = new KoaRouter()
5
+ const v1 = new KoaRouter({ prefix: '/v1/data' })
6
+
7
+ v1.post('/export', Controllers.export)
8
+ v1.post('/import', Controllers.import)
9
+ v1.post('/clear', Controllers.clear)
10
+
11
+ // Testing Helpers
12
+ v1.post('/clear-services', Controllers.clearServices)
13
+
14
+ Router.use(v1.routes())
15
+
16
+ export { Router }
@@ -0,0 +1,33 @@
1
+ import { Logic } from './Logic.js'
2
+
3
+ export const Controllers = {
4
+ async list (ctx) {
5
+ const { traceId } = ctx.state
6
+ const result = await Logic.listContainers({ traceId })
7
+ ctx.reply(result)
8
+ },
9
+ async start (ctx) {
10
+ const { traceId } = ctx.state
11
+ const { containerName } = ctx.request.body
12
+ const result = await Logic.startContainer({ containerName, traceId })
13
+ ctx.reply(result)
14
+ },
15
+ async stop (ctx) {
16
+ const { traceId } = ctx.state
17
+ const { containerName } = ctx.request.body
18
+ const result = await Logic.stopContainer({ containerName, traceId })
19
+ ctx.reply(result)
20
+ },
21
+ async restart (ctx) {
22
+ const { traceId } = ctx.state
23
+ const { containerName } = ctx.request.body
24
+ const result = await Logic.restartContainer({ containerName, traceId })
25
+ ctx.reply(result)
26
+ },
27
+ async logs (ctx) {
28
+ const { traceId } = ctx.state
29
+ const { containerName, lines } = ctx.request.body
30
+ const result = await Logic.getContainerLogs({ containerName, lines, traceId })
31
+ ctx.reply(result)
32
+ }
33
+ }
@@ -0,0 +1,268 @@
1
+ import http from 'http'
2
+ import { Logger } from '../../singletons/Logger.js'
3
+ import { ContainerNames, ContainerDisplayNames, ContainerDescriptions, ContainerPorts } from '../../enums/ContainerNames.js'
4
+
5
+ const DOCKER_SOCKET = '/var/run/docker.sock'
6
+ const DOCKER_API_VERSION = 'v1.44'
7
+
8
+ // Map docker container names to our service identifiers
9
+ const CONTAINER_NAME_MAP = {
10
+ 'goki-postgres': ContainerNames.POSTGRES,
11
+ 'goki-redis': ContainerNames.REDIS,
12
+ 'goki-redis-logs': ContainerNames.REDIS_LOGS,
13
+ 'goki-pubsub-emulator': ContainerNames.PUBSUB,
14
+ 'goki-firestore-emulator': ContainerNames.FIRESTORE,
15
+ 'goki-elasticsearch': ContainerNames.ELASTICSEARCH,
16
+ 'goki-kibana': ContainerNames.KIBANA,
17
+ 'goki-dev-tools-backend': ContainerNames.DEV_TOOLS_BACKEND,
18
+ 'goki-dev-tools-frontend': ContainerNames.DEV_TOOLS_FRONTEND,
19
+ 'device-native-app': ContainerNames.DEVICE_NATIVE,
20
+ 'device-simulator-app': ContainerNames.DEVICE_SIMULATOR,
21
+ 'integration-apaleo-app': ContainerNames.INTEGRATION_APALEO,
22
+ 'core-pms-app': ContainerNames.CORE_PMS,
23
+ 'core-key-app': ContainerNames.CORE_KEY,
24
+ 'integration-mews-app': ContainerNames.INTEGRATION_MEWS,
25
+ 'integration-opera-app': ContainerNames.INTEGRATION_OPERA
26
+ }
27
+
28
+ // Reverse map for looking up docker names
29
+ const SERVICE_TO_CONTAINER = Object.fromEntries(
30
+ Object.entries(CONTAINER_NAME_MAP).map(([k, v]) => [v, k])
31
+ )
32
+
33
+ const dockerRequest = (method, path, body) => {
34
+ return new Promise((resolve, reject) => {
35
+ const options = {
36
+ socketPath: DOCKER_SOCKET,
37
+ path: `/${DOCKER_API_VERSION}${path}`,
38
+ method,
39
+ headers: { 'Content-Type': 'application/json' }
40
+ }
41
+ const req = http.request(options, (res) => {
42
+ let data = ''
43
+ res.on('data', (chunk) => { data += chunk })
44
+ res.on('end', () => {
45
+ if (res.statusCode >= 200 && res.statusCode < 300) {
46
+ resolve(data ? JSON.parse(data) : null)
47
+ } else if (res.statusCode === 304) {
48
+ resolve(null)
49
+ } else {
50
+ const error = new Error(data ? JSON.parse(data).message : `Docker API returned ${res.statusCode}`)
51
+ error.statusCode = res.statusCode
52
+ reject(error)
53
+ }
54
+ })
55
+ })
56
+ req.on('error', reject)
57
+ if (body) req.write(JSON.stringify(body))
58
+ req.end()
59
+ })
60
+ }
61
+
62
+ const dockerRequestRaw = (method, path) => {
63
+ return new Promise((resolve, reject) => {
64
+ const options = {
65
+ socketPath: DOCKER_SOCKET,
66
+ path: `/${DOCKER_API_VERSION}${path}`,
67
+ method
68
+ }
69
+ const req = http.request(options, (res) => {
70
+ const chunks = []
71
+ res.on('data', (chunk) => chunks.push(chunk))
72
+ res.on('end', () => {
73
+ const data = Buffer.concat(chunks).toString('utf8')
74
+ if (res.statusCode >= 200 && res.statusCode < 300) {
75
+ resolve(data)
76
+ } else {
77
+ reject(new Error(data || `Docker API returned ${res.statusCode}`))
78
+ }
79
+ })
80
+ })
81
+ req.on('error', reject)
82
+ req.end()
83
+ })
84
+ }
85
+
86
+ const parseContainerStatus = (state) => {
87
+ if (!state) return 'unknown'
88
+ const lower = state.toLowerCase()
89
+ if (lower === 'running') return 'running'
90
+ if (lower === 'exited') return 'stopped'
91
+ if (lower === 'created') return 'created'
92
+ if (lower === 'restarting') return 'restarting'
93
+ if (lower === 'paused') return 'paused'
94
+ return 'unknown'
95
+ }
96
+
97
+ const parseUptime = (status) => {
98
+ if (!status) return null
99
+ const match = status.match(/Up\s+(.+?)(?:\s+\(|$)/i)
100
+ if (!match) return null
101
+ return match[1].trim()
102
+ }
103
+
104
+ const parsePublicPort = (ports) => {
105
+ if (!ports || !ports.length) return null
106
+ const publicPort = ports.find(p => p.PublicPort)
107
+ return publicPort ? publicPort.PublicPort : null
108
+ }
109
+
110
+ const generateDisplayName = (containerName) => {
111
+ return containerName
112
+ .replace(/-app$/, '')
113
+ .replace(/^goki-/, '')
114
+ .split('-')
115
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
116
+ .join(' ')
117
+ }
118
+
119
+ export const Logic = {
120
+ async listContainers (params) {
121
+ const { traceId } = params
122
+ try {
123
+ const filters = JSON.stringify({ network: ['goki-network'] })
124
+ const rawContainers = await dockerRequest('GET', `/containers/json?all=true&filters=${encodeURIComponent(filters)}`)
125
+ const containers = rawContainers.map(c => {
126
+ const name = c.Names[0].replace(/^\//, '')
127
+ const serviceId = CONTAINER_NAME_MAP[name] || name
128
+ const isKnown = !!CONTAINER_NAME_MAP[name]
129
+ const isSelf = serviceId === ContainerNames.DEV_TOOLS_BACKEND
130
+ return {
131
+ name: serviceId,
132
+ containerName: name,
133
+ displayName: ContainerDisplayNames[serviceId] || generateDisplayName(name),
134
+ description: ContainerDescriptions[serviceId] || '',
135
+ status: parseContainerStatus(c.State),
136
+ statusText: c.Status,
137
+ uptime: parseUptime(c.Status),
138
+ ports: ContainerPorts[serviceId] || parsePublicPort(c.Ports),
139
+ image: c.Image,
140
+ category: isKnown ? 'infrastructure' : 'microservice',
141
+ capabilities: {
142
+ canStart: !isSelf,
143
+ canStop: !isSelf,
144
+ canRestart: !isSelf
145
+ }
146
+ }
147
+ })
148
+ containers.sort((a, b) => a.displayName.localeCompare(b.displayName))
149
+ return {
150
+ data: containers,
151
+ traceId
152
+ }
153
+ } catch (error) {
154
+ Logger.log({
155
+ level: 'error',
156
+ message: 'Failed to list Docker containers',
157
+ data: { error: error.message, traceId }
158
+ })
159
+ return {
160
+ data: [],
161
+ message: 'Failed to list containers: ' + error.message,
162
+ traceId
163
+ }
164
+ }
165
+ },
166
+ async startContainer (params) {
167
+ const { containerName, traceId } = params
168
+ const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
169
+ try {
170
+ await dockerRequest('POST', `/containers/${dockerName}/start`)
171
+ Logger.log({
172
+ level: 'info',
173
+ message: 'Container started',
174
+ data: { containerName: dockerName, traceId }
175
+ })
176
+ return {
177
+ message: `${ContainerDisplayNames[containerName] || containerName} started successfully`,
178
+ traceId
179
+ }
180
+ } catch (error) {
181
+ Logger.log({
182
+ level: 'error',
183
+ message: 'Failed to start container',
184
+ data: { containerName: dockerName, error: error.message, traceId }
185
+ })
186
+ return {
187
+ message: `Failed to start ${containerName}: ${error.message}`,
188
+ traceId
189
+ }
190
+ }
191
+ },
192
+ async stopContainer (params) {
193
+ const { containerName, traceId } = params
194
+ const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
195
+ try {
196
+ await dockerRequest('POST', `/containers/${dockerName}/stop`)
197
+ Logger.log({
198
+ level: 'info',
199
+ message: 'Container stopped',
200
+ data: { containerName: dockerName, traceId }
201
+ })
202
+ return {
203
+ message: `${ContainerDisplayNames[containerName] || containerName} stopped successfully`,
204
+ traceId
205
+ }
206
+ } catch (error) {
207
+ Logger.log({
208
+ level: 'error',
209
+ message: 'Failed to stop container',
210
+ data: { containerName: dockerName, error: error.message, traceId }
211
+ })
212
+ return {
213
+ message: `Failed to stop ${containerName}: ${error.message}`,
214
+ traceId
215
+ }
216
+ }
217
+ },
218
+ async restartContainer (params) {
219
+ const { containerName, traceId } = params
220
+ const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
221
+ try {
222
+ await dockerRequest('POST', `/containers/${dockerName}/restart`)
223
+ Logger.log({
224
+ level: 'info',
225
+ message: 'Container restarted',
226
+ data: { containerName: dockerName, traceId }
227
+ })
228
+ return {
229
+ message: `${ContainerDisplayNames[containerName] || containerName} restarted successfully`,
230
+ traceId
231
+ }
232
+ } catch (error) {
233
+ Logger.log({
234
+ level: 'error',
235
+ message: 'Failed to restart container',
236
+ data: { containerName: dockerName, error: error.message, traceId }
237
+ })
238
+ return {
239
+ message: `Failed to restart ${containerName}: ${error.message}`,
240
+ traceId
241
+ }
242
+ }
243
+ },
244
+ async getContainerLogs (params) {
245
+ const { containerName, lines = 100, traceId } = params
246
+ const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
247
+ try {
248
+ const raw = await dockerRequestRaw('GET', `/containers/${dockerName}/logs?stdout=true&stderr=true&tail=${lines}`)
249
+ // Docker log stream has 8-byte header per frame, strip them
250
+ const cleaned = raw.replace(/[\x00-\x08]/g, '').replace(/\r/g, '')
251
+ return {
252
+ data: cleaned,
253
+ traceId
254
+ }
255
+ } catch (error) {
256
+ Logger.log({
257
+ level: 'error',
258
+ message: 'Failed to get container logs',
259
+ data: { containerName: dockerName, error: error.message, traceId }
260
+ })
261
+ return {
262
+ data: '',
263
+ message: `Failed to get logs: ${error.message}`,
264
+ traceId
265
+ }
266
+ }
267
+ }
268
+ }
@@ -0,0 +1,15 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ const Router = new KoaRouter()
5
+ const v1 = new KoaRouter({ prefix: '/v1/docker' })
6
+
7
+ v1.post('/containers/list', Controllers.list)
8
+ v1.post('/containers/start', Controllers.start)
9
+ v1.post('/containers/stop', Controllers.stop)
10
+ v1.post('/containers/restart', Controllers.restart)
11
+ v1.post('/containers/logs', Controllers.logs)
12
+
13
+ Router.use(v1.routes())
14
+
15
+ export { Router }
@@ -0,0 +1,80 @@
1
+ import { Joi } from '@gokiteam/koa'
2
+
3
+ export const Schemas = {
4
+ list: {
5
+ request: {
6
+ body: {}
7
+ },
8
+ responses: {
9
+ success: {
10
+ data: Joi.array().items(
11
+ Joi.object({
12
+ name: Joi.string().required(),
13
+ containerName: Joi.string().required(),
14
+ displayName: Joi.string().required(),
15
+ description: Joi.string().allow('').required(),
16
+ status: Joi.string().valid('running', 'stopped', 'created', 'restarting', 'paused', 'unknown').required(),
17
+ statusText: Joi.string().required(),
18
+ uptime: Joi.string().allow(null).required(),
19
+ ports: Joi.number().integer().allow(null).required(),
20
+ image: Joi.string().required(),
21
+ capabilities: Joi.object({
22
+ canStart: Joi.boolean().required(),
23
+ canStop: Joi.boolean().required(),
24
+ canRestart: Joi.boolean().required()
25
+ }).required()
26
+ })
27
+ ).required()
28
+ }
29
+ }
30
+ },
31
+ start: {
32
+ request: {
33
+ body: {
34
+ containerName: Joi.string().required()
35
+ }
36
+ },
37
+ responses: {
38
+ success: {
39
+ message: Joi.string().required()
40
+ }
41
+ }
42
+ },
43
+ stop: {
44
+ request: {
45
+ body: {
46
+ containerName: Joi.string().required()
47
+ }
48
+ },
49
+ responses: {
50
+ success: {
51
+ message: Joi.string().required()
52
+ }
53
+ }
54
+ },
55
+ restart: {
56
+ request: {
57
+ body: {
58
+ containerName: Joi.string().required()
59
+ }
60
+ },
61
+ responses: {
62
+ success: {
63
+ message: Joi.string().required()
64
+ }
65
+ }
66
+ },
67
+ logs: {
68
+ request: {
69
+ body: {
70
+ containerName: Joi.string().required(),
71
+ lines: Joi.number().integer().min(1).max(1000).default(100)
72
+ }
73
+ },
74
+ responses: {
75
+ success: {
76
+ data: Joi.string().required()
77
+ }
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,15 @@
1
+ import { Logic } from './Logic.js'
2
+
3
+ export const Controllers = {
4
+ async resolve (ctx) {
5
+ const docPath = ctx.params[0] || ''
6
+ const result = Logic.resolve(docPath)
7
+ if (!result) {
8
+ ctx.status = 404
9
+ ctx.body = 'Not found'
10
+ return
11
+ }
12
+ ctx.type = result.contentType
13
+ ctx.body = result.content
14
+ }
15
+ }
@@ -0,0 +1,85 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = path.dirname(__filename)
7
+ const projectRoot = path.resolve(__dirname, '../../../')
8
+ const docsRoot = path.join(projectRoot, 'docs')
9
+
10
+ const ALLOWED_EXTENSIONS = ['.md', '.js', '.ts', '.json', '.txt']
11
+
12
+ const CONTENT_TYPES = {
13
+ '.md': 'text/markdown; charset=utf-8',
14
+ '.js': 'text/javascript; charset=utf-8',
15
+ '.ts': 'text/typescript; charset=utf-8',
16
+ '.json': 'application/json; charset=utf-8',
17
+ '.txt': 'text/plain; charset=utf-8'
18
+ }
19
+
20
+ const isAllowedFile = (filename) => {
21
+ const ext = path.extname(filename).toLowerCase()
22
+ return ALLOWED_EXTENSIONS.includes(ext)
23
+ }
24
+
25
+ const getContentType = (filename) => {
26
+ const ext = path.extname(filename).toLowerCase()
27
+ return CONTENT_TYPES[ext] || 'text/plain; charset=utf-8'
28
+ }
29
+
30
+ const resolveSafe = (requestedPath) => {
31
+ const resolved = path.resolve(docsRoot, requestedPath)
32
+ if (!resolved.startsWith(docsRoot)) return null
33
+ if (resolved.includes('node_modules')) return null
34
+ return resolved
35
+ }
36
+
37
+ const toApiPath = (fsPath) => {
38
+ const relative = path.relative(docsRoot, fsPath)
39
+ return `/v1/docs/${relative}`.replace(/\/+/g, '/')
40
+ }
41
+
42
+ const generateDirectoryListing = (absDir, dirName) => {
43
+ const entries = fs.readdirSync(absDir, { withFileTypes: true })
44
+ const dirs = []
45
+ const files = []
46
+ for (const entry of entries) {
47
+ if (entry.name.startsWith('.')) continue
48
+ if (entry.name === 'node_modules') continue
49
+ if (entry.name === 'index.md') continue
50
+ const fullPath = path.join(absDir, entry.name)
51
+ if (entry.isDirectory()) {
52
+ dirs.push(`- [${entry.name}/](${toApiPath(fullPath)}/)`)
53
+ } else if (isAllowedFile(entry.name)) {
54
+ files.push(`- [${entry.name}](${toApiPath(fullPath)})`)
55
+ }
56
+ }
57
+ dirs.sort()
58
+ files.sort()
59
+ const title = dirName || 'docs'
60
+ const lines = [`# ${title}\n`]
61
+ if (dirs.length) lines.push('## Directories\n', ...dirs, '')
62
+ if (files.length) lines.push('## Files\n', ...files, '')
63
+ return lines.join('\n')
64
+ }
65
+
66
+ export const Logic = {
67
+ resolve (docPath) {
68
+ const cleaned = (docPath || '').replace(/^\/+/, '').replace(/\/+$/, '')
69
+ const absPath = cleaned ? resolveSafe(cleaned) : docsRoot
70
+ if (!absPath || !fs.existsSync(absPath)) return null
71
+ const stat = fs.statSync(absPath)
72
+ if (stat.isDirectory()) {
73
+ const indexPath = path.join(absPath, 'index.md')
74
+ const content = fs.existsSync(indexPath)
75
+ ? fs.readFileSync(indexPath, 'utf-8')
76
+ : generateDirectoryListing(absPath, cleaned.split('/').pop())
77
+ return { contentType: 'text/markdown; charset=utf-8', content }
78
+ }
79
+ if (stat.isFile()) {
80
+ if (!isAllowedFile(absPath)) return null
81
+ return { contentType: getContentType(absPath), content: fs.readFileSync(absPath, 'utf-8') }
82
+ }
83
+ return null
84
+ }
85
+ }