skedyul 1.2.23 → 1.2.25

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.
@@ -13,4 +13,5 @@ export type { InstallConfig, ProvisionConfig, BuildConfig, CorsOptions, SkedyulC
13
13
  export { defineConfig } from './app-config';
14
14
  export { defineModel, defineChannel, definePage, defineWorkflow, defineAgent, defineEnv, defineNavigation, } from './define';
15
15
  export { CONFIG_FILE_NAMES, loadConfig, validateConfig } from './loader';
16
+ export { loadAndResolveConfig, serializeResolvedConfig, type ResolvedConfig } from './resolver';
16
17
  export { getAllEnvKeys, getRequiredInstallEnvKeys } from './utils';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Config resolution utilities for build-time config export.
3
+ *
4
+ * This module provides functions to fully resolve a skedyul.config.ts file,
5
+ * including all dynamic imports (tools, webhooks, provision).
6
+ */
7
+ import type { SkedyulConfig, SerializableSkedyulConfig, ProvisionConfig } from './app-config';
8
+ import type { ToolRegistry, WebhookRegistry } from '../types';
9
+ /**
10
+ * Resolved config with all promises awaited.
11
+ */
12
+ export interface ResolvedConfig extends Omit<SkedyulConfig, 'tools' | 'webhooks' | 'provision'> {
13
+ tools?: ToolRegistry;
14
+ webhooks?: WebhookRegistry;
15
+ provision?: ProvisionConfig;
16
+ }
17
+ /**
18
+ * Load and fully resolve a skedyul.config.ts file.
19
+ *
20
+ * This function:
21
+ * 1. Dynamically imports the config file using tsx/node
22
+ * 2. Awaits all dynamic imports (tools, webhooks, provision)
23
+ * 3. Returns the fully resolved config
24
+ *
25
+ * @param configPath - Path to the skedyul.config.ts file
26
+ * @returns Fully resolved config with all promises awaited
27
+ */
28
+ export declare function loadAndResolveConfig(configPath: string): Promise<ResolvedConfig>;
29
+ /**
30
+ * Serialize a resolved config to the format stored in the database.
31
+ *
32
+ * This converts ToolRegistry and WebhookRegistry to their metadata-only forms,
33
+ * stripping out handler functions.
34
+ *
35
+ * @param config - Fully resolved config
36
+ * @returns Serializable config for database storage
37
+ */
38
+ export declare function serializeResolvedConfig(config: ResolvedConfig): SerializableSkedyulConfig;
@@ -11,7 +11,6 @@ import type { ResourceDependency } from './resource';
11
11
  * Field data types (lowercase).
12
12
  * - 'string': Short text (single line)
13
13
  * - 'long_string': Long text (multi-line, stored as text in DB)
14
- * - 'text': Alias for long_string
15
14
  * - 'number': Numeric value
16
15
  * - 'boolean': True/false
17
16
  * - 'date': Date only (no time)
@@ -22,7 +21,7 @@ import type { ResourceDependency } from './resource';
22
21
  * - 'relation': Reference to another model
23
22
  * - 'object': JSON object
24
23
  */
25
- export type FieldType = 'string' | 'long_string' | 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'file' | 'image' | 'relation' | 'object';
24
+ export type FieldType = 'string' | 'long_string' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'file' | 'image' | 'relation' | 'object';
26
25
  /**
27
26
  * Behavior when a related record is deleted.
28
27
  * - 'none': No action (orphan the reference)
@@ -485,6 +485,7 @@ function createCallToolHandler(registry, state, onMaxRequests) {
485
485
  }
486
486
 
487
487
  // src/server/dedicated.ts
488
+ var fs = __toESM(require("fs"));
488
489
  var import_http2 = __toESM(require("http"));
489
490
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
490
491
 
@@ -1128,6 +1129,7 @@ function isMethodAllowed(webhookRegistry, handle, method) {
1128
1129
  }
1129
1130
 
1130
1131
  // src/server/dedicated.ts
1132
+ var CONFIG_FILE_PATH = ".skedyul/config.json";
1131
1133
  function createDedicatedServerInstance(config, tools, callTool, state, mcpServer) {
1132
1134
  const port = getListeningPort(config);
1133
1135
  const registry = config.tools;
@@ -1148,6 +1150,15 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1148
1150
  return;
1149
1151
  }
1150
1152
  if (pathname === "/config" && req.method === "GET") {
1153
+ try {
1154
+ if (fs.existsSync(CONFIG_FILE_PATH)) {
1155
+ const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, "utf-8"));
1156
+ sendJSON(res, 200, fileConfig);
1157
+ return;
1158
+ }
1159
+ } catch (err) {
1160
+ console.warn("[/config] Failed to read config file, falling back to runtime serialization:", err);
1161
+ }
1151
1162
  sendJSON(res, 200, serializeConfig(config));
1152
1163
  return;
1153
1164
  }
@@ -1460,6 +1471,8 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1460
1471
  }
1461
1472
 
1462
1473
  // src/server/serverless.ts
1474
+ var fs2 = __toESM(require("fs"));
1475
+ var CONFIG_FILE_PATH2 = ".skedyul/config.json";
1463
1476
  function createServerlessInstance(config, tools, callTool, state, mcpServer) {
1464
1477
  const headers = getDefaultHeaders(config.cors);
1465
1478
  const registry = config.tools;
@@ -1720,6 +1733,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
1720
1733
  return createResponse(200, state.getHealthStatus(), headers);
1721
1734
  }
1722
1735
  if (path === "/config" && method === "GET") {
1736
+ try {
1737
+ if (fs2.existsSync(CONFIG_FILE_PATH2)) {
1738
+ const fileConfig = JSON.parse(fs2.readFileSync(CONFIG_FILE_PATH2, "utf-8"));
1739
+ return createResponse(200, fileConfig, headers);
1740
+ }
1741
+ } catch (err) {
1742
+ console.warn("[/config] Failed to read config file, falling back to runtime serialization:", err);
1743
+ }
1723
1744
  return createResponse(200, serializeConfig(config), headers);
1724
1745
  }
1725
1746
  if (path === "/mcp" && method === "POST") {
@@ -13,4 +13,4 @@
13
13
  * - BUILD_EXTERNAL: comma-separated list of external dependencies (e.g., 'twilio,stripe')
14
14
  * - MCP_ENV_JSON: JSON string of environment variables to bake into the image
15
15
  */
16
- export declare const DEFAULT_DOCKERFILE = "# =============================================================================\n# BUILDER STAGE - Common build for all targets\n# =============================================================================\nFROM public.ecr.aws/docker/library/node:22-alpine AS builder\n\nARG COMPUTE_LAYER=serverless\nARG BUILD_EXTERNAL=\"\"\nWORKDIR /app\n\n# Install pnpm\nRUN corepack enable && corepack prepare pnpm@latest --activate\n\n# Copy package files and source code\nCOPY package.json tsconfig.json skedyul.config.ts ./\nCOPY src ./src\n\n# Copy optional root-level TypeScript files (provision.ts, env.ts, etc.)\n# Using a wildcard that copies all .ts files from root except those in src/\n# Note: tsup.config.ts is optional - skedyul build generates it if not present\nCOPY *.ts ./\n\n# Install dependencies (including dev deps for build), compile, smoke test, then prune\n# Note: Using --no-frozen-lockfile since lockfile may not exist\n# skedyul build reads computeLayer from skedyul.config.ts\n# Smoke test runs before pruning since skedyul CLI is a dev dependency\nRUN pnpm install --no-frozen-lockfile && \\\n pnpm run build && \\\n pnpm exec skedyul smoke-test && \\\n pnpm prune --prod && \\\n pnpm store prune && \\\n rm -rf /tmp/* /var/cache/apk/* ~/.npm\n\n# =============================================================================\n# DEDICATED STAGE - For local Docker and ECS deployments (HTTP server)\n# =============================================================================\nFROM public.ecr.aws/docker/library/node:22-alpine AS dedicated\n\nWORKDIR /app\n\n# Copy built artifacts\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/package.json ./package.json\n\n# Allow overriding the baked-in MCP env at runtime\nARG MCP_ENV_JSON=\"{}\"\nENV MCP_ENV_JSON=${MCP_ENV_JSON}\n\n# Expose the HTTP port\nEXPOSE 3000\n\n# Run as HTTP server (dedicated mode auto-detected by absence of AWS_LAMBDA_FUNCTION_NAME)\n# Support both .js (dedicated builds) and .mjs (serverless builds) extensions\nCMD [\"sh\", \"-c\", \"node dist/server/mcp_server.mjs 2>/dev/null || node dist/server/mcp_server.js\"]\n\n# =============================================================================\n# SERVERLESS STAGE - For AWS Lambda deployments\n# =============================================================================\nFROM public.ecr.aws/lambda/nodejs:22 AS serverless\n\nWORKDIR ${LAMBDA_TASK_ROOT}\n\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/package.json ./package.json\n\n# Allow overriding the baked-in MCP env at runtime\nARG MCP_ENV_JSON=\"{}\"\nENV MCP_ENV_JSON=${MCP_ENV_JSON}\n\n# Lambda handler format\nCMD [\"dist/server/mcp_server.handler\"]\n\n# =============================================================================\n# DEFAULT - Use dedicated for local development, override with --target for production\n# =============================================================================\nFROM dedicated\n";
16
+ export declare const DEFAULT_DOCKERFILE = "# =============================================================================\n# BUILDER STAGE - Common build for all targets\n# =============================================================================\nFROM public.ecr.aws/docker/library/node:22-alpine AS builder\n\nARG COMPUTE_LAYER=serverless\nARG BUILD_EXTERNAL=\"\"\nWORKDIR /app\n\n# Install pnpm\nRUN corepack enable && corepack prepare pnpm@latest --activate\n\n# Copy package files and source code\nCOPY package.json tsconfig.json skedyul.config.ts ./\nCOPY src ./src\n\n# Copy optional root-level TypeScript files (provision.ts, env.ts, etc.)\n# Using a wildcard that copies all .ts files from root except those in src/\n# Note: tsup.config.ts is optional - skedyul build generates it if not present\nCOPY *.ts ./\n\n# Install dependencies (including dev deps for build), compile, export config, smoke test, then prune\n# Note: Using --no-frozen-lockfile since lockfile may not exist\n# skedyul build reads computeLayer from skedyul.config.ts\n# skedyul config:export resolves all dynamic imports and writes .skedyul/config.json\n# Smoke test runs before pruning since skedyul CLI is a dev dependency\nRUN pnpm install --no-frozen-lockfile && \\\n pnpm run build && \\\n pnpm exec skedyul config:export && \\\n pnpm exec skedyul smoke-test && \\\n pnpm prune --prod && \\\n pnpm store prune && \\\n rm -rf /tmp/* /var/cache/apk/* ~/.npm\n\n# =============================================================================\n# DEDICATED STAGE - For local Docker and ECS deployments (HTTP server)\n# =============================================================================\nFROM public.ecr.aws/docker/library/node:22-alpine AS dedicated\n\nWORKDIR /app\n\n# Copy built artifacts\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/package.json ./package.json\nCOPY --from=builder /app/.skedyul ./.skedyul\n\n# Allow overriding the baked-in MCP env at runtime\nARG MCP_ENV_JSON=\"{}\"\nENV MCP_ENV_JSON=${MCP_ENV_JSON}\n\n# Expose the HTTP port\nEXPOSE 3000\n\n# Run as HTTP server (dedicated mode auto-detected by absence of AWS_LAMBDA_FUNCTION_NAME)\n# Support both .js (dedicated builds) and .mjs (serverless builds) extensions\nCMD [\"sh\", \"-c\", \"node dist/server/mcp_server.mjs 2>/dev/null || node dist/server/mcp_server.js\"]\n\n# =============================================================================\n# SERVERLESS STAGE - For AWS Lambda deployments\n# =============================================================================\nFROM public.ecr.aws/lambda/nodejs:22 AS serverless\n\nWORKDIR ${LAMBDA_TASK_ROOT}\n\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/package.json ./package.json\nCOPY --from=builder /app/.skedyul ./.skedyul\n\n# Allow overriding the baked-in MCP env at runtime\nARG MCP_ENV_JSON=\"{}\"\nENV MCP_ENV_JSON=${MCP_ENV_JSON}\n\n# Lambda handler format\nCMD [\"dist/server/mcp_server.handler\"]\n\n# =============================================================================\n# DEFAULT - Use dedicated for local development, override with --target for production\n# =============================================================================\nFROM dedicated\n";
@@ -900,13 +900,13 @@ function mergeRuntimeEnv() {
900
900
 
901
901
  // src/server/utils/http.ts
902
902
  function readRawRequestBody(req) {
903
- return new Promise((resolve2, reject) => {
903
+ return new Promise((resolve3, reject) => {
904
904
  let body = "";
905
905
  req.on("data", (chunk) => {
906
906
  body += chunk.toString();
907
907
  });
908
908
  req.on("end", () => {
909
- resolve2(body);
909
+ resolve3(body);
910
910
  });
911
911
  req.on("error", reject);
912
912
  });
@@ -2197,6 +2197,7 @@ function createCallToolHandler(registry, state, onMaxRequests) {
2197
2197
  }
2198
2198
 
2199
2199
  // src/server/dedicated.ts
2200
+ import * as fs from "fs";
2200
2201
  import http from "http";
2201
2202
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2202
2203
 
@@ -2729,7 +2730,7 @@ async function handleOAuthCallback(parsedBody, hooks) {
2729
2730
  }
2730
2731
 
2731
2732
  // src/server/handlers/webhook-handler.ts
2732
- function parseWebhookRequest(parsedBody, method, url, path2, headers, query, rawBody, appIdHeader, appVersionIdHeader) {
2733
+ function parseWebhookRequest(parsedBody, method, url, path3, headers, query, rawBody, appIdHeader, appVersionIdHeader) {
2733
2734
  const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
2734
2735
  if (isEnvelope) {
2735
2736
  const envelope = parsedBody;
@@ -2783,7 +2784,7 @@ function parseWebhookRequest(parsedBody, method, url, path2, headers, query, raw
2783
2784
  const webhookRequest = {
2784
2785
  method,
2785
2786
  url,
2786
- path: path2,
2787
+ path: path3,
2787
2788
  headers,
2788
2789
  query,
2789
2790
  body: parsedBody,
@@ -2840,6 +2841,7 @@ function isMethodAllowed(webhookRegistry, handle, method) {
2840
2841
  }
2841
2842
 
2842
2843
  // src/server/dedicated.ts
2844
+ var CONFIG_FILE_PATH = ".skedyul/config.json";
2843
2845
  function createDedicatedServerInstance(config, tools, callTool, state, mcpServer) {
2844
2846
  const port = getListeningPort(config);
2845
2847
  const registry = config.tools;
@@ -2860,6 +2862,15 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2860
2862
  return;
2861
2863
  }
2862
2864
  if (pathname === "/config" && req.method === "GET") {
2865
+ try {
2866
+ if (fs.existsSync(CONFIG_FILE_PATH)) {
2867
+ const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, "utf-8"));
2868
+ sendJSON(res, 200, fileConfig);
2869
+ return;
2870
+ }
2871
+ } catch (err) {
2872
+ console.warn("[/config] Failed to read config file, falling back to runtime serialization:", err);
2873
+ }
2863
2874
  sendJSON(res, 200, serializeConfig(config));
2864
2875
  return;
2865
2876
  }
@@ -3159,10 +3170,10 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
3159
3170
  return {
3160
3171
  async listen(listenPort) {
3161
3172
  const finalPort = listenPort ?? port;
3162
- return new Promise((resolve2, reject) => {
3173
+ return new Promise((resolve3, reject) => {
3163
3174
  httpServer.listen(finalPort, () => {
3164
3175
  printStartupLog(config, tools, finalPort);
3165
- resolve2();
3176
+ resolve3();
3166
3177
  });
3167
3178
  httpServer.once("error", reject);
3168
3179
  });
@@ -3172,6 +3183,8 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
3172
3183
  }
3173
3184
 
3174
3185
  // src/server/serverless.ts
3186
+ import * as fs2 from "fs";
3187
+ var CONFIG_FILE_PATH2 = ".skedyul/config.json";
3175
3188
  function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3176
3189
  const headers = getDefaultHeaders(config.cors);
3177
3190
  const registry = config.tools;
@@ -3184,13 +3197,13 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3184
3197
  hasLoggedStartup = true;
3185
3198
  }
3186
3199
  try {
3187
- const path2 = event.path || event.rawPath || "/";
3200
+ const path3 = event.path || event.rawPath || "/";
3188
3201
  const method = event.httpMethod || event.requestContext?.http?.method || "POST";
3189
3202
  if (method === "OPTIONS") {
3190
3203
  return createResponse(200, { message: "OK" }, headers);
3191
3204
  }
3192
- if (path2.startsWith("/webhooks/") && webhookRegistry) {
3193
- const handle = path2.slice("/webhooks/".length);
3205
+ if (path3.startsWith("/webhooks/") && webhookRegistry) {
3206
+ const handle = path3.slice("/webhooks/".length);
3194
3207
  if (!webhookRegistry[handle]) {
3195
3208
  return createResponse(404, { error: `Webhook handler '${handle}' not found` }, headers);
3196
3209
  }
@@ -3213,12 +3226,12 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3213
3226
  const protocol = forwardedProto ?? "https";
3214
3227
  const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
3215
3228
  const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
3216
- const webhookUrl = `${protocol}://${host}${path2}${queryString}`;
3229
+ const webhookUrl = `${protocol}://${host}${path3}${queryString}`;
3217
3230
  const parseResult = parseWebhookRequest(
3218
3231
  parsedBody,
3219
3232
  method,
3220
3233
  webhookUrl,
3221
- path2,
3234
+ path3,
3222
3235
  event.headers,
3223
3236
  event.queryStringParameters ?? {},
3224
3237
  rawBody,
@@ -3239,7 +3252,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3239
3252
  body: result.body !== void 0 ? typeof result.body === "string" ? result.body : JSON.stringify(result.body) : ""
3240
3253
  };
3241
3254
  }
3242
- if (path2 === "/core" && method === "POST") {
3255
+ if (path3 === "/core" && method === "POST") {
3243
3256
  let coreBody;
3244
3257
  try {
3245
3258
  coreBody = event.body ? JSON.parse(event.body) : {};
@@ -3271,7 +3284,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3271
3284
  const result = await handleCoreMethod(coreMethod, coreBody.params);
3272
3285
  return createResponse(result.status, result.payload, headers);
3273
3286
  }
3274
- if (path2 === "/core/webhook" && method === "POST") {
3287
+ if (path3 === "/core/webhook" && method === "POST") {
3275
3288
  const rawWebhookBody = event.body ?? "";
3276
3289
  let webhookBody;
3277
3290
  try {
@@ -3286,14 +3299,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3286
3299
  const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
3287
3300
  const protocol = forwardedProto ?? "https";
3288
3301
  const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
3289
- const webhookUrl = `${protocol}://${host}${path2}`;
3302
+ const webhookUrl = `${protocol}://${host}${path3}`;
3290
3303
  const coreWebhookRequest = {
3291
3304
  method,
3292
3305
  headers: event.headers ?? {},
3293
3306
  body: webhookBody,
3294
3307
  query: event.queryStringParameters ?? {},
3295
3308
  url: webhookUrl,
3296
- path: path2,
3309
+ path: path3,
3297
3310
  rawBody: rawWebhookBody ? Buffer.from(rawWebhookBody, "utf-8") : void 0
3298
3311
  };
3299
3312
  const webhookResponse = await coreApiService.dispatchWebhook(
@@ -3305,7 +3318,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3305
3318
  headers
3306
3319
  );
3307
3320
  }
3308
- if (path2 === "/estimate" && method === "POST") {
3321
+ if (path3 === "/estimate" && method === "POST") {
3309
3322
  let estimateBody;
3310
3323
  try {
3311
3324
  estimateBody = event.body ? JSON.parse(event.body) : {};
@@ -3371,7 +3384,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3371
3384
  );
3372
3385
  }
3373
3386
  }
3374
- if (path2 === "/install" && method === "POST") {
3387
+ if (path3 === "/install" && method === "POST") {
3375
3388
  let installBody;
3376
3389
  try {
3377
3390
  installBody = event.body ? JSON.parse(event.body) : {};
@@ -3385,7 +3398,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3385
3398
  const result = await handleInstall(installBody, config.hooks);
3386
3399
  return createResponse(result.status, result.body, headers);
3387
3400
  }
3388
- if (path2 === "/uninstall" && method === "POST") {
3401
+ if (path3 === "/uninstall" && method === "POST") {
3389
3402
  let uninstallBody;
3390
3403
  try {
3391
3404
  uninstallBody = event.body ? JSON.parse(event.body) : {};
@@ -3399,7 +3412,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3399
3412
  const result = await handleUninstall(uninstallBody, config.hooks);
3400
3413
  return createResponse(result.status, result.body, headers);
3401
3414
  }
3402
- if (path2 === "/provision" && method === "POST") {
3415
+ if (path3 === "/provision" && method === "POST") {
3403
3416
  let provisionBody;
3404
3417
  try {
3405
3418
  provisionBody = event.body ? JSON.parse(event.body) : {};
@@ -3413,7 +3426,7 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3413
3426
  const result = await handleProvision(provisionBody, config.hooks);
3414
3427
  return createResponse(result.status, result.body, headers);
3415
3428
  }
3416
- if (path2 === "/oauth_callback" && method === "POST") {
3429
+ if (path3 === "/oauth_callback" && method === "POST") {
3417
3430
  let parsedBody;
3418
3431
  try {
3419
3432
  parsedBody = event.body ? JSON.parse(event.body) : {};
@@ -3428,13 +3441,21 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3428
3441
  const result = await handleOAuthCallback(parsedBody, config.hooks);
3429
3442
  return createResponse(result.status, result.body, headers);
3430
3443
  }
3431
- if (path2 === "/health" && method === "GET") {
3444
+ if (path3 === "/health" && method === "GET") {
3432
3445
  return createResponse(200, state.getHealthStatus(), headers);
3433
3446
  }
3434
- if (path2 === "/config" && method === "GET") {
3447
+ if (path3 === "/config" && method === "GET") {
3448
+ try {
3449
+ if (fs2.existsSync(CONFIG_FILE_PATH2)) {
3450
+ const fileConfig = JSON.parse(fs2.readFileSync(CONFIG_FILE_PATH2, "utf-8"));
3451
+ return createResponse(200, fileConfig, headers);
3452
+ }
3453
+ } catch (err) {
3454
+ console.warn("[/config] Failed to read config file, falling back to runtime serialization:", err);
3455
+ }
3435
3456
  return createResponse(200, serializeConfig(config), headers);
3436
3457
  }
3437
- if (path2 === "/mcp" && method === "POST") {
3458
+ if (path3 === "/mcp" && method === "POST") {
3438
3459
  let body;
3439
3460
  try {
3440
3461
  body = event.body ? JSON.parse(event.body) : {};
@@ -3842,12 +3863,14 @@ COPY src ./src
3842
3863
  # Note: tsup.config.ts is optional - skedyul build generates it if not present
3843
3864
  COPY *.ts ./
3844
3865
 
3845
- # Install dependencies (including dev deps for build), compile, smoke test, then prune
3866
+ # Install dependencies (including dev deps for build), compile, export config, smoke test, then prune
3846
3867
  # Note: Using --no-frozen-lockfile since lockfile may not exist
3847
3868
  # skedyul build reads computeLayer from skedyul.config.ts
3869
+ # skedyul config:export resolves all dynamic imports and writes .skedyul/config.json
3848
3870
  # Smoke test runs before pruning since skedyul CLI is a dev dependency
3849
3871
  RUN pnpm install --no-frozen-lockfile && \\
3850
3872
  pnpm run build && \\
3873
+ pnpm exec skedyul config:export && \\
3851
3874
  pnpm exec skedyul smoke-test && \\
3852
3875
  pnpm prune --prod && \\
3853
3876
  pnpm store prune && \\
@@ -3864,6 +3887,7 @@ WORKDIR /app
3864
3887
  COPY --from=builder /app/node_modules ./node_modules
3865
3888
  COPY --from=builder /app/dist ./dist
3866
3889
  COPY --from=builder /app/package.json ./package.json
3890
+ COPY --from=builder /app/.skedyul ./.skedyul
3867
3891
 
3868
3892
  # Allow overriding the baked-in MCP env at runtime
3869
3893
  ARG MCP_ENV_JSON="{}"
@@ -3886,6 +3910,7 @@ WORKDIR \${LAMBDA_TASK_ROOT}
3886
3910
  COPY --from=builder /app/node_modules ./node_modules
3887
3911
  COPY --from=builder /app/dist ./dist
3888
3912
  COPY --from=builder /app/package.json ./package.json
3913
+ COPY --from=builder /app/.skedyul ./.skedyul
3889
3914
 
3890
3915
  # Allow overriding the baked-in MCP env at runtime
3891
3916
  ARG MCP_ENV_JSON="{}"
@@ -3929,7 +3954,7 @@ function defineNavigation(navigation) {
3929
3954
  }
3930
3955
 
3931
3956
  // src/config/loader.ts
3932
- import * as fs from "fs";
3957
+ import * as fs3 from "fs";
3933
3958
  import * as path from "path";
3934
3959
  import * as os from "os";
3935
3960
  var CONFIG_FILE_NAMES = [
@@ -3939,7 +3964,7 @@ var CONFIG_FILE_NAMES = [
3939
3964
  "skedyul.config.cjs"
3940
3965
  ];
3941
3966
  async function transpileTypeScript(filePath) {
3942
- const content = fs.readFileSync(filePath, "utf-8");
3967
+ const content = fs3.readFileSync(filePath, "utf-8");
3943
3968
  const configDir = path.dirname(path.resolve(filePath));
3944
3969
  let transpiled = content.replace(/import\s+type\s+\{[^}]+\}\s+from\s+['"][^'"]+['"]\s*;?\n?/g, "").replace(/import\s+\{\s*defineConfig\s*\}\s+from\s+['"]skedyul['"]\s*;?\n?/g, "").replace(/:\s*SkedyulConfig/g, "").replace(/export\s+default\s+/, "module.exports = ").replace(/defineConfig\s*\(\s*\{/, "{").replace(/\}\s*\)\s*;?\s*$/, "}");
3945
3970
  transpiled = transpiled.replace(
@@ -3954,7 +3979,7 @@ async function transpileTypeScript(filePath) {
3954
3979
  }
3955
3980
  async function loadConfig(configPath) {
3956
3981
  const absolutePath = path.resolve(configPath);
3957
- if (!fs.existsSync(absolutePath)) {
3982
+ if (!fs3.existsSync(absolutePath)) {
3958
3983
  throw new Error(`Config file not found: ${absolutePath}`);
3959
3984
  }
3960
3985
  const isTypeScript = absolutePath.endsWith(".ts");
@@ -3964,7 +3989,7 @@ async function loadConfig(configPath) {
3964
3989
  const transpiled = await transpileTypeScript(absolutePath);
3965
3990
  const tempDir = os.tmpdir();
3966
3991
  const tempFile = path.join(tempDir, `skedyul-config-${Date.now()}.js`);
3967
- fs.writeFileSync(tempFile, transpiled);
3992
+ fs3.writeFileSync(tempFile, transpiled);
3968
3993
  moduleToLoad = tempFile;
3969
3994
  try {
3970
3995
  const module2 = __require(moduleToLoad);
@@ -3978,7 +4003,7 @@ async function loadConfig(configPath) {
3978
4003
  return config2;
3979
4004
  } finally {
3980
4005
  try {
3981
- fs.unlinkSync(tempFile);
4006
+ fs3.unlinkSync(tempFile);
3982
4007
  } catch {
3983
4008
  }
3984
4009
  }
@@ -4009,6 +4034,9 @@ function validateConfig(config) {
4009
4034
  return { valid: errors.length === 0, errors };
4010
4035
  }
4011
4036
 
4037
+ // src/config/resolver.ts
4038
+ import * as path2 from "path";
4039
+
4012
4040
  // src/config/utils.ts
4013
4041
  function getAllEnvKeys(config) {
4014
4042
  const provision = config.provision && "env" in config.provision ? config.provision : void 0;