@mugwork/mug 0.1.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/explorer.js +3 -0
  4. package/dist/packages/email-template/src/email-template.d.ts +18 -0
  5. package/dist/packages/email-template/src/email-template.js +74 -0
  6. package/dist/packages/email-template/src/index.d.ts +1 -0
  7. package/dist/packages/email-template/src/index.js +1 -0
  8. package/dist/packages/surface-renderer/src/form-renderer.d.ts +117 -0
  9. package/dist/packages/surface-renderer/src/form-renderer.js +719 -0
  10. package/dist/packages/surface-renderer/src/index.d.ts +4 -0
  11. package/dist/packages/surface-renderer/src/index.js +2 -0
  12. package/dist/packages/surface-renderer/src/portal-renderer.d.ts +177 -0
  13. package/dist/packages/surface-renderer/src/portal-renderer.js +1089 -0
  14. package/dist/packages/surface-renderer/src/workspace-home.d.ts +46 -0
  15. package/dist/packages/surface-renderer/src/workspace-home.js +345 -0
  16. package/dist/runtime/agent-types.d.ts +48 -0
  17. package/dist/runtime/agent-types.js +3 -0
  18. package/dist/runtime/ai-router.d.ts +32 -0
  19. package/dist/runtime/ai-router.js +112 -0
  20. package/dist/runtime/app.d.ts +6 -0
  21. package/dist/runtime/app.js +399 -0
  22. package/dist/runtime/chunker.d.ts +6 -0
  23. package/dist/runtime/chunker.js +30 -0
  24. package/dist/runtime/context.d.ts +115 -0
  25. package/dist/runtime/context.js +440 -0
  26. package/dist/runtime/do/workspace-database.d.ts +10 -0
  27. package/dist/runtime/do/workspace-database.js +199 -0
  28. package/dist/runtime/form-types.d.ts +143 -0
  29. package/dist/runtime/form-types.js +1 -0
  30. package/dist/runtime/runtime.d.ts +9 -0
  31. package/dist/runtime/runtime.js +7 -0
  32. package/dist/runtime/source-types.d.ts +15 -0
  33. package/dist/runtime/source-types.js +1 -0
  34. package/dist/runtime/source.d.ts +70 -0
  35. package/dist/runtime/source.js +21 -0
  36. package/dist/runtime/sync-runtime.d.ts +10 -0
  37. package/dist/runtime/sync-runtime.js +185 -0
  38. package/dist/runtime/types.d.ts +21 -0
  39. package/dist/runtime/types.js +1 -0
  40. package/dist/runtime/workflow-entrypoint.d.ts +31 -0
  41. package/dist/runtime/workflow-entrypoint.js +1297 -0
  42. package/dist/runtime/workflow.d.ts +285 -0
  43. package/dist/runtime/workflow.js +1008 -0
  44. package/dist/src/cli.d.ts +2 -0
  45. package/dist/src/cli.js +44116 -0
  46. package/dist/src/commands/ai-gateway-route.d.ts +24 -0
  47. package/dist/src/commands/ai-gateway-route.js +192 -0
  48. package/dist/src/commands/auth.d.ts +1 -0
  49. package/dist/src/commands/auth.js +42 -0
  50. package/dist/src/commands/billing.d.ts +6 -0
  51. package/dist/src/commands/billing.js +76 -0
  52. package/dist/src/commands/brain.d.ts +1 -0
  53. package/dist/src/commands/brain.js +194 -0
  54. package/dist/src/commands/demo.d.ts +12 -0
  55. package/dist/src/commands/demo.js +147 -0
  56. package/dist/src/commands/deploy.d.ts +1 -0
  57. package/dist/src/commands/deploy.js +1052 -0
  58. package/dist/src/commands/dev.d.ts +14 -0
  59. package/dist/src/commands/dev.js +2818 -0
  60. package/dist/src/commands/form.d.ts +8 -0
  61. package/dist/src/commands/form.js +396 -0
  62. package/dist/src/commands/init.d.ts +1 -0
  63. package/dist/src/commands/init.js +139 -0
  64. package/dist/src/commands/issue.d.ts +7 -0
  65. package/dist/src/commands/issue.js +191 -0
  66. package/dist/src/commands/login.d.ts +9 -0
  67. package/dist/src/commands/login.js +163 -0
  68. package/dist/src/commands/logs.d.ts +8 -0
  69. package/dist/src/commands/logs.js +113 -0
  70. package/dist/src/commands/portal.d.ts +2 -0
  71. package/dist/src/commands/portal.js +111 -0
  72. package/dist/src/commands/pull.d.ts +3 -0
  73. package/dist/src/commands/pull.js +184 -0
  74. package/dist/src/commands/push.d.ts +4 -0
  75. package/dist/src/commands/push.js +183 -0
  76. package/dist/src/commands/run.d.ts +6 -0
  77. package/dist/src/commands/run.js +91 -0
  78. package/dist/src/commands/secret.d.ts +7 -0
  79. package/dist/src/commands/secret.js +105 -0
  80. package/dist/src/commands/shutdown.d.ts +1 -0
  81. package/dist/src/commands/shutdown.js +46 -0
  82. package/dist/src/commands/sql.d.ts +8 -0
  83. package/dist/src/commands/sql.js +142 -0
  84. package/dist/src/commands/status.d.ts +5 -0
  85. package/dist/src/commands/status.js +39 -0
  86. package/dist/src/commands/sync.d.ts +7 -0
  87. package/dist/src/commands/sync.js +991 -0
  88. package/dist/src/commands/usage.d.ts +6 -0
  89. package/dist/src/commands/usage.js +78 -0
  90. package/dist/src/commands/webhooks.d.ts +1 -0
  91. package/dist/src/commands/webhooks.js +102 -0
  92. package/dist/src/commands/workspace.d.ts +23 -0
  93. package/dist/src/commands/workspace.js +590 -0
  94. package/dist/src/connector-migration.d.ts +20 -0
  95. package/dist/src/connector-migration.js +43 -0
  96. package/dist/src/connector-parser.d.ts +14 -0
  97. package/dist/src/connector-parser.js +94 -0
  98. package/dist/src/connector-service/discover.d.ts +37 -0
  99. package/dist/src/connector-service/discover.js +79 -0
  100. package/dist/src/connector-service/gather.d.ts +22 -0
  101. package/dist/src/connector-service/gather.js +89 -0
  102. package/dist/src/connector-service/init.d.ts +14 -0
  103. package/dist/src/connector-service/init.js +109 -0
  104. package/dist/src/connector-service/scaffold.d.ts +17 -0
  105. package/dist/src/connector-service/scaffold.js +194 -0
  106. package/dist/src/connector-service/spec-storage.d.ts +8 -0
  107. package/dist/src/connector-service/spec-storage.js +48 -0
  108. package/dist/src/connector-service/types.d.ts +57 -0
  109. package/dist/src/connector-service/types.js +2 -0
  110. package/dist/src/connector-service/verify.d.ts +24 -0
  111. package/dist/src/connector-service/verify.js +575 -0
  112. package/dist/src/email-template.d.ts +2 -0
  113. package/dist/src/email-template.js +1 -0
  114. package/dist/src/manifest.d.ts +31 -0
  115. package/dist/src/manifest.js +25 -0
  116. package/dist/src/mug-icon.d.ts +1 -0
  117. package/dist/src/mug-icon.js +12 -0
  118. package/dist/src/slack-manifest.d.ts +119 -0
  119. package/dist/src/slack-manifest.js +163 -0
  120. package/dist/src/source-migration.d.ts +20 -0
  121. package/dist/src/source-migration.js +43 -0
  122. package/dist/src/surface-renderer.d.ts +5 -0
  123. package/dist/src/surface-renderer.js +3 -0
  124. package/dist/src/templates.d.ts +3 -0
  125. package/dist/src/templates.js +48 -0
  126. package/dist/src/version-check.d.ts +1 -0
  127. package/dist/src/version-check.js +28 -0
  128. package/dist/src/workflow-parser.d.ts +95 -0
  129. package/dist/src/workflow-parser.js +526 -0
  130. package/dist/worker/src/agent-types.d.ts +27 -0
  131. package/dist/worker/src/agent-types.js +3 -0
  132. package/dist/worker/src/source-types.d.ts +14 -0
  133. package/dist/worker/src/source-types.js +1 -0
  134. package/package.json +90 -0
  135. package/src/data/model-capabilities.json +171 -0
@@ -0,0 +1,1052 @@
1
+ import { checkCliVersion } from "../version-check.js";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import * as esbuild from "esbuild";
6
+ import { readSecrets } from "./secret.js";
7
+ import { getAccountToken } from "./login.js";
8
+ import { syncLocalFiles, syncLocalDatabases, syncFrameworkFiles } from "./sync.js";
9
+ import { migrateConfig } from "../connector-migration.js";
10
+ import { generateMugIconSvg } from "../mug-icon.js";
11
+ import { generateSlackManifest, generateManifestUrl, legacyToSlackJson } from "../slack-manifest.js";
12
+ import { parseWorkflowSteps } from "../workflow-parser.js";
13
+ import { exec } from "node:child_process";
14
+ const DEPLOY_URL = "https://api.mug.work/deploy";
15
+ const API_BASE = "https://api.mug.work";
16
+ function resolveDir(cwd, newPath, oldPath) {
17
+ return existsSync(join(cwd, newPath)) ? join(cwd, newPath) : join(cwd, oldPath);
18
+ }
19
+ function detectNotifyChannels(cwd) {
20
+ const channels = new Set();
21
+ const srcDir = join(cwd, "src");
22
+ if (!existsSync(srcDir))
23
+ return channels;
24
+ function scanDir(dir) {
25
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
26
+ if (entry.isDirectory()) {
27
+ scanDir(join(dir, entry.name));
28
+ }
29
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
30
+ const content = readFileSync(join(dir, entry.name), "utf-8");
31
+ if (content.includes("ctx.notify.sms(") || content.includes('notify.sms('))
32
+ channels.add("sms");
33
+ if (content.includes("ctx.notify.email(") || content.includes('notify.email('))
34
+ channels.add("email");
35
+ }
36
+ }
37
+ }
38
+ scanDir(srcDir);
39
+ try {
40
+ const mugJson = JSON.parse(readFileSync(join(cwd, "mug.json"), "utf-8"));
41
+ if (mugJson.sms || mugJson.notifications?.sms)
42
+ channels.add("sms");
43
+ if (mugJson.email || mugJson.notifications?.email)
44
+ channels.add("email");
45
+ }
46
+ catch { }
47
+ return channels;
48
+ }
49
+ function openBrowser(url) {
50
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
51
+ exec(`${cmd} "${url}"`);
52
+ }
53
+ async function ensureConsent(workspace, channel, token) {
54
+ const checkRes = await fetch(`${API_BASE}/api/consent/check?workspace=${workspace}&channel=${channel}`);
55
+ if (checkRes.ok) {
56
+ const { consented } = await checkRes.json();
57
+ if (consented)
58
+ return;
59
+ }
60
+ const createRes = await fetch(`${API_BASE}/api/consent`, {
61
+ method: "POST",
62
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
63
+ body: JSON.stringify({ workspace, channel }),
64
+ });
65
+ if (!createRes.ok) {
66
+ console.error(`Failed to create ${channel} consent request.`);
67
+ process.exit(1);
68
+ }
69
+ const { id, status } = await createRes.json();
70
+ if (status === "completed")
71
+ return;
72
+ const url = `https://mug.work/consent/${channel}?id=${id}`;
73
+ console.log(`\n${channel.toUpperCase()} consent required for this workspace.`);
74
+ console.log(`Opening consent form: ${url}`);
75
+ openBrowser(url);
76
+ console.log("Waiting for consent...");
77
+ while (true) {
78
+ await new Promise((r) => setTimeout(r, 2000));
79
+ const pollRes = await fetch(`${API_BASE}/api/consent/${id}`);
80
+ if (pollRes.ok) {
81
+ const { status: s } = await pollRes.json();
82
+ if (s === "completed") {
83
+ console.log(`${channel.toUpperCase()} consent recorded.\n`);
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ export async function deploy() {
90
+ checkCliVersion();
91
+ const cwd = process.cwd();
92
+ const mugJsonPath = join(cwd, "mug.json");
93
+ if (!existsSync(mugJsonPath)) {
94
+ console.error("No mug.json found. Run `mug init` first.");
95
+ process.exit(1);
96
+ }
97
+ const config = JSON.parse(readFileSync(mugJsonPath, "utf-8"));
98
+ const name = config.name;
99
+ const token = getAccountToken();
100
+ const frameworkUpdated = syncFrameworkFiles(cwd);
101
+ if (frameworkUpdated > 0) {
102
+ console.log(`Updated ${frameworkUpdated} framework file${frameworkUpdated !== 1 ? "s" : ""}.`);
103
+ }
104
+ const { mkdirSync } = await import("node:fs");
105
+ mkdirSync(join(cwd, ".mug"), { recursive: true });
106
+ console.log(`Bundling "${name}"...`);
107
+ const entrypoint = generateEntrypoint(cwd);
108
+ writeFileSync(join(cwd, ".mug", "_entrypoint.ts"), entrypoint);
109
+ // Resolve @mugwork/mug → CLI package's bundled runtime
110
+ const cliDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
111
+ const cliRuntimePath = join(cliDir, "runtime", "runtime.js");
112
+ const result = await esbuild.build({
113
+ entryPoints: [join(cwd, ".mug", "_entrypoint.ts")],
114
+ bundle: true,
115
+ format: "esm",
116
+ target: "es2022",
117
+ platform: "neutral",
118
+ outfile: join(cwd, ".mug", "bundle.js"),
119
+ external: ["cloudflare:workers"],
120
+ minify: true,
121
+ sourcemap: false,
122
+ alias: {
123
+ "@mugwork/mug": cliRuntimePath,
124
+ },
125
+ nodePaths: [join(cwd, "node_modules")],
126
+ });
127
+ if (result.errors.length > 0) {
128
+ console.error("Bundle failed:");
129
+ for (const err of result.errors)
130
+ console.error(` ${err.text}`);
131
+ process.exit(1);
132
+ }
133
+ const bundlePath = join(cwd, ".mug", "bundle.js");
134
+ const bundleContent = readFileSync(bundlePath, "utf-8");
135
+ const bundleSize = Buffer.byteLength(bundleContent);
136
+ console.log(`Bundle: ${(bundleSize / 1024).toFixed(1)} KB`);
137
+ const notifyChannels = detectNotifyChannels(cwd);
138
+ for (const channel of notifyChannels) {
139
+ await ensureConsent(name, channel, token);
140
+ }
141
+ console.log(`Deploying "${name}"...`);
142
+ const sources = migrateConfig(config);
143
+ const secrets = readSecrets(cwd);
144
+ const rawBrandingConfig = config.branding;
145
+ if (rawBrandingConfig) {
146
+ const prodBranding = {};
147
+ const subdomainForUrl = config.subdomain ?? name;
148
+ if (rawBrandingConfig.accentColor)
149
+ prodBranding.accentColor = rawBrandingConfig.accentColor;
150
+ prodBranding.ogImage = `https://${subdomainForUrl}.mug.work/_og-image.png`;
151
+ for (const key of ["logo", "logoSquare"]) {
152
+ const localPath = rawBrandingConfig[key];
153
+ if (!localPath)
154
+ continue;
155
+ if (localPath.startsWith("http://") || localPath.startsWith("https://")) {
156
+ prodBranding[key] = localPath;
157
+ }
158
+ else {
159
+ const ext = localPath.split(".").pop() ?? "png";
160
+ const serveName = `${key === "logoSquare" ? "logo-square" : "logo"}.${ext}`;
161
+ prodBranding[key] = `https://${subdomainForUrl}.mug.work/_branding/${serveName}`;
162
+ }
163
+ }
164
+ secrets.MUG_BRANDING = JSON.stringify(prodBranding);
165
+ }
166
+ const aiConfig = config.ai;
167
+ if (aiConfig?.routing) {
168
+ const validTiers = ["fast", "balanced", "powerful"];
169
+ const validProviders = ["openai", "anthropic", "workers-ai"];
170
+ for (const [tier, spec] of Object.entries(aiConfig.routing)) {
171
+ if (!validTiers.includes(tier)) {
172
+ console.error(`Invalid AI routing tier "${tier}". Must be one of: ${validTiers.join(", ")}`);
173
+ process.exit(1);
174
+ }
175
+ const provider = spec.startsWith("@cf/") ? "workers-ai" : spec.split("/")[0];
176
+ if (!spec.includes("/") && !spec.startsWith("@cf/")) {
177
+ console.error(`Invalid AI routing model "${spec}" for tier "${tier}". Use provider/model format (e.g., "openai/gpt-4.1-mini").`);
178
+ process.exit(1);
179
+ }
180
+ if (!validProviders.includes(provider)) {
181
+ console.error(`Unknown AI provider "${provider}" in tier "${tier}". Supported: ${validProviders.join(", ")}`);
182
+ process.exit(1);
183
+ }
184
+ }
185
+ secrets.MUG_AI_ROUTING = JSON.stringify(aiConfig.routing);
186
+ }
187
+ if (aiConfig?.billing) {
188
+ const validKeys = ["default", "fast", "balanced", "powerful"];
189
+ const localSecrets = readSecrets(cwd);
190
+ for (const [tier, value] of Object.entries(aiConfig.billing)) {
191
+ if (!validKeys.includes(tier)) {
192
+ console.error(`Invalid AI billing key "${tier}". Must be one of: ${validKeys.join(", ")}`);
193
+ process.exit(1);
194
+ }
195
+ if (value !== "mug-metered" && !localSecrets[value]) {
196
+ console.error(`AI billing key "${value}" for tier "${tier}" not found in secrets. Run: mug secret set ${value}=<your-api-key>`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+ secrets.MUG_AI_BILLING = JSON.stringify(aiConfig.billing);
201
+ }
202
+ const agentConfigs = await scanAgents(cwd);
203
+ const workflows = config.workflows ?? {};
204
+ if (Object.keys(workflows).length > 0) {
205
+ console.log(` ⚠ mug.json "workflows" section is deprecated — move schedule/trigger config to workflow .ts options`);
206
+ }
207
+ // Schedules built after TS source scan (below)
208
+ let schedules = [];
209
+ const subdomain = config.subdomain ?? name;
210
+ const metadata = JSON.stringify({
211
+ name,
212
+ subdomain,
213
+ compatibility_date: "2025-05-08",
214
+ compatibility_flags: ["nodejs_compat"],
215
+ sources: Object.keys(sources).length > 0 ? sources : undefined,
216
+ secrets: Object.keys(secrets).length > 0 ? secrets : undefined,
217
+ schedules: schedules.length > 0 ? schedules : undefined,
218
+ accentColor: rawBrandingConfig?.accentColor ?? null,
219
+ aiRouting: aiConfig?.routing,
220
+ plan: config.plan ?? "starter",
221
+ agents: Object.keys(agentConfigs).length > 0 ? agentConfigs : undefined,
222
+ });
223
+ const connectorSpecs = collectConnectorSpecs(cwd);
224
+ const form = new FormData();
225
+ form.append("script", new Blob([bundleContent], { type: "application/javascript+module" }), "index.js");
226
+ form.append("metadata", metadata);
227
+ if (connectorSpecs.length > 0) {
228
+ form.append("specs", JSON.stringify(connectorSpecs));
229
+ console.log(`Bundling ${connectorSpecs.length} connector spec(s) for catalog.`);
230
+ }
231
+ const res = await fetch(DEPLOY_URL, {
232
+ method: "POST",
233
+ headers: { Authorization: `Bearer ${token}` },
234
+ body: form,
235
+ });
236
+ const data = (await res.json());
237
+ if (!res.ok || data.error) {
238
+ console.error(`Deploy failed: ${data.error ?? res.statusText}`);
239
+ process.exit(1);
240
+ }
241
+ console.log(`\nDeployed.`);
242
+ console.log(` ${data.url}`);
243
+ if (Object.keys(secrets).length > 0) {
244
+ console.log(` Secrets: ${Object.keys(secrets).join(", ")}`);
245
+ }
246
+ if (data.schedules) {
247
+ const s = data.schedules;
248
+ const parts = [];
249
+ if (s.created > 0)
250
+ parts.push(`${s.created} created`);
251
+ if (s.updated > 0)
252
+ parts.push(`${s.updated} updated`);
253
+ if (s.deleted > 0)
254
+ parts.push(`${s.deleted} removed`);
255
+ if (parts.length > 0)
256
+ console.log(` Schedules: ${parts.join(", ")}`);
257
+ }
258
+ if (data.aiRoute) {
259
+ console.log(` AI route: dynamic/${data.aiRoute}`);
260
+ }
261
+ if (Object.keys(agentConfigs).length > 0) {
262
+ console.log(`\nProvisioning ${Object.keys(agentConfigs).length} agent(s)...`);
263
+ const agentRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-agents`, {
264
+ method: "POST",
265
+ headers: {
266
+ Authorization: `Bearer ${token}`,
267
+ "Content-Type": "application/json",
268
+ "X-Workspace": name,
269
+ },
270
+ body: JSON.stringify(agentConfigs),
271
+ });
272
+ if (agentRes.ok) {
273
+ const result = (await agentRes.json());
274
+ for (const agentName of result.agents) {
275
+ console.log(` ${agentName}: provisioned`);
276
+ }
277
+ }
278
+ else {
279
+ console.error(` Agent provisioning failed: ${await agentRes.text()}`);
280
+ }
281
+ }
282
+ const workflowDir = join(cwd, "src", "workflows");
283
+ if (existsSync(workflowDir)) {
284
+ const workflowFiles = readdirSync(workflowDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
285
+ if (workflowFiles.length > 0) {
286
+ console.log(`\nBundling ${workflowFiles.length} workflow(s)...`);
287
+ const workflowEntry = join(cwd, ".mug", "_workflow-entry.ts");
288
+ const imports = workflowFiles.map((f) => `import "${join(workflowDir, f)}";`).join("\n");
289
+ const entrypointPath = join(cwd, "src", "workflow-entrypoint.ts");
290
+ const entryContent = `${imports}\nexport { MugWorkflow } from "${entrypointPath}";\nexport { default } from "${entrypointPath}";\n`;
291
+ const { writeFileSync } = await import("node:fs");
292
+ writeFileSync(workflowEntry, entryContent);
293
+ const wfResult = await esbuild.build({
294
+ entryPoints: [workflowEntry],
295
+ bundle: true,
296
+ format: "esm",
297
+ target: "es2022",
298
+ platform: "neutral",
299
+ outfile: join(cwd, ".mug", "workflow-bundle.js"),
300
+ external: ["cloudflare:workers"],
301
+ minify: true,
302
+ sourcemap: false,
303
+ });
304
+ if (wfResult.errors.length > 0) {
305
+ console.error("Workflow bundle failed:");
306
+ for (const err of wfResult.errors)
307
+ console.error(` ${err.text}`);
308
+ }
309
+ else {
310
+ const wfBundle = readFileSync(join(cwd, ".mug", "workflow-bundle.js"), "utf-8");
311
+ console.log(`Workflow bundle: ${(Buffer.byteLength(wfBundle) / 1024).toFixed(1)} KB`);
312
+ const uploadRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-workflows`, {
313
+ method: "POST",
314
+ headers: {
315
+ Authorization: `Bearer ${token}`,
316
+ "Content-Type": "application/javascript",
317
+ "X-Workspace": name,
318
+ },
319
+ body: wfBundle,
320
+ });
321
+ if (uploadRes.ok) {
322
+ console.log(` Workflows deployed.`);
323
+ }
324
+ else {
325
+ const err = await uploadRes.text();
326
+ console.error(` Workflow deploy failed: ${err}`);
327
+ }
328
+ if (Object.keys(secrets).length > 0) {
329
+ const secretsRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-workflow-secrets`, {
330
+ method: "POST",
331
+ headers: {
332
+ Authorization: `Bearer ${token}`,
333
+ "Content-Type": "application/json",
334
+ "X-Workspace": name,
335
+ },
336
+ body: JSON.stringify(secrets),
337
+ });
338
+ if (secretsRes.ok) {
339
+ console.log(` Workflow secrets synced.`);
340
+ }
341
+ else {
342
+ console.error(` Workflow secrets sync failed: ${await secretsRes.text()}`);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ // Scan workflow TS source for schedule, webhook, inbound, and trigger config
349
+ const parsedWebhookConfig = {};
350
+ const parsedInboundConfig = {};
351
+ const parsedSchedules = {};
352
+ const parsedSlackTriggers = {};
353
+ const wfDir = existsSync(join(cwd, "workflows")) ? join(cwd, "workflows") : join(cwd, "src", "workflows");
354
+ if (existsSync(wfDir)) {
355
+ for (const f of readdirSync(wfDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"))) {
356
+ try {
357
+ const source = readFileSync(join(wfDir, f), "utf-8");
358
+ const wfName = f.replace(/\.(ts|js)$/, "");
359
+ const parsed = parseWorkflowSteps(source, wfName, f);
360
+ if (parsed.schedule) {
361
+ parsedSchedules[parsed.name] = parsed.schedule;
362
+ }
363
+ if (parsed.webhook) {
364
+ if (typeof parsed.webhook === "object") {
365
+ parsedWebhookConfig[parsed.name] = { mode: parsed.webhook.auth, secret: parsed.webhook.secret };
366
+ }
367
+ else {
368
+ parsedWebhookConfig[parsed.name] = { mode: "none" };
369
+ }
370
+ }
371
+ if (parsed.inbound) {
372
+ parsedInboundConfig[parsed.inbound] = parsed.name;
373
+ }
374
+ if (parsed.trigger?.type?.startsWith("slack_")) {
375
+ parsedSlackTriggers[parsed.name] = parsed.trigger;
376
+ }
377
+ }
378
+ catch { }
379
+ }
380
+ }
381
+ // Build schedules: TS source wins over mug.json
382
+ schedules = [];
383
+ for (const [wfName, cronExpr] of Object.entries(parsedSchedules)) {
384
+ schedules.push({ itemType: "workflow", itemName: wfName, cronExpr });
385
+ }
386
+ for (const [wfName, wf] of Object.entries(workflows)) {
387
+ if (wf.schedule) {
388
+ if (parsedSchedules[wfName]) {
389
+ console.log(` ⚠ ${wfName}: schedule in both mug.json and TS source — using TS source (mug.json is deprecated)`);
390
+ }
391
+ else {
392
+ schedules.push({ itemType: "workflow", itemName: wfName, cronExpr: wf.schedule });
393
+ console.log(` ⚠ ${wfName}: schedule in mug.json is deprecated — move to workflow options: { schedule: "${wf.schedule}" }`);
394
+ }
395
+ }
396
+ }
397
+ // Source sync schedules: TS source wins over mug.json
398
+ const parsedConnectorSchedules = {};
399
+ const connDir = resolveDir(cwd, "connectors", "src/sources");
400
+ if (existsSync(connDir)) {
401
+ for (const f of readdirSync(connDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."))) {
402
+ try {
403
+ const connSource = readFileSync(join(connDir, f), "utf-8");
404
+ const meta = parseConnectorMeta(connSource);
405
+ if (meta?.syncSchedule) {
406
+ parsedConnectorSchedules[meta.name] = meta.syncSchedule;
407
+ schedules.push({ itemType: "source", itemName: meta.name, cronExpr: meta.syncSchedule });
408
+ }
409
+ }
410
+ catch { }
411
+ }
412
+ }
413
+ for (const [srcName, src] of Object.entries(sources)) {
414
+ for (const [syncName, sync] of Object.entries(src.syncs)) {
415
+ if (sync.schedule) {
416
+ if (parsedConnectorSchedules[syncName]) {
417
+ console.log(` ⚠ ${syncName}: sync schedule in both mug.json and TS source — using TS source (mug.json is deprecated)`);
418
+ }
419
+ else {
420
+ schedules.push({ itemType: "source", itemName: syncName, cronExpr: sync.schedule });
421
+ console.log(` ⚠ ${syncName}: sync schedule in mug.json is deprecated — move to source def: { syncSchedule: "${sync.schedule}" }`);
422
+ }
423
+ }
424
+ }
425
+ }
426
+ // Merge webhook config: TS source wins over mug.json
427
+ const webhookConfig = { ...parsedWebhookConfig };
428
+ const webhookWorkflows = Object.keys(webhookConfig);
429
+ const mjWebhooks = Object.entries(workflows).filter(([, wf]) => wf.webhook);
430
+ for (const [wfName, wf] of mjWebhooks) {
431
+ if (!webhookConfig[wfName]) {
432
+ const auth = typeof wf.webhook === "object" ? wf.webhook : { auth: "none" };
433
+ webhookConfig[wfName] = { mode: auth.auth, secret: auth.secret };
434
+ webhookWorkflows.push(wfName);
435
+ console.log(` ⚠ ${wfName}: webhook config in mug.json is deprecated — move to workflow options: { webhook: { auth: "${auth.auth}" } }`);
436
+ }
437
+ else if (wf.webhook) {
438
+ console.log(` ⚠ ${wfName}: webhook config in both mug.json and TS source — using TS source (mug.json is deprecated)`);
439
+ }
440
+ }
441
+ if (webhookWorkflows.length > 0) {
442
+ const webhookRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-webhook-config`, {
443
+ method: "POST",
444
+ headers: {
445
+ Authorization: `Bearer ${token}`,
446
+ "Content-Type": "application/json",
447
+ "X-Workspace": name,
448
+ },
449
+ body: JSON.stringify(webhookConfig),
450
+ });
451
+ if (webhookRes.ok) {
452
+ console.log(`\nWebhooks:`);
453
+ for (const wfName of webhookWorkflows) {
454
+ console.log(` ${wfName}: https://api.mug.work/hook/${name}/${wfName}`);
455
+ }
456
+ }
457
+ else {
458
+ console.error(` Webhook config deploy failed: ${await webhookRes.text()}`);
459
+ }
460
+ }
461
+ // Inbound message routing config — TS source wins over mug.json
462
+ const inbound = { ...parsedInboundConfig };
463
+ const mjInbound = config.inbound;
464
+ if (mjInbound) {
465
+ for (const [channel, wfName] of Object.entries(mjInbound)) {
466
+ if (!inbound[channel]) {
467
+ inbound[channel] = wfName;
468
+ console.log(` ⚠ ${channel}: inbound config in mug.json is deprecated — add \`inbound: "${channel}"\` to workflow options`);
469
+ }
470
+ }
471
+ }
472
+ if (Object.keys(inbound).length > 0) {
473
+ const inboundRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-inbound-config`, {
474
+ method: "POST",
475
+ headers: {
476
+ Authorization: `Bearer ${token}`,
477
+ "Content-Type": "application/json",
478
+ "X-Workspace": name,
479
+ },
480
+ body: JSON.stringify(inbound),
481
+ });
482
+ if (inboundRes.ok) {
483
+ console.log(`\nInbound webhooks:`);
484
+ if (inbound.sms)
485
+ console.log(` SMS: https://api.mug.work/inbound/sms/${name}`);
486
+ if (inbound.email)
487
+ console.log(` Email: https://api.mug.work/inbound/email/${name}`);
488
+ if (inbound.slack)
489
+ console.log(` Slack: https://api.mug.work/inbound/slack/${name}`);
490
+ }
491
+ else {
492
+ console.error(` Inbound config deploy failed: ${await inboundRes.text()}`);
493
+ }
494
+ }
495
+ // Slack app lifecycle — read from slack.json, fall back to mug.json
496
+ let slackConfig;
497
+ const slackJsonPath = join(cwd, "slack.json");
498
+ if (existsSync(slackJsonPath)) {
499
+ slackConfig = JSON.parse(readFileSync(slackJsonPath, "utf-8"));
500
+ }
501
+ else if (config.slack) {
502
+ slackConfig = legacyToSlackJson(config.slack);
503
+ console.log(` ⚠ Slack config in mug.json is deprecated — migrate to slack.json`);
504
+ }
505
+ if (slackConfig?.enabled) {
506
+ console.log(`\nSlack app:`);
507
+ // Merge Slack triggers: TS source + mug.json (TS wins)
508
+ const slackTriggers = { ...parsedSlackTriggers };
509
+ for (const [wfName, wf] of Object.entries(workflows)) {
510
+ const trigger = wf.trigger;
511
+ if (trigger?.type?.startsWith("slack_") && !slackTriggers[wfName]) {
512
+ slackTriggers[wfName] = trigger;
513
+ }
514
+ }
515
+ if (Object.keys(slackTriggers).length > 0) {
516
+ console.log(` Triggers: ${Object.entries(slackTriggers).map(([wf, t]) => `${wf} (${t.type})`).join(", ")}`);
517
+ }
518
+ const manifest = generateSlackManifest(name, slackConfig, inbound, slackTriggers);
519
+ // Scope change detection
520
+ const lastManifestPath = join(cwd, ".mug", "last-slack-manifest.json");
521
+ if (existsSync(lastManifestPath)) {
522
+ try {
523
+ const lastManifest = JSON.parse(readFileSync(lastManifestPath, "utf-8"));
524
+ const lastScopes = new Set(lastManifest?.oauth_config?.scopes?.bot ?? []);
525
+ const newScopes = manifest.oauth_config.scopes.bot;
526
+ const addedScopes = newScopes.filter((s) => !lastScopes.has(s));
527
+ if (addedScopes.length > 0) {
528
+ console.log(` ⚠ New OAuth scopes: ${addedScopes.join(", ")} — workspace admin may need to re-authorize`);
529
+ }
530
+ }
531
+ catch { }
532
+ }
533
+ writeFileSync(lastManifestPath, JSON.stringify(manifest, null, 2));
534
+ const configToken = secrets.SLACK_CONFIG_TOKEN;
535
+ const refreshToken = secrets.SLACK_CONFIG_REFRESH_TOKEN;
536
+ const slackRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-slack-app`, {
537
+ method: "POST",
538
+ headers: {
539
+ Authorization: `Bearer ${token}`,
540
+ "Content-Type": "application/json",
541
+ "X-Workspace": name,
542
+ },
543
+ body: JSON.stringify({
544
+ manifest,
545
+ config_token: configToken,
546
+ refresh_token: refreshToken,
547
+ triggers: Object.keys(slackTriggers).length > 0 ? slackTriggers : undefined,
548
+ homeTab: slackConfig.homeTab ?? undefined,
549
+ shortcuts: slackConfig.shortcuts ?? undefined,
550
+ }),
551
+ });
552
+ if (slackRes.ok) {
553
+ const result = (await slackRes.json());
554
+ if (result.status === "created") {
555
+ console.log(` App created: ${result.app_id}`);
556
+ console.log(` Install URL: ${result.install_url}`);
557
+ }
558
+ else if (result.status === "updated") {
559
+ console.log(` App updated: ${result.app_id}`);
560
+ console.log(` Install URL: https://api.mug.work/slack/install/${name}`);
561
+ }
562
+ else if (result.status === "manifest_saved") {
563
+ console.log(` Manifest saved (no config token for auto-deploy).`);
564
+ console.log(` Create app manually: ${generateManifestUrl(manifest)}`);
565
+ console.log(` After creating, run: mug secrets set SLACK_APP_ID=<app_id>`);
566
+ console.log(` mug secrets set SLACK_CLIENT_ID=<client_id>`);
567
+ console.log(` mug secrets set SLACK_CLIENT_SECRET=<client_secret>`);
568
+ console.log(` mug secrets set SLACK_SIGNING_SECRET=<signing_secret>`);
569
+ }
570
+ }
571
+ else {
572
+ const err = await slackRes.text().catch(() => slackRes.statusText);
573
+ console.error(` Slack deploy failed: ${err}`);
574
+ }
575
+ if (secrets.SLACK_APP_ID && secrets.SLACK_CLIENT_ID && secrets.SLACK_CLIENT_SECRET && !configToken) {
576
+ const credRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-slack-credentials`, {
577
+ method: "POST",
578
+ headers: {
579
+ Authorization: `Bearer ${token}`,
580
+ "Content-Type": "application/json",
581
+ "X-Workspace": name,
582
+ },
583
+ body: JSON.stringify({
584
+ app_id: secrets.SLACK_APP_ID,
585
+ client_id: secrets.SLACK_CLIENT_ID,
586
+ client_secret: secrets.SLACK_CLIENT_SECRET,
587
+ signing_secret: secrets.SLACK_SIGNING_SECRET ?? "",
588
+ }),
589
+ });
590
+ if (credRes.ok) {
591
+ const result = (await credRes.json());
592
+ console.log(` Credentials synced.`);
593
+ if (result.install_url) {
594
+ console.log(` Install URL: ${result.install_url}`);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ await syncLocalFiles(cwd, config);
600
+ await syncLocalDatabases(cwd, config);
601
+ if (rawBrandingConfig) {
602
+ const hasLocalFiles = ["logo", "logoSquare", "ogImage"].some((k) => {
603
+ const v = rawBrandingConfig[k];
604
+ return v && !String(v).startsWith("http://") && !String(v).startsWith("https://");
605
+ });
606
+ if (hasLocalFiles) {
607
+ console.log(`\nUploading branding...`);
608
+ for (const key of ["logo", "logoSquare"]) {
609
+ const localPath = rawBrandingConfig[key];
610
+ if (!localPath || localPath.startsWith("http://") || localPath.startsWith("https://"))
611
+ continue;
612
+ const fullPath = join(cwd, localPath);
613
+ if (!existsSync(fullPath)) {
614
+ console.error(` ${key}: file not found: ${localPath}`);
615
+ continue;
616
+ }
617
+ const fileContent = readFileSync(fullPath);
618
+ const ext = localPath.split(".").pop() ?? "png";
619
+ const r2Key = `workspaces/${name}/branding/${key === "logoSquare" ? "logo-square" : "logo"}.${ext}`;
620
+ const uploadRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-branding`, {
621
+ method: "POST",
622
+ headers: {
623
+ Authorization: `Bearer ${token}`,
624
+ "X-Workspace": name,
625
+ "X-R2-Key": r2Key,
626
+ "Content-Type": "application/octet-stream",
627
+ },
628
+ body: fileContent,
629
+ });
630
+ if (uploadRes.ok) {
631
+ console.log(` ${key}: uploaded`);
632
+ }
633
+ else {
634
+ console.error(` ${key}: upload failed`);
635
+ }
636
+ }
637
+ const ogImagePath = rawBrandingConfig.ogImage;
638
+ if (ogImagePath && !ogImagePath.startsWith("http://") && !ogImagePath.startsWith("https://")) {
639
+ const fullPath = join(cwd, ogImagePath);
640
+ if (!existsSync(fullPath)) {
641
+ console.error(` ogImage: file not found: ${ogImagePath}`);
642
+ }
643
+ else {
644
+ const fileContent = readFileSync(fullPath);
645
+ const r2Key = `workspaces/${name}/branding/og-image.png`;
646
+ const uploadRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-branding`, {
647
+ method: "POST",
648
+ headers: {
649
+ Authorization: `Bearer ${token}`,
650
+ "X-Workspace": name,
651
+ "X-R2-Key": r2Key,
652
+ "Content-Type": "application/octet-stream",
653
+ },
654
+ body: fileContent,
655
+ });
656
+ if (uploadRes.ok) {
657
+ console.log(` ogImage: uploaded`);
658
+ }
659
+ else {
660
+ console.error(` ogImage: upload failed`);
661
+ }
662
+ }
663
+ }
664
+ }
665
+ }
666
+ await generatePwaIcons(cwd, name, rawBrandingConfig, token);
667
+ const resolvedBranding = secrets.MUG_BRANDING ? JSON.parse(secrets.MUG_BRANDING) : undefined;
668
+ const surfDir = resolveDir(cwd, "surfaces", "src/surfaces");
669
+ const homeScreenPath = join(surfDir, "_home.json");
670
+ if (existsSync(homeScreenPath)) {
671
+ console.log(`\nDeploying home screen config...`);
672
+ const homeScreenConfig = JSON.parse(readFileSync(homeScreenPath, "utf-8"));
673
+ const surfaceFiles = existsSync(surfDir)
674
+ ? readdirSync(surfDir).filter((f) => f.endsWith(".json") && f !== "_home.json").map((f) => f.replace(/\.json$/, ""))
675
+ : [];
676
+ const invalidSurfaces = [];
677
+ for (const group of (homeScreenConfig.groups ?? [])) {
678
+ for (const btn of group.buttons ?? []) {
679
+ if (!surfaceFiles.includes(btn.surface))
680
+ invalidSurfaces.push(btn.surface);
681
+ }
682
+ for (const card of group.cards ?? []) {
683
+ if (!surfaceFiles.includes(card.surface))
684
+ invalidSurfaces.push(card.surface);
685
+ }
686
+ }
687
+ if (invalidSurfaces.length > 0) {
688
+ console.error(` Warning: _home.json references unknown surfaces: ${invalidSurfaces.join(", ")}`);
689
+ }
690
+ const r2Key = `workspaces/${name}/home-screen.json`;
691
+ const hsRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-branding`, {
692
+ method: "POST",
693
+ headers: {
694
+ Authorization: `Bearer ${token}`,
695
+ "X-Workspace": name,
696
+ "X-R2-Key": r2Key,
697
+ "Content-Type": "application/octet-stream",
698
+ },
699
+ body: JSON.stringify(homeScreenConfig),
700
+ });
701
+ if (hsRes.ok) {
702
+ console.log(` Home screen config deployed.`);
703
+ }
704
+ else {
705
+ console.error(` Home screen deploy failed: ${await hsRes.text()}`);
706
+ }
707
+ }
708
+ if (existsSync(surfDir)) {
709
+ const surfaceFiles = readdirSync(surfDir).filter((f) => f.endsWith(".json") && f !== "_home.json");
710
+ if (surfaceFiles.length > 0) {
711
+ console.log(`\nDeploying ${surfaceFiles.length} surface(s)...`);
712
+ for (const file of surfaceFiles) {
713
+ const surfaceConfig = JSON.parse(readFileSync(join(surfDir, file), "utf-8"));
714
+ const surfaceId = file.replace(/\.json$/, "");
715
+ surfaceConfig.workspace = name;
716
+ surfaceConfig.surfaceId = surfaceId;
717
+ surfaceConfig.timezone = surfaceConfig.timezone ?? config.settings?.timezone;
718
+ const surfaceBranding = surfaceConfig.branding;
719
+ surfaceConfig.branding = surfaceBranding || resolvedBranding ? { ...resolvedBranding, ...surfaceBranding } : undefined;
720
+ const configErrors = [];
721
+ const validId = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
722
+ const checkId = (val, field) => {
723
+ if (!validId.test(val) && !/^"[^"]*"$/.test(val))
724
+ configErrors.push(`${field}: invalid SQL identifier "${val}"`);
725
+ };
726
+ if (surfaceConfig.access?.table)
727
+ checkId(surfaceConfig.access.table, "access.table");
728
+ if (surfaceConfig.access?.matchColumn)
729
+ checkId(surfaceConfig.access.matchColumn, "access.matchColumn");
730
+ if (surfaceConfig.editMode?.table)
731
+ checkId(surfaceConfig.editMode.table, "editMode.table");
732
+ if (surfaceConfig.editMode?.matchColumn)
733
+ checkId(surfaceConfig.editMode.matchColumn, "editMode.matchColumn");
734
+ if (surfaceConfig.type === "portal") {
735
+ const allSections = [...(surfaceConfig.sections ?? []), ...(surfaceConfig.tabs ?? []).flatMap((t) => t.sections ?? [])];
736
+ for (const section of allSections) {
737
+ if (section.primaryKey)
738
+ checkId(section.primaryKey, "section.primaryKey");
739
+ if (section.query) {
740
+ const q = section.query.trim().toUpperCase();
741
+ if (!q.startsWith("SELECT") && !q.startsWith("WITH"))
742
+ configErrors.push("section query must start with SELECT or WITH");
743
+ if (section.query.includes(";"))
744
+ configErrors.push("section query must not contain multiple statements");
745
+ }
746
+ for (const col of section.columns ?? []) {
747
+ if (col.key)
748
+ checkId(col.key, "column.key");
749
+ }
750
+ }
751
+ }
752
+ if (surfaceConfig.embedAllowOrigins) {
753
+ for (const origin of surfaceConfig.embedAllowOrigins) {
754
+ try {
755
+ const u = new URL(origin);
756
+ if (u.pathname !== "/" || u.search || u.hash)
757
+ configErrors.push(`embedAllowOrigins: "${origin}" must be origin only (no path/query)`);
758
+ }
759
+ catch {
760
+ configErrors.push(`embedAllowOrigins: "${origin}" is not a valid URL`);
761
+ }
762
+ }
763
+ }
764
+ if (configErrors.length > 0) {
765
+ console.error(` ${surfaceId}: validation failed`);
766
+ for (const err of configErrors)
767
+ console.error(` - ${err}`);
768
+ continue;
769
+ }
770
+ if (surfaceBranding) {
771
+ for (const key of ["logo", "logoSquare"]) {
772
+ const localPath = surfaceBranding[key];
773
+ if (!localPath || localPath.startsWith("http://") || localPath.startsWith("https://") || localPath.startsWith("/"))
774
+ continue;
775
+ const fullPath = join(cwd, localPath);
776
+ if (!existsSync(fullPath))
777
+ continue;
778
+ const fileContent = readFileSync(fullPath);
779
+ const ext = localPath.split(".").pop() ?? "png";
780
+ const r2Key = `workspaces/${name}/branding/${localPath.replace(/\//g, "-")}`;
781
+ const uploadRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-branding`, {
782
+ method: "POST",
783
+ headers: { Authorization: `Bearer ${token}`, "X-Workspace": name, "X-R2-Key": r2Key, "Content-Type": "application/octet-stream" },
784
+ body: fileContent,
785
+ });
786
+ if (uploadRes.ok) {
787
+ surfaceConfig.branding[key] = `/_branding/${localPath.replace(/\//g, "-")}`;
788
+ }
789
+ }
790
+ }
791
+ const surfaceRes = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-surface`, {
792
+ method: "POST",
793
+ headers: {
794
+ "Content-Type": "application/json",
795
+ Authorization: `Bearer ${token}`,
796
+ },
797
+ body: JSON.stringify(surfaceConfig),
798
+ });
799
+ if (surfaceRes.ok) {
800
+ console.log(` ${surfaceId}: https://${subdomain}.mug.work/${surfaceId}`);
801
+ }
802
+ else {
803
+ console.error(` ${surfaceId}: deploy failed — ${await surfaceRes.text()}`);
804
+ }
805
+ }
806
+ }
807
+ }
808
+ }
809
+ function collectConnectorSpecs(cwd) {
810
+ const specsDir = join(resolveDir(cwd, "connectors", "src/connectors"), ".specs");
811
+ if (!existsSync(specsDir))
812
+ return [];
813
+ const specs = [];
814
+ try {
815
+ for (const slug of readdirSync(specsDir)) {
816
+ const slugDir = join(specsDir, slug);
817
+ const metaPath = join(slugDir, "meta.yaml");
818
+ const specPath = join(slugDir, "openapi.yaml");
819
+ if (!existsSync(metaPath) || !existsSync(specPath))
820
+ continue;
821
+ try {
822
+ const metaRaw = readFileSync(metaPath, "utf-8");
823
+ const specRaw = readFileSync(specPath, "utf-8");
824
+ specs.push({ slug, meta: { _raw: metaRaw }, spec: { _raw: specRaw } });
825
+ }
826
+ catch { }
827
+ }
828
+ }
829
+ catch { }
830
+ return specs;
831
+ }
832
+ async function generatePwaIcons(cwd, name, branding, token) {
833
+ let sharp;
834
+ try {
835
+ sharp = (await import("sharp")).default;
836
+ }
837
+ catch {
838
+ return;
839
+ }
840
+ let sourceBuffer;
841
+ const logoSquare = branding?.logoSquare;
842
+ if (logoSquare && !logoSquare.startsWith("http://") && !logoSquare.startsWith("https://")) {
843
+ const fullPath = join(cwd, logoSquare);
844
+ if (existsSync(fullPath)) {
845
+ sourceBuffer = readFileSync(fullPath);
846
+ }
847
+ else {
848
+ sourceBuffer = Buffer.from(generateMugIconSvg(branding?.accentColor));
849
+ }
850
+ }
851
+ else {
852
+ sourceBuffer = Buffer.from(generateMugIconSvg(branding?.accentColor));
853
+ }
854
+ console.log(`\nGenerating PWA icons...`);
855
+ for (const size of [192, 512]) {
856
+ const png = await sharp(sourceBuffer)
857
+ .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
858
+ .png()
859
+ .toBuffer();
860
+ const r2Key = `workspaces/${name}/branding/icon-${size}.png`;
861
+ const res = await fetch(`${DEPLOY_URL.replace("/deploy", "")}/deploy-branding`, {
862
+ method: "POST",
863
+ headers: {
864
+ Authorization: `Bearer ${token}`,
865
+ "X-Workspace": name,
866
+ "X-R2-Key": r2Key,
867
+ "Content-Type": "application/octet-stream",
868
+ },
869
+ body: new Uint8Array(png),
870
+ });
871
+ if (!res.ok) {
872
+ console.error(` icon-${size}.png: upload failed`);
873
+ return;
874
+ }
875
+ }
876
+ console.log(` PWA icons: uploaded (192px, 512px)`);
877
+ }
878
+ const VALID_MODELS = [
879
+ "claude-sonnet", "claude-haiku", "claude-opus",
880
+ "gpt-4o", "gpt-4o-mini", "gpt-4.1-nano", "gpt-4.1-mini", "gpt-4.1",
881
+ ];
882
+ const VALID_TOOLS = ["query", "search", "ask", "notify", "http", "workspace", "ai"];
883
+ async function scanAgents(cwd) {
884
+ const agentsDir = resolveDir(cwd, "agents", "src/agents");
885
+ if (!existsSync(agentsDir))
886
+ return {};
887
+ const entries = readdirSync(agentsDir);
888
+ const configs = {};
889
+ const agentFolders = entries.filter((f) => {
890
+ if (f === "shared-skills")
891
+ return false;
892
+ const p = join(agentsDir, f);
893
+ return statSync(p).isDirectory() && existsSync(join(p, "agent.json"));
894
+ });
895
+ if (agentFolders.length === 0) {
896
+ const legacyFiles = entries.filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
897
+ if (legacyFiles.length > 0) {
898
+ console.warn(` Warning: found legacy agent files (${legacyFiles.join(", ")}). Migrate to folder structure: src/agents/<name>/agent.json + soul.md`);
899
+ }
900
+ return {};
901
+ }
902
+ const sharedSkillsDir = join(agentsDir, "shared-skills");
903
+ const sharedSkills = [];
904
+ if (existsSync(sharedSkillsDir)) {
905
+ for (const skillDir of readdirSync(sharedSkillsDir)) {
906
+ const skillFile = join(sharedSkillsDir, skillDir, "SKILL.md");
907
+ if (existsSync(skillFile)) {
908
+ sharedSkills.push({ name: skillDir, content: readFileSync(skillFile, "utf-8") });
909
+ }
910
+ }
911
+ }
912
+ for (const folder of agentFolders) {
913
+ const agentDir = join(agentsDir, folder);
914
+ const configPath = join(agentDir, "agent.json");
915
+ try {
916
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
917
+ if (!config.name) {
918
+ config.name = folder;
919
+ }
920
+ if (!config.model) {
921
+ console.error(` ${folder}: missing model in agent.json`);
922
+ process.exit(1);
923
+ }
924
+ if (typeof config.model === "string") {
925
+ if (!VALID_MODELS.includes(config.model) && !config.model.includes("/")) {
926
+ console.warn(` ${folder}: model "${config.model}" not in standard list — will use as-is via AI Gateway`);
927
+ }
928
+ }
929
+ else if (typeof config.model === "object") {
930
+ for (const [tier, model] of Object.entries(config.model)) {
931
+ if (!["fast", "balanced", "powerful"].includes(tier)) {
932
+ console.error(` ${folder}: invalid routing tier "${tier}" — must be fast, balanced, or powerful`);
933
+ process.exit(1);
934
+ }
935
+ if (typeof model === "string" && !VALID_MODELS.includes(model) && !model.includes("/")) {
936
+ console.warn(` ${folder}: model "${model}" for tier "${tier}" not in standard list`);
937
+ }
938
+ }
939
+ }
940
+ const soulPath = join(agentDir, config.instructions ?? "soul.md");
941
+ if (!existsSync(soulPath)) {
942
+ console.error(` ${folder}: instruction file not found: ${config.instructions ?? "soul.md"}`);
943
+ process.exit(1);
944
+ }
945
+ if (config.tools) {
946
+ for (const tool of config.tools) {
947
+ if (!VALID_TOOLS.includes(tool) && !tool.startsWith("custom:")) {
948
+ console.warn(` ${folder}: tool "${tool}" is not a standard grant — treating as custom`);
949
+ }
950
+ }
951
+ }
952
+ if (config.caps) {
953
+ if (config.caps.maxTurns && (config.caps.maxTurns < 1 || config.caps.maxTurns > 500)) {
954
+ console.error(` ${folder}: maxTurns must be 1-500`);
955
+ process.exit(1);
956
+ }
957
+ if (config.caps.maxCredits && config.caps.maxCredits < 1) {
958
+ console.error(` ${folder}: maxCredits must be positive`);
959
+ process.exit(1);
960
+ }
961
+ }
962
+ const deployConfig = { ...config };
963
+ deployConfig.instructionContent = readFileSync(soulPath, "utf-8");
964
+ const skillsDir = join(agentDir, "skills");
965
+ if (existsSync(skillsDir)) {
966
+ deployConfig.skills = [];
967
+ for (const skillDir of readdirSync(skillsDir)) {
968
+ const skillFile = join(skillsDir, skillDir, "SKILL.md");
969
+ if (existsSync(skillFile)) {
970
+ deployConfig.skills.push({ name: skillDir, content: readFileSync(skillFile, "utf-8") });
971
+ }
972
+ }
973
+ }
974
+ if (sharedSkills.length > 0) {
975
+ deployConfig.sharedSkills = sharedSkills;
976
+ }
977
+ const modelLabel = typeof config.model === "string" ? config.model : "dynamic";
978
+ configs[config.name] = deployConfig;
979
+ console.log(` ${config.name} (${modelLabel})`);
980
+ }
981
+ catch (err) {
982
+ if (err instanceof SyntaxError) {
983
+ console.error(` ${folder}: invalid JSON in agent.json — ${err.message}`);
984
+ }
985
+ else {
986
+ console.error(` ${folder}: failed to parse — ${err instanceof Error ? err.message : err}`);
987
+ }
988
+ process.exit(1);
989
+ }
990
+ }
991
+ return configs;
992
+ }
993
+ function parseConnectorMeta(source) {
994
+ try {
995
+ const { transformSync } = require("esbuild");
996
+ const acorn = require("acorn");
997
+ const walk = require("acorn-walk");
998
+ const js = transformSync(source, { loader: "ts", target: "es2022" }).code;
999
+ const ast = acorn.parse(js, { ecmaVersion: 2022, sourceType: "module" });
1000
+ let result = null;
1001
+ walk.simple(ast, {
1002
+ CallExpression(node) {
1003
+ const callee = node.callee?.name;
1004
+ if (callee !== "source" && callee !== "connector")
1005
+ return;
1006
+ const arg = node.arguments?.[0];
1007
+ if (!arg || arg.type !== "ObjectExpression")
1008
+ return;
1009
+ const meta = { name: "" };
1010
+ for (const prop of arg.properties ?? []) {
1011
+ if (prop.key?.name === "name" && prop.value?.type === "Literal") {
1012
+ meta.name = prop.value.value;
1013
+ }
1014
+ if (prop.key?.name === "database" && prop.value?.type === "Literal") {
1015
+ meta.database = prop.value.value;
1016
+ }
1017
+ if (prop.key?.name === "syncSchedule" && prop.value?.type === "Literal") {
1018
+ meta.syncSchedule = prop.value.value;
1019
+ }
1020
+ }
1021
+ if (meta.name)
1022
+ result = meta;
1023
+ },
1024
+ });
1025
+ return result;
1026
+ }
1027
+ catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function generateEntrypoint(cwd) {
1032
+ const lines = [];
1033
+ // Auto-discover connectors
1034
+ const connDir = resolveDir(cwd, "connectors", "src/sources");
1035
+ if (existsSync(connDir)) {
1036
+ const connBase = existsSync(join(cwd, "connectors")) ? "../connectors" : "../src/sources";
1037
+ for (const f of readdirSync(connDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."))) {
1038
+ lines.push(`import "${connBase}/${f.replace(/\.ts$/, ".js")}";`);
1039
+ }
1040
+ }
1041
+ // Auto-discover workflows
1042
+ const wfDirPath = resolveDir(cwd, "workflows", "src/workflows");
1043
+ if (existsSync(wfDirPath)) {
1044
+ const wfBase = existsSync(join(cwd, "workflows")) ? "../workflows" : "../src/workflows";
1045
+ for (const f of readdirSync(wfDirPath).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."))) {
1046
+ lines.push(`import "${wfBase}/${f.replace(/\.ts$/, ".js")}";`);
1047
+ }
1048
+ }
1049
+ // Framework exports — resolved via @mugwork/mug alias (points to CLI package runtime)
1050
+ lines.push(`export { WorkspaceDatabase, default } from "@mugwork/mug";`);
1051
+ return lines.join("\n") + "\n";
1052
+ }