@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,710 @@
1
+ import { v4 as uuid } from 'uuid'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { exec } from 'child_process'
5
+ import { promisify } from 'util'
6
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
7
+ import { FunctionsService } from '../../singletons/FunctionsService.js'
8
+ import { FunctionTriggerDispatcher } from '../../singletons/FunctionTriggerDispatcher.js'
9
+ import { CLOUD_FUNCTIONS, CLOUD_FUNCTION_INVOCATIONS } from '../../db/Tables.js'
10
+ import { Application } from '../../configs/Application.js'
11
+ import { FunctionTriggerTypes, FunctionTriggerTypeList } from '../../enums/FunctionTriggerTypes.js'
12
+ import { FunctionStatuses } from '../../enums/FunctionStatuses.js'
13
+
14
+ const DEFAULT_SOURCE_DIR = path.resolve(Application.storage.dataDir || './data', 'functions')
15
+ const CONTAINER_PROJECT_DIR = '/app'
16
+
17
+ function resolveHostPath (containerPath) {
18
+ const hostProjectDir = Application.hostProjectDir
19
+ if (!hostProjectDir || !containerPath) return containerPath
20
+ if (containerPath.startsWith(CONTAINER_PROJECT_DIR + '/')) {
21
+ return path.join(hostProjectDir, containerPath.slice(CONTAINER_PROJECT_DIR.length))
22
+ }
23
+ return containerPath
24
+ }
25
+
26
+ function buildFileTree (baseDir, currentDir) {
27
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true })
28
+ const result = []
29
+ for (const entry of entries) {
30
+ if (entry.name === 'node_modules' || entry.name === '.git') continue
31
+ const relativePath = path.relative(baseDir, path.join(currentDir, entry.name))
32
+ if (entry.isDirectory()) {
33
+ result.push({
34
+ name: entry.name,
35
+ path: relativePath,
36
+ type: 'directory',
37
+ children: buildFileTree(baseDir, path.join(currentDir, entry.name))
38
+ })
39
+ } else {
40
+ const ext = path.extname(entry.name).slice(1)
41
+ result.push({
42
+ name: entry.name,
43
+ path: relativePath,
44
+ type: 'file',
45
+ language: getLanguageFromExt(ext)
46
+ })
47
+ }
48
+ }
49
+ // Sort: directories first, then files, both alphabetically
50
+ result.sort((a, b) => {
51
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
52
+ return a.name.localeCompare(b.name)
53
+ })
54
+ return result
55
+ }
56
+
57
+ function getLanguageFromExt (ext) {
58
+ const map = {
59
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript',
60
+ ts: 'typescript', tsx: 'typescript',
61
+ json: 'json',
62
+ md: 'markdown',
63
+ yaml: 'yaml', yml: 'yaml',
64
+ html: 'html',
65
+ css: 'css',
66
+ sh: 'shell', bash: 'shell',
67
+ env: 'plaintext', txt: 'plaintext'
68
+ }
69
+ return map[ext] || 'plaintext'
70
+ }
71
+
72
+ /**
73
+ * Ensure functions directory has a package.json with type=commonjs
74
+ * so that .js files can use `exports` / `module.exports` syntax
75
+ * (the main project has "type": "module" which would break CJS functions)
76
+ */
77
+ function ensureFunctionsPackageJson (dir) {
78
+ const pkgPath = path.join(dir, 'package.json')
79
+ if (!fs.existsSync(pkgPath)) {
80
+ fs.writeFileSync(pkgPath, JSON.stringify({
81
+ name: 'cloud-functions',
82
+ version: '1.0.0',
83
+ type: 'commonjs',
84
+ private: true
85
+ }, null, 2))
86
+ }
87
+ }
88
+
89
+ function resolveFunction (params) {
90
+ const { id, name, traceId } = params
91
+ if (id) {
92
+ const fn = SqliteStore.get(CLOUD_FUNCTIONS, id)
93
+ if (!fn) return { success: false, message: `Function not found with id: ${id}`, traceId }
94
+ return fn
95
+ }
96
+ if (name) {
97
+ const matches = SqliteStore.find(CLOUD_FUNCTIONS, { name })
98
+ if (matches.length === 0) return { success: false, message: `Function not found: ${name}`, traceId }
99
+ return matches[0]
100
+ }
101
+ return { success: false, message: 'id or name is required', traceId }
102
+ }
103
+
104
+ function mergeLiveStatus (fn) {
105
+ const statuses = FunctionsService.getAllStatuses()
106
+ const live = statuses.find(s => s.name === fn.name)
107
+ if (live) {
108
+ return { ...fn, status: live.status, port: live.port, pid: live.pid, uptime: live.uptime }
109
+ }
110
+ return fn
111
+ }
112
+
113
+ export const Logic = {
114
+ async create (params) {
115
+ const { name, source, sourcePath, entryPoint, triggerType, triggerConfig, signatureType, runtime, environmentVariables, timeoutSeconds, description, traceId } = params
116
+ if (!name) return { success: false, message: 'name is required', traceId }
117
+ if (!source && !sourcePath) return { success: false, message: 'source or sourcePath is required', traceId }
118
+ if (!entryPoint) return { success: false, message: 'entryPoint is required', traceId }
119
+ if (!triggerType) return { success: false, message: 'triggerType is required', traceId }
120
+ if (!FunctionTriggerTypeList.includes(triggerType)) {
121
+ return { success: false, message: `Invalid triggerType. Must be one of: ${FunctionTriggerTypeList.join(', ')}`, traceId }
122
+ }
123
+ const existing = SqliteStore.find(CLOUD_FUNCTIONS, { name })
124
+ if (existing.length > 0) {
125
+ return { success: false, message: `Function with name '${name}' already exists`, traceId }
126
+ }
127
+ let resolvedSourcePath = sourcePath
128
+ let resolvedSource = source
129
+ if (!sourcePath && source) {
130
+ const funcDir = path.join(DEFAULT_SOURCE_DIR, name)
131
+ if (!fs.existsSync(funcDir)) {
132
+ fs.mkdirSync(funcDir, { recursive: true })
133
+ }
134
+ ensureFunctionsPackageJson(funcDir)
135
+ resolvedSourcePath = funcDir
136
+ const sourceFile = path.join(funcDir, 'index.js')
137
+ fs.writeFileSync(sourceFile, source)
138
+ resolvedSource = 'index.js'
139
+ }
140
+ const id = uuid()
141
+ const record = {
142
+ id,
143
+ name,
144
+ source: resolvedSource,
145
+ sourcePath: resolvedSourcePath || null,
146
+ entryPoint,
147
+ triggerType,
148
+ triggerConfig: triggerConfig || {},
149
+ signatureType: signatureType || 'cloudevent',
150
+ runtime: runtime || 'nodejs20',
151
+ environmentVariables: environmentVariables || {},
152
+ timeoutSeconds: timeoutSeconds || 60,
153
+ description: description || '',
154
+ status: FunctionStatuses.STOPPED,
155
+ enabled: 1,
156
+ invocationCount: 0,
157
+ createdAt: new Date().toISOString(),
158
+ updatedAt: new Date().toISOString()
159
+ }
160
+ SqliteStore.create(CLOUD_FUNCTIONS, record)
161
+ try {
162
+ await FunctionsService.deployFunction(record)
163
+ FunctionTriggerDispatcher.registerFunction(record)
164
+ } catch (error) {
165
+ // Function created in DB but failed to start - record error status
166
+ SqliteStore.update(CLOUD_FUNCTIONS, id, {
167
+ status: FunctionStatuses.ERROR,
168
+ errorMessage: error.message
169
+ })
170
+ }
171
+ const created = SqliteStore.get(CLOUD_FUNCTIONS, id)
172
+ return {
173
+ data: { function: mergeLiveStatus(created) },
174
+ message: 'Function created',
175
+ traceId
176
+ }
177
+ },
178
+
179
+ list (params) {
180
+ const { filter, limit, offset, traceId } = params
181
+ const options = {
182
+ orderBy: 'created_at DESC'
183
+ }
184
+ if (limit !== undefined) options.limit = limit
185
+ if (offset !== undefined) options.offset = offset
186
+ if (filter) {
187
+ const where = {}
188
+ if (filter.triggerType) where.triggerType = filter.triggerType
189
+ if (filter.status) where.status = filter.status
190
+ if (filter.enabled !== undefined) where.enabled = filter.enabled ? 1 : 0
191
+ if (Object.keys(where).length > 0) options.where = where
192
+ }
193
+ const { data, total } = SqliteStore.list(CLOUD_FUNCTIONS, options)
194
+ const functions = data.map(fn => mergeLiveStatus(fn))
195
+ return { data: { functions, total }, traceId }
196
+ },
197
+
198
+ details (params) {
199
+ const fn = resolveFunction(params)
200
+ if (fn.success === false) return fn
201
+ const fnData = mergeLiveStatus(fn)
202
+ fnData.hostSourcePath = resolveHostPath(fn.sourcePath)
203
+ return { data: { function: fnData }, traceId: params.traceId }
204
+ },
205
+
206
+ async update (params) {
207
+ const { id, name, traceId, ...updates } = params
208
+ const fn = resolveFunction({ id, name, traceId })
209
+ if (fn.success === false) return fn
210
+ const allowedFields = ['description', 'triggerConfig', 'enabled', 'environmentVariables', 'timeoutSeconds', 'entryPoint', 'signatureType']
211
+ const cleanUpdates = {}
212
+ for (const field of allowedFields) {
213
+ if (updates[field] !== undefined) cleanUpdates[field] = updates[field]
214
+ }
215
+ if (Object.keys(cleanUpdates).length === 0) {
216
+ return { success: false, message: 'No valid fields to update', traceId }
217
+ }
218
+ cleanUpdates.updatedAt = new Date().toISOString()
219
+ const triggerConfigChanged = cleanUpdates.triggerConfig !== undefined &&
220
+ JSON.stringify(cleanUpdates.triggerConfig) !== JSON.stringify(fn.triggerConfig)
221
+ const enabledChanged = cleanUpdates.enabled !== undefined && cleanUpdates.enabled !== fn.enabled
222
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, cleanUpdates)
223
+ if (triggerConfigChanged) {
224
+ FunctionTriggerDispatcher.unregisterFunction(fn)
225
+ const updatedFn = SqliteStore.get(CLOUD_FUNCTIONS, fn.id)
226
+ FunctionTriggerDispatcher.registerFunction(updatedFn)
227
+ }
228
+ if (enabledChanged) {
229
+ if (cleanUpdates.enabled) {
230
+ try {
231
+ await FunctionsService.startFunction(fn.name)
232
+ const updatedFn = SqliteStore.get(CLOUD_FUNCTIONS, fn.id)
233
+ FunctionTriggerDispatcher.registerFunction(updatedFn)
234
+ } catch (error) {
235
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, {
236
+ status: FunctionStatuses.ERROR,
237
+ errorMessage: error.message
238
+ })
239
+ }
240
+ } else {
241
+ try {
242
+ await FunctionsService.stopFunction(fn.name)
243
+ } catch {
244
+ // may not be running
245
+ }
246
+ FunctionTriggerDispatcher.unregisterFunction(fn)
247
+ }
248
+ }
249
+ const updated = SqliteStore.get(CLOUD_FUNCTIONS, fn.id)
250
+ return {
251
+ data: { function: mergeLiveStatus(updated) },
252
+ message: 'Function updated',
253
+ traceId
254
+ }
255
+ },
256
+
257
+ async delete (params) {
258
+ const fn = resolveFunction(params)
259
+ if (fn.success === false) return fn
260
+ try {
261
+ await FunctionsService.removeFunction(fn.name)
262
+ } catch {
263
+ // may not be running
264
+ }
265
+ FunctionTriggerDispatcher.unregisterFunction(fn)
266
+ // Delete invocations for this function
267
+ try {
268
+ const invocations = SqliteStore.find(CLOUD_FUNCTION_INVOCATIONS, { functionId: fn.id })
269
+ for (const inv of invocations) {
270
+ SqliteStore.delete(CLOUD_FUNCTION_INVOCATIONS, inv.id)
271
+ }
272
+ } catch {
273
+ // Ignore cleanup errors
274
+ }
275
+ SqliteStore.delete(CLOUD_FUNCTIONS, fn.id)
276
+ return { message: `Function '${fn.name}' deleted`, traceId: params.traceId }
277
+ },
278
+
279
+ async start (params) {
280
+ const fn = resolveFunction(params)
281
+ if (fn.success === false) return fn
282
+ try {
283
+ const status = await FunctionsService.startFunction(fn.name)
284
+ return {
285
+ data: { function: mergeLiveStatus(SqliteStore.get(CLOUD_FUNCTIONS, fn.id)), status },
286
+ message: `Function '${fn.name}' started`,
287
+ traceId: params.traceId
288
+ }
289
+ } catch (error) {
290
+ return { success: false, message: `Failed to start function: ${error.message}`, traceId: params.traceId }
291
+ }
292
+ },
293
+
294
+ async stop (params) {
295
+ const fn = resolveFunction(params)
296
+ if (fn.success === false) return fn
297
+ try {
298
+ await FunctionsService.stopFunction(fn.name)
299
+ return {
300
+ data: { function: mergeLiveStatus(SqliteStore.get(CLOUD_FUNCTIONS, fn.id)) },
301
+ message: `Function '${fn.name}' stopped`,
302
+ traceId: params.traceId
303
+ }
304
+ } catch (error) {
305
+ return { success: false, message: `Failed to stop function: ${error.message}`, traceId: params.traceId }
306
+ }
307
+ },
308
+
309
+ async restart (params) {
310
+ const fn = resolveFunction(params)
311
+ if (fn.success === false) return fn
312
+ try {
313
+ const status = await FunctionsService.restartFunction(fn.name)
314
+ return {
315
+ data: { function: mergeLiveStatus(SqliteStore.get(CLOUD_FUNCTIONS, fn.id)), status },
316
+ message: `Function '${fn.name}' restarted`,
317
+ traceId: params.traceId
318
+ }
319
+ } catch (error) {
320
+ return { success: false, message: `Failed to restart function: ${error.message}`, traceId: params.traceId }
321
+ }
322
+ },
323
+
324
+ readSource (params) {
325
+ const fn = resolveFunction(params)
326
+ if (fn.success === false) return fn
327
+ if (!fn.sourcePath || !fn.source) {
328
+ return { success: false, message: 'Function has no source path configured', traceId: params.traceId }
329
+ }
330
+ const filePath = path.isAbsolute(fn.source)
331
+ ? fn.source
332
+ : path.join(fn.sourcePath, fn.source)
333
+ try {
334
+ const content = fs.readFileSync(filePath, 'utf-8')
335
+ return {
336
+ data: { source: content, filePath },
337
+ traceId: params.traceId
338
+ }
339
+ } catch (error) {
340
+ return { success: false, message: `Failed to read source: ${error.message}`, traceId: params.traceId }
341
+ }
342
+ },
343
+
344
+ async updateSource (params) {
345
+ const { content, traceId } = params
346
+ const fn = resolveFunction(params)
347
+ if (fn.success === false) return fn
348
+ if (!content) return { success: false, message: 'content is required', traceId }
349
+ if (!fn.sourcePath || !fn.source) {
350
+ return { success: false, message: 'Function has no source path configured', traceId }
351
+ }
352
+ const filePath = path.isAbsolute(fn.source)
353
+ ? fn.source
354
+ : path.join(fn.sourcePath, fn.source)
355
+ try {
356
+ fs.writeFileSync(filePath, content)
357
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, { updatedAt: new Date().toISOString() })
358
+ } catch (error) {
359
+ return { success: false, message: `Failed to write source: ${error.message}`, traceId }
360
+ }
361
+ // Restart function to pick up changes
362
+ try {
363
+ await FunctionsService.restartFunction(fn.name)
364
+ } catch {
365
+ // Function may not be running
366
+ }
367
+ return {
368
+ data: { filePath },
369
+ message: 'Source updated and function restarted',
370
+ traceId
371
+ }
372
+ },
373
+
374
+ readDependencies (params) {
375
+ const fn = resolveFunction(params)
376
+ if (fn.success === false) return fn
377
+ const dir = fn.sourcePath || DEFAULT_SOURCE_DIR
378
+ const pkgPath = path.join(dir, 'package.json')
379
+ try {
380
+ if (!fs.existsSync(pkgPath)) {
381
+ return {
382
+ data: {
383
+ packageJson: JSON.stringify({
384
+ name: 'cloud-functions',
385
+ version: '1.0.0',
386
+ type: 'commonjs',
387
+ private: true,
388
+ dependencies: {}
389
+ }, null, 2),
390
+ exists: false
391
+ },
392
+ traceId: params.traceId
393
+ }
394
+ }
395
+ const content = fs.readFileSync(pkgPath, 'utf-8')
396
+ return { data: { packageJson: content, exists: true }, traceId: params.traceId }
397
+ } catch (error) {
398
+ return { success: false, message: `Failed to read package.json: ${error.message}`, traceId: params.traceId }
399
+ }
400
+ },
401
+
402
+ async updateDependencies (params) {
403
+ const { packageJson, install, traceId } = params
404
+ const fn = resolveFunction(params)
405
+ if (fn.success === false) return fn
406
+ if (!packageJson) return { success: false, message: 'packageJson is required', traceId }
407
+ const dir = fn.sourcePath || DEFAULT_SOURCE_DIR
408
+ if (!fs.existsSync(dir)) {
409
+ fs.mkdirSync(dir, { recursive: true })
410
+ }
411
+ const pkgPath = path.join(dir, 'package.json')
412
+ try {
413
+ JSON.parse(packageJson)
414
+ } catch {
415
+ return { success: false, message: 'packageJson must be valid JSON', traceId }
416
+ }
417
+ try {
418
+ fs.writeFileSync(pkgPath, packageJson)
419
+ } catch (error) {
420
+ return { success: false, message: `Failed to write package.json: ${error.message}`, traceId }
421
+ }
422
+ let installResult = null
423
+ if (install !== false) {
424
+ try {
425
+ const execAsync = promisify(exec)
426
+ const { stdout, stderr } = await execAsync('npm install --production', {
427
+ cwd: dir,
428
+ timeout: 60000
429
+ })
430
+ installResult = { success: true, stdout: stdout.trim(), stderr: stderr.trim() }
431
+ } catch (error) {
432
+ installResult = { success: false, error: error.message }
433
+ }
434
+ }
435
+ // Restart function to pick up new dependencies
436
+ try {
437
+ await FunctionsService.restartFunction(fn.name)
438
+ } catch {
439
+ // Function may not be running
440
+ }
441
+ return {
442
+ data: { filePath: pkgPath, installResult },
443
+ message: install !== false ? 'Dependencies updated and installed' : 'package.json updated',
444
+ traceId
445
+ }
446
+ },
447
+
448
+ listFiles (params) {
449
+ const fn = resolveFunction(params)
450
+ if (fn.success === false) return fn
451
+ const dir = fn.sourcePath || path.join(DEFAULT_SOURCE_DIR, fn.name)
452
+ try {
453
+ if (!fs.existsSync(dir)) {
454
+ return { data: { files: [], basePath: dir }, traceId: params.traceId }
455
+ }
456
+ const tree = buildFileTree(dir, dir)
457
+ return { data: { files: tree, basePath: dir }, traceId: params.traceId }
458
+ } catch (error) {
459
+ return { success: false, message: `Failed to list files: ${error.message}`, traceId: params.traceId }
460
+ }
461
+ },
462
+
463
+ readFile (params) {
464
+ const { filePath, traceId } = params
465
+ const fn = resolveFunction(params)
466
+ if (fn.success === false) return fn
467
+ const dir = fn.sourcePath || DEFAULT_SOURCE_DIR
468
+ // Security: prevent path traversal
469
+ const resolved = path.resolve(dir, filePath)
470
+ if (!resolved.startsWith(path.resolve(dir))) {
471
+ return { success: false, message: 'Invalid file path: path traversal not allowed', traceId }
472
+ }
473
+ try {
474
+ const content = fs.readFileSync(resolved, 'utf-8')
475
+ const ext = path.extname(filePath).slice(1)
476
+ return { data: { content, filePath, language: getLanguageFromExt(ext) }, traceId }
477
+ } catch (error) {
478
+ return { success: false, message: `Failed to read file: ${error.message}`, traceId }
479
+ }
480
+ },
481
+
482
+ async writeFile (params) {
483
+ const { filePath, content, traceId } = params
484
+ const fn = resolveFunction(params)
485
+ if (fn.success === false) return fn
486
+ const dir = fn.sourcePath || DEFAULT_SOURCE_DIR
487
+ // Security: prevent path traversal
488
+ const resolved = path.resolve(dir, filePath)
489
+ if (!resolved.startsWith(path.resolve(dir))) {
490
+ return { success: false, message: 'Invalid file path: path traversal not allowed', traceId }
491
+ }
492
+ try {
493
+ // Ensure parent directory exists
494
+ const parentDir = path.dirname(resolved)
495
+ if (!fs.existsSync(parentDir)) {
496
+ fs.mkdirSync(parentDir, { recursive: true })
497
+ }
498
+ fs.writeFileSync(resolved, content)
499
+ return { data: { filePath }, message: 'File saved', traceId }
500
+ } catch (error) {
501
+ return { success: false, message: `Failed to write file: ${error.message}`, traceId }
502
+ }
503
+ },
504
+
505
+ deleteFile (params) {
506
+ const { filePath, traceId } = params
507
+ const fn = resolveFunction(params)
508
+ if (fn.success === false) return fn
509
+ const dir = fn.sourcePath || DEFAULT_SOURCE_DIR
510
+ const resolved = path.resolve(dir, filePath)
511
+ if (!resolved.startsWith(path.resolve(dir))) {
512
+ return { success: false, message: 'Invalid file path: path traversal not allowed', traceId }
513
+ }
514
+ // Prevent deleting the main source file or package.json
515
+ const basename = path.basename(resolved)
516
+ if (basename === 'package.json' || resolved === path.resolve(dir, fn.source)) {
517
+ return { success: false, message: 'Cannot delete the main source file or package.json', traceId }
518
+ }
519
+ try {
520
+ if (!fs.existsSync(resolved)) {
521
+ return { success: false, message: `File not found: ${filePath}`, traceId }
522
+ }
523
+ fs.unlinkSync(resolved)
524
+ // Clean up empty parent directories
525
+ const parentDir = path.dirname(resolved)
526
+ if (parentDir !== path.resolve(dir) && fs.readdirSync(parentDir).length === 0) {
527
+ fs.rmdirSync(parentDir)
528
+ }
529
+ return { data: { filePath }, message: 'File deleted', traceId }
530
+ } catch (error) {
531
+ return { success: false, message: `Failed to delete file: ${error.message}`, traceId }
532
+ }
533
+ },
534
+
535
+ async invoke (params) {
536
+ const { payload, traceId } = params
537
+ const fn = resolveFunction(params)
538
+ if (fn.success === false) return fn
539
+ const cloudEvent = FunctionTriggerDispatcher.buildManualCloudEvent(payload)
540
+ try {
541
+ const result = await FunctionTriggerDispatcher.invokeFunction(fn, cloudEvent, 'manual')
542
+ return {
543
+ data: { invocation: result },
544
+ message: `Function '${fn.name}' invoked`,
545
+ traceId
546
+ }
547
+ } catch (error) {
548
+ return { success: false, message: `Invocation failed: ${error.message}`, traceId }
549
+ }
550
+ },
551
+
552
+ async callHttpFunction (params) {
553
+ const { functionName, method, headers, body, path: subPath, query, traceId } = params
554
+ const matches = SqliteStore.find(CLOUD_FUNCTIONS, { name: functionName })
555
+ if (matches.length === 0) {
556
+ return { success: false, message: `Function not found: ${functionName}`, traceId }
557
+ }
558
+ const fn = matches[0]
559
+ if (fn.triggerType !== FunctionTriggerTypes.HTTP) {
560
+ return { success: false, message: `Function '${functionName}' is not an HTTP function`, traceId }
561
+ }
562
+ const endpointUrl = FunctionsService.getEndpointUrl(fn.name)
563
+ if (!endpointUrl) {
564
+ return { success: false, message: `Function '${functionName}' is not running`, traceId }
565
+ }
566
+ const targetUrl = new URL(endpointUrl)
567
+ if (subPath) targetUrl.pathname = `/${subPath}`
568
+ if (query) {
569
+ for (const [key, value] of Object.entries(query)) {
570
+ targetUrl.searchParams.set(key, value)
571
+ }
572
+ }
573
+ const startMs = Date.now()
574
+ const invocationId = uuid()
575
+ const startedAt = new Date().toISOString()
576
+ try {
577
+ const fetchOptions = {
578
+ method,
579
+ headers: {
580
+ 'Content-Type': headers['content-type'] || 'application/json'
581
+ },
582
+ signal: AbortSignal.timeout((fn.timeoutSeconds || 60) * 1000)
583
+ }
584
+ if (method !== 'GET' && method !== 'HEAD' && body) {
585
+ fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body)
586
+ }
587
+ const response = await fetch(targetUrl.toString(), fetchOptions)
588
+ const responseTimeMs = Date.now() - startMs
589
+ const responseBody = await response.text()
590
+ const responseHeaders = {}
591
+ response.headers.forEach((value, key) => {
592
+ responseHeaders[key] = value
593
+ })
594
+ // Record invocation
595
+ try {
596
+ SqliteStore.create(CLOUD_FUNCTION_INVOCATIONS, {
597
+ id: invocationId,
598
+ functionId: fn.id,
599
+ functionName: fn.name,
600
+ triggerType: FunctionTriggerTypes.HTTP,
601
+ triggerSource: `http:${method} ${subPath || '/'}`,
602
+ responseStatus: response.status,
603
+ responseBody,
604
+ responseTimeMs,
605
+ startedAt,
606
+ completedAt: new Date().toISOString()
607
+ })
608
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, {
609
+ invocationCount: (fn.invocationCount || 0) + 1,
610
+ lastInvokedAt: new Date().toISOString(),
611
+ lastStatus: response.status
612
+ })
613
+ } catch {
614
+ // Ignore recording errors
615
+ }
616
+ FunctionsService.broadcastInvocation({
617
+ id: invocationId,
618
+ functionName: fn.name,
619
+ triggerType: FunctionTriggerTypes.HTTP,
620
+ responseStatus: response.status,
621
+ responseTimeMs,
622
+ completedAt: new Date().toISOString()
623
+ })
624
+ return {
625
+ status: response.status,
626
+ headers: responseHeaders,
627
+ body: responseBody
628
+ }
629
+ } catch (error) {
630
+ const responseTimeMs = Date.now() - startMs
631
+ try {
632
+ SqliteStore.create(CLOUD_FUNCTION_INVOCATIONS, {
633
+ id: invocationId,
634
+ functionId: fn.id,
635
+ functionName: fn.name,
636
+ triggerType: FunctionTriggerTypes.HTTP,
637
+ triggerSource: `http:${method} ${subPath || '/'}`,
638
+ error: error.message,
639
+ responseTimeMs,
640
+ startedAt,
641
+ completedAt: new Date().toISOString()
642
+ })
643
+ } catch {
644
+ // Ignore
645
+ }
646
+ return { success: false, message: `HTTP call failed: ${error.message}`, traceId }
647
+ }
648
+ },
649
+
650
+ listInvocations (params) {
651
+ const { functionId, functionName, triggerType, limit, offset, traceId } = params
652
+ const options = {
653
+ orderBy: 'started_at DESC',
654
+ limit: limit || 50,
655
+ offset: offset || 0
656
+ }
657
+ const where = {}
658
+ if (functionId) where.functionId = functionId
659
+ if (functionName) where.functionName = functionName
660
+ if (triggerType) where.triggerType = triggerType
661
+ if (Object.keys(where).length > 0) options.where = where
662
+ const { data, total } = SqliteStore.list(CLOUD_FUNCTION_INVOCATIONS, options)
663
+ return { data: { invocations: data, total }, traceId }
664
+ },
665
+
666
+ invocationDetails (params) {
667
+ const { id, traceId } = params
668
+ if (!id) return { success: false, message: 'id is required', traceId }
669
+ const invocation = SqliteStore.get(CLOUD_FUNCTION_INVOCATIONS, id)
670
+ if (!invocation) return { success: false, message: `Invocation not found: ${id}`, traceId }
671
+ return { data: { invocation }, traceId }
672
+ },
673
+
674
+ clearInvocations (params) {
675
+ const { functionId, traceId } = params
676
+ if (functionId) {
677
+ const invocations = SqliteStore.find(CLOUD_FUNCTION_INVOCATIONS, { functionId })
678
+ for (const inv of invocations) {
679
+ SqliteStore.delete(CLOUD_FUNCTION_INVOCATIONS, inv.id)
680
+ }
681
+ return { message: `Cleared ${invocations.length} invocations for function`, traceId }
682
+ }
683
+ SqliteStore.clear(CLOUD_FUNCTION_INVOCATIONS)
684
+ return { message: 'All invocations cleared', traceId }
685
+ },
686
+
687
+ stats (params) {
688
+ const { traceId } = params
689
+ const { data: allFunctions } = SqliteStore.list(CLOUD_FUNCTIONS, {})
690
+ const byTriggerType = {}
691
+ const byStatus = {}
692
+ for (const fn of allFunctions) {
693
+ byTriggerType[fn.triggerType] = (byTriggerType[fn.triggerType] || 0) + 1
694
+ const liveStatus = mergeLiveStatus(fn).status
695
+ byStatus[liveStatus] = (byStatus[liveStatus] || 0) + 1
696
+ }
697
+ const { total: totalInvocations } = SqliteStore.list(CLOUD_FUNCTION_INVOCATIONS, { limit: 0 })
698
+ const runtimeStatuses = FunctionsService.getAllStatuses()
699
+ return {
700
+ data: {
701
+ totalFunctions: allFunctions.length,
702
+ totalInvocations,
703
+ byTriggerType,
704
+ byStatus,
705
+ runningProcesses: runtimeStatuses.length
706
+ },
707
+ traceId
708
+ }
709
+ }
710
+ }