@meetploy/cli 1.14.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,11 +8,11 @@ import { parse } from 'yaml';
8
8
  import { build } from 'esbuild';
9
9
  import { watch } from 'chokidar';
10
10
  import { randomBytes, randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, createHash } from 'crypto';
11
+ import { homedir, tmpdir } from 'os';
11
12
  import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
12
13
  import { URL, fileURLToPath } from 'url';
13
14
  import { serve } from '@hono/node-server';
14
15
  import { Hono } from 'hono';
15
- import { homedir, tmpdir } from 'os';
16
16
  import Database from 'better-sqlite3';
17
17
  import { spawn, exec } from 'child_process';
18
18
  import createClient from 'openapi-fetch';
@@ -183,10 +183,24 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
183
183
  validatedConfig.out = validateRelativePath(config.out, "out", configFile);
184
184
  validatedConfig.base = validateRelativePath(config.base, "base", configFile);
185
185
  validatedConfig.main = validateRelativePath(config.main, "main", configFile);
186
+ if (config.env !== void 0) {
187
+ if (typeof config.env !== "object" || config.env === null) {
188
+ throw new Error(`'env' in ${configFile} must be a map of KEY: value`);
189
+ }
190
+ for (const [key, value] of Object.entries(config.env)) {
191
+ if (!BINDING_NAME_REGEX.test(key)) {
192
+ throw new Error(`Invalid env key '${key}' in ${configFile}. Env keys must be uppercase with underscores (e.g., FOO, MY_VAR)`);
193
+ }
194
+ if (typeof value !== "string") {
195
+ throw new Error(`Env key '${key}' in ${configFile} must have a string value`);
196
+ }
197
+ }
198
+ }
186
199
  validateBindings(config.db, "db", configFile);
187
200
  validateBindings(config.queue, "queue", configFile);
188
201
  validateBindings(config.cache, "cache", configFile);
189
202
  validateBindings(config.state, "state", configFile);
203
+ validateBindings(config.fs, "fs", configFile);
190
204
  validateBindings(config.workflow, "workflow", configFile);
191
205
  if (config.ai !== void 0 && typeof config.ai !== "boolean") {
192
206
  throw new Error(`'ai' in ${configFile} must be a boolean`);
@@ -263,7 +277,50 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
263
277
  return validatePloyConfig(config, configFile, validationOptions);
264
278
  }
265
279
  function hasBindings(config) {
266
- return !!(config.db ?? config.queue ?? config.cache ?? config.state ?? config.workflow ?? config.ai ?? config.auth);
280
+ return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.ai ?? config.auth);
281
+ }
282
+ function parseDotEnv(content) {
283
+ const result = {};
284
+ for (const line of content.split("\n")) {
285
+ const trimmed = line.trim();
286
+ if (!trimmed || trimmed.startsWith("#")) {
287
+ continue;
288
+ }
289
+ const eqIndex = trimmed.indexOf("=");
290
+ if (eqIndex === -1) {
291
+ continue;
292
+ }
293
+ const key = trimmed.slice(0, eqIndex).trim();
294
+ let value = trimmed.slice(eqIndex + 1).trim();
295
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
296
+ value = value.slice(1, -1);
297
+ }
298
+ result[key] = value;
299
+ }
300
+ return result;
301
+ }
302
+ function loadDotEnvSync(projectDir) {
303
+ const envPath = join(projectDir, ".env");
304
+ if (!existsSync(envPath)) {
305
+ return {};
306
+ }
307
+ const content = readFileSync(envPath, "utf-8");
308
+ return parseDotEnv(content);
309
+ }
310
+ function resolveEnvVars(configEnv, dotEnv, processEnv) {
311
+ const result = {};
312
+ for (const [key, value] of Object.entries(configEnv)) {
313
+ if (value.startsWith("$")) {
314
+ const refName = value.slice(1);
315
+ const resolved = dotEnv[refName] ?? processEnv[refName];
316
+ if (resolved !== void 0) {
317
+ result[key] = resolved;
318
+ }
319
+ } else {
320
+ result[key] = value;
321
+ }
322
+ }
323
+ return result;
267
324
  }
268
325
  function getWorkerEntryPoint(projectDir, config) {
269
326
  if (config.main) {
@@ -306,10 +363,13 @@ __export(cli_exports, {
306
363
  getWorkerEntryPoint: () => getWorkerEntryPoint,
307
364
  hasBindings: () => hasBindings,
308
365
  isPnpmWorkspace: () => isPnpmWorkspace,
366
+ loadDotEnvSync: () => loadDotEnvSync,
367
+ parseDotEnv: () => parseDotEnv,
309
368
  readAndValidatePloyConfig: () => readAndValidatePloyConfig,
310
369
  readAndValidatePloyConfigSync: () => readAndValidatePloyConfigSync,
311
370
  readPloyConfig: () => readPloyConfig,
312
371
  readPloyConfigSync: () => readPloyConfigSync,
372
+ resolveEnvVars: () => resolveEnvVars,
313
373
  validatePloyConfig: () => validatePloyConfig
314
374
  });
315
375
  var init_cli = __esm({
@@ -698,6 +758,124 @@ var init_db_runtime = __esm({
698
758
  }
699
759
  });
700
760
 
761
+ // ../emulator/dist/runtime/fs-runtime.js
762
+ var FS_RUNTIME_CODE;
763
+ var init_fs_runtime = __esm({
764
+ "../emulator/dist/runtime/fs-runtime.js"() {
765
+ FS_RUNTIME_CODE = `
766
+ interface FileStoragePutOptions {
767
+ contentType?: string;
768
+ }
769
+
770
+ interface FileStorageListOptions {
771
+ prefix?: string;
772
+ limit?: number;
773
+ }
774
+
775
+ interface FileStorageObject {
776
+ body: string;
777
+ contentType: string;
778
+ size: number;
779
+ }
780
+
781
+ interface FileStorageKey {
782
+ key: string;
783
+ size: number;
784
+ contentType: string;
785
+ }
786
+
787
+ interface FileStorageListResult {
788
+ keys: FileStorageKey[];
789
+ }
790
+
791
+ interface FileStorageBinding {
792
+ put: (key: string, value: string, options?: FileStoragePutOptions) => Promise<void>;
793
+ get: (key: string) => Promise<FileStorageObject | null>;
794
+ delete: (key: string) => Promise<void>;
795
+ list: (options?: FileStorageListOptions) => Promise<FileStorageListResult>;
796
+ }
797
+
798
+ export function initializeFileStorage(fsName: string, serviceUrl: string): FileStorageBinding {
799
+ return {
800
+ async put(key: string, value: string, options?: FileStoragePutOptions): Promise<void> {
801
+ const response = await fetch(serviceUrl + "/fs/put", {
802
+ method: "POST",
803
+ headers: { "Content-Type": "application/json" },
804
+ body: JSON.stringify({
805
+ fsName,
806
+ key,
807
+ value,
808
+ contentType: options?.contentType || "application/octet-stream",
809
+ }),
810
+ });
811
+
812
+ if (!response.ok) {
813
+ const errorText = await response.text();
814
+ throw new Error("File storage put failed: " + errorText);
815
+ }
816
+ },
817
+
818
+ async get(key: string): Promise<FileStorageObject | null> {
819
+ const response = await fetch(serviceUrl + "/fs/get", {
820
+ method: "POST",
821
+ headers: { "Content-Type": "application/json" },
822
+ body: JSON.stringify({ fsName, key }),
823
+ });
824
+
825
+ if (!response.ok) {
826
+ const errorText = await response.text();
827
+ throw new Error("File storage get failed: " + errorText);
828
+ }
829
+
830
+ const result = await response.json();
831
+ if (result.found === false) {
832
+ return null;
833
+ }
834
+ return {
835
+ body: result.body,
836
+ contentType: result.contentType,
837
+ size: result.size,
838
+ };
839
+ },
840
+
841
+ async delete(key: string): Promise<void> {
842
+ const response = await fetch(serviceUrl + "/fs/delete", {
843
+ method: "POST",
844
+ headers: { "Content-Type": "application/json" },
845
+ body: JSON.stringify({ fsName, key }),
846
+ });
847
+
848
+ if (!response.ok) {
849
+ const errorText = await response.text();
850
+ throw new Error("File storage delete failed: " + errorText);
851
+ }
852
+ },
853
+
854
+ async list(options?: FileStorageListOptions): Promise<FileStorageListResult> {
855
+ const response = await fetch(serviceUrl + "/fs/list", {
856
+ method: "POST",
857
+ headers: { "Content-Type": "application/json" },
858
+ body: JSON.stringify({
859
+ fsName,
860
+ prefix: options?.prefix,
861
+ limit: options?.limit,
862
+ }),
863
+ });
864
+
865
+ if (!response.ok) {
866
+ const errorText = await response.text();
867
+ throw new Error("File storage list failed: " + errorText);
868
+ }
869
+
870
+ const result = await response.json();
871
+ return { keys: result.keys };
872
+ },
873
+ };
874
+ }
875
+ `;
876
+ }
877
+ });
878
+
701
879
  // ../emulator/dist/runtime/queue-runtime.js
702
880
  var QUEUE_RUNTIME_CODE;
703
881
  var init_queue_runtime = __esm({
@@ -1035,7 +1213,7 @@ export async function executeWorkflow<TInput, TOutput, TEnv>(
1035
1213
  `;
1036
1214
  }
1037
1215
  });
1038
- function generateWrapperCode(config, mockServiceUrl) {
1216
+ function generateWrapperCode(config, mockServiceUrl, envVars) {
1039
1217
  const imports = [];
1040
1218
  const bindings = [];
1041
1219
  if (config.db) {
@@ -1062,6 +1240,12 @@ function generateWrapperCode(config, mockServiceUrl) {
1062
1240
  bindings.push(` ${bindingName}: initializeState("${stateName}", "${mockServiceUrl}"),`);
1063
1241
  }
1064
1242
  }
1243
+ if (config.fs) {
1244
+ imports.push('import { initializeFileStorage } from "__ploy_fs_runtime__";');
1245
+ for (const [bindingName, fsName] of Object.entries(config.fs)) {
1246
+ bindings.push(` ${bindingName}: initializeFileStorage("${fsName}", "${mockServiceUrl}"),`);
1247
+ }
1248
+ }
1065
1249
  if (config.workflow) {
1066
1250
  imports.push('import { initializeWorkflow, createStepContext, executeWorkflow } from "__ploy_workflow_runtime__";');
1067
1251
  for (const [bindingName, workflowName] of Object.entries(config.workflow)) {
@@ -1125,6 +1309,11 @@ function generateWrapperCode(config, mockServiceUrl) {
1125
1309
  }
1126
1310
  }
1127
1311
  }` : "";
1312
+ const envVarsEntries = envVars && Object.keys(envVars).length > 0 ? Object.entries(envVars).map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}`).join(",\n") : "";
1313
+ const envVarsCode = envVarsEntries ? `
1314
+ injectedEnv.vars = {
1315
+ ${envVarsEntries}
1316
+ };` : "";
1128
1317
  return `${imports.join("\n")}
1129
1318
 
1130
1319
  const ployBindings = {
@@ -1133,7 +1322,7 @@ ${bindings.join("\n")}
1133
1322
 
1134
1323
  export default {
1135
1324
  async fetch(request, env, ctx) {
1136
- const injectedEnv = { ...env, ...ployBindings };
1325
+ const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
1137
1326
  ${workflowHandlerCode}
1138
1327
  ${queueHandlerCode}
1139
1328
 
@@ -1146,7 +1335,7 @@ export default {
1146
1335
 
1147
1336
  async scheduled(event, env, ctx) {
1148
1337
  if (userWorker.scheduled) {
1149
- const injectedEnv = { ...env, ...ployBindings };
1338
+ const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
1150
1339
  return userWorker.scheduled(event, injectedEnv, ctx);
1151
1340
  }
1152
1341
  }
@@ -1173,6 +1362,10 @@ function createRuntimePlugin(_config) {
1173
1362
  path: "__ploy_state_runtime__",
1174
1363
  namespace: "ploy-runtime"
1175
1364
  }));
1365
+ build2.onResolve({ filter: /^__ploy_fs_runtime__$/ }, () => ({
1366
+ path: "__ploy_fs_runtime__",
1367
+ namespace: "ploy-runtime"
1368
+ }));
1176
1369
  build2.onResolve({ filter: /^__ploy_workflow_runtime__$/ }, () => ({
1177
1370
  path: "__ploy_workflow_runtime__",
1178
1371
  namespace: "ploy-runtime"
@@ -1193,6 +1386,10 @@ function createRuntimePlugin(_config) {
1193
1386
  contents: STATE_RUNTIME_CODE,
1194
1387
  loader: "ts"
1195
1388
  }));
1389
+ build2.onLoad({ filter: /^__ploy_fs_runtime__$/, namespace: "ploy-runtime" }, () => ({
1390
+ contents: FS_RUNTIME_CODE,
1391
+ loader: "ts"
1392
+ }));
1196
1393
  build2.onLoad({ filter: /^__ploy_workflow_runtime__$/, namespace: "ploy-runtime" }, () => ({
1197
1394
  contents: WORKFLOW_RUNTIME_CODE,
1198
1395
  loader: "ts"
@@ -1201,8 +1398,8 @@ function createRuntimePlugin(_config) {
1201
1398
  };
1202
1399
  }
1203
1400
  async function bundleWorker(options) {
1204
- const { projectDir, tempDir, entryPoint, config, mockServiceUrl } = options;
1205
- const wrapperCode = generateWrapperCode(config, mockServiceUrl);
1401
+ const { projectDir, tempDir, entryPoint, config, mockServiceUrl, envVars } = options;
1402
+ const wrapperCode = generateWrapperCode(config, mockServiceUrl, envVars);
1206
1403
  const wrapperPath = join(tempDir, "wrapper.ts");
1207
1404
  writeFileSync(wrapperPath, wrapperCode);
1208
1405
  const bundlePath = join(tempDir, "worker.bundle.js");
@@ -1235,6 +1432,7 @@ var init_bundler = __esm({
1235
1432
  "../emulator/dist/bundler/bundler.js"() {
1236
1433
  init_cache_runtime();
1237
1434
  init_db_runtime();
1435
+ init_fs_runtime();
1238
1436
  init_queue_runtime();
1239
1437
  init_state_runtime();
1240
1438
  init_workflow_runtime();
@@ -1412,6 +1610,20 @@ var init_watcher = __esm({
1412
1610
  }
1413
1611
  });
1414
1612
 
1613
+ // ../emulator/dist/config/env.js
1614
+ function resolveEnvVars2(projectDir, config) {
1615
+ if (!config.env) {
1616
+ return {};
1617
+ }
1618
+ const dotEnv = loadDotEnvSync(projectDir);
1619
+ return resolveEnvVars(config.env, dotEnv, process.env);
1620
+ }
1621
+ var init_env = __esm({
1622
+ "../emulator/dist/config/env.js"() {
1623
+ init_cli();
1624
+ }
1625
+ });
1626
+
1415
1627
  // ../emulator/dist/config/ploy-config.js
1416
1628
  function readPloyConfig2(projectDir, configPath) {
1417
1629
  const config = readPloyConfigSync(projectDir, configPath);
@@ -1481,6 +1693,34 @@ var init_workerd_config = __esm({
1481
1693
  "../emulator/dist/config/workerd-config.js"() {
1482
1694
  }
1483
1695
  });
1696
+ function getProjectHash(projectDir) {
1697
+ return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
1698
+ }
1699
+ function getTempDir(projectDir) {
1700
+ const hash = getProjectHash(projectDir);
1701
+ return join(tmpdir(), `ploy-emulator-${hash}`);
1702
+ }
1703
+ function getDataDir(projectDir) {
1704
+ return join(projectDir, ".ploy");
1705
+ }
1706
+ function ensureDir(dir) {
1707
+ mkdirSync(dir, { recursive: true });
1708
+ }
1709
+ function ensureTempDir(projectDir) {
1710
+ const tempDir = getTempDir(projectDir);
1711
+ ensureDir(tempDir);
1712
+ return tempDir;
1713
+ }
1714
+ function ensureDataDir(projectDir) {
1715
+ const dataDir = getDataDir(projectDir);
1716
+ ensureDir(dataDir);
1717
+ ensureDir(join(dataDir, "db"));
1718
+ return dataDir;
1719
+ }
1720
+ var init_paths = __esm({
1721
+ "../emulator/dist/utils/paths.js"() {
1722
+ }
1723
+ });
1484
1724
  function generateId() {
1485
1725
  return randomBytes(16).toString("hex");
1486
1726
  }
@@ -1793,6 +2033,7 @@ function createDashboardRoutes(app, dbManager2, config) {
1793
2033
  queue: config.queue,
1794
2034
  cache: config.cache,
1795
2035
  state: config.state,
2036
+ fs: config.fs,
1796
2037
  workflow: config.workflow,
1797
2038
  auth: config.auth
1798
2039
  });
@@ -2287,6 +2528,37 @@ function createDashboardRoutes(app, dbManager2, config) {
2287
2528
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2288
2529
  }
2289
2530
  });
2531
+ app.get("/api/fs/:binding/entries", (c) => {
2532
+ const binding = c.req.param("binding");
2533
+ const fsName = config.fs?.[binding];
2534
+ const limit = parseInt(c.req.query("limit") || "20", 10);
2535
+ const offset = parseInt(c.req.query("offset") || "0", 10);
2536
+ if (!fsName) {
2537
+ return c.json({ error: "File storage binding not found" }, 404);
2538
+ }
2539
+ try {
2540
+ const db = dbManager2.emulatorDb;
2541
+ const total = db.prepare(`SELECT COUNT(*) as count FROM fs_entries WHERE fs_name = ?`).get(fsName).count;
2542
+ const entries = db.prepare(`SELECT key, size, content_type, created_at
2543
+ FROM fs_entries
2544
+ WHERE fs_name = ?
2545
+ ORDER BY key ASC
2546
+ LIMIT ? OFFSET ?`).all(fsName, limit, offset);
2547
+ return c.json({
2548
+ entries: entries.map((e) => ({
2549
+ key: e.key,
2550
+ size: e.size,
2551
+ contentType: e.content_type,
2552
+ createdAt: new Date(e.created_at * 1e3).toISOString()
2553
+ })),
2554
+ total,
2555
+ limit,
2556
+ offset
2557
+ });
2558
+ } catch (err) {
2559
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2560
+ }
2561
+ });
2290
2562
  if (hasDashboard) {
2291
2563
  app.get("/assets/*", (c) => {
2292
2564
  const path = c.req.path;
@@ -2437,6 +2709,125 @@ var init_db_service = __esm({
2437
2709
  "../emulator/dist/services/db-service.js"() {
2438
2710
  }
2439
2711
  });
2712
+ function createFsHandlers(db, dataDir) {
2713
+ const fsBaseDir = join(dataDir, "fs");
2714
+ mkdirSync(fsBaseDir, { recursive: true });
2715
+ function getFsDir(fsName) {
2716
+ const dir = join(fsBaseDir, fsName);
2717
+ mkdirSync(dir, { recursive: true });
2718
+ return dir;
2719
+ }
2720
+ function encodedKeyPath(fsDir, key) {
2721
+ return join(fsDir, encodeURIComponent(key));
2722
+ }
2723
+ const putHandler = async (c) => {
2724
+ try {
2725
+ const body = await c.req.json();
2726
+ const { fsName, key, value, contentType } = body;
2727
+ if (!fsName || !key) {
2728
+ return c.json({ error: "Missing required fields: fsName, key, value" }, 400);
2729
+ }
2730
+ const fsDir = getFsDir(fsName);
2731
+ const filePath = encodedKeyPath(fsDir, key);
2732
+ writeFileSync(filePath, value, "utf-8");
2733
+ const size = Buffer.byteLength(value, "utf-8");
2734
+ const now = Math.floor(Date.now() / 1e3);
2735
+ db.prepare(`INSERT OR REPLACE INTO fs_entries (fs_name, key, content_type, size, created_at) VALUES (?, ?, ?, ?, ?)`).run(fsName, key, contentType ?? "application/octet-stream", size, now);
2736
+ return c.json({ success: true });
2737
+ } catch (err) {
2738
+ error(`[fs-service] put error: ${err instanceof Error ? err.message : String(err)}`);
2739
+ return c.json({
2740
+ error: `put failed: ${err instanceof Error ? err.message : String(err)}`
2741
+ }, 500);
2742
+ }
2743
+ };
2744
+ const getHandler = async (c) => {
2745
+ try {
2746
+ const body = await c.req.json();
2747
+ const { fsName, key } = body;
2748
+ if (!fsName || !key) {
2749
+ return c.json({ error: "Missing required fields: fsName, key" }, 400);
2750
+ }
2751
+ const row = db.prepare(`SELECT content_type, size FROM fs_entries WHERE fs_name = ? AND key = ?`).get(fsName, key);
2752
+ if (!row) {
2753
+ return c.json({ found: false });
2754
+ }
2755
+ const fsDir = getFsDir(fsName);
2756
+ const filePath = encodedKeyPath(fsDir, key);
2757
+ if (!existsSync(filePath)) {
2758
+ db.prepare(`DELETE FROM fs_entries WHERE fs_name = ? AND key = ?`).run(fsName, key);
2759
+ return c.json({ found: false });
2760
+ }
2761
+ const fileContent = readFileSync(filePath, "utf-8");
2762
+ return c.json({
2763
+ found: true,
2764
+ body: fileContent,
2765
+ contentType: row.content_type,
2766
+ size: row.size
2767
+ });
2768
+ } catch (err) {
2769
+ error(`[fs-service] get error: ${err instanceof Error ? err.message : String(err)}`);
2770
+ return c.json({
2771
+ error: `get failed: ${err instanceof Error ? err.message : String(err)}`
2772
+ }, 500);
2773
+ }
2774
+ };
2775
+ const deleteHandler = async (c) => {
2776
+ try {
2777
+ const body = await c.req.json();
2778
+ const { fsName, key } = body;
2779
+ if (!fsName || !key) {
2780
+ return c.json({ error: "Missing required fields: fsName, key" }, 400);
2781
+ }
2782
+ db.prepare(`DELETE FROM fs_entries WHERE fs_name = ? AND key = ?`).run(fsName, key);
2783
+ const fsDir = getFsDir(fsName);
2784
+ const filePath = encodedKeyPath(fsDir, key);
2785
+ if (existsSync(filePath)) {
2786
+ unlinkSync(filePath);
2787
+ }
2788
+ return c.json({ success: true });
2789
+ } catch (err) {
2790
+ error(`[fs-service] delete error: ${err instanceof Error ? err.message : String(err)}`);
2791
+ return c.json({
2792
+ error: `delete failed: ${err instanceof Error ? err.message : String(err)}`
2793
+ }, 500);
2794
+ }
2795
+ };
2796
+ const listHandler = async (c) => {
2797
+ try {
2798
+ const body = await c.req.json();
2799
+ const { fsName, prefix, limit } = body;
2800
+ if (!fsName) {
2801
+ return c.json({ error: "Missing required field: fsName" }, 400);
2802
+ }
2803
+ const effectiveLimit = limit ?? 1e3;
2804
+ const keys = prefix ? db.prepare(`SELECT key, size, content_type FROM fs_entries WHERE fs_name = ? AND key LIKE ? ORDER BY key ASC LIMIT ?`).all(fsName, `${prefix}%`, effectiveLimit) : db.prepare(`SELECT key, size, content_type FROM fs_entries WHERE fs_name = ? ORDER BY key ASC LIMIT ?`).all(fsName, effectiveLimit);
2805
+ return c.json({
2806
+ keys: keys.map((k) => ({
2807
+ key: k.key,
2808
+ size: k.size,
2809
+ contentType: k.content_type
2810
+ }))
2811
+ });
2812
+ } catch (err) {
2813
+ error(`[fs-service] list error: ${err instanceof Error ? err.message : String(err)}`);
2814
+ return c.json({
2815
+ error: `list failed: ${err instanceof Error ? err.message : String(err)}`
2816
+ }, 500);
2817
+ }
2818
+ };
2819
+ return {
2820
+ putHandler,
2821
+ getHandler,
2822
+ deleteHandler,
2823
+ listHandler
2824
+ };
2825
+ }
2826
+ var init_fs_service = __esm({
2827
+ "../emulator/dist/services/fs-service.js"() {
2828
+ init_logger();
2829
+ }
2830
+ });
2440
2831
  function createQueueHandlers(db) {
2441
2832
  const sendHandler = async (c) => {
2442
2833
  try {
@@ -2870,6 +3261,14 @@ async function startMockServer(dbManager2, config, options = {}) {
2870
3261
  app.post("/state/set", stateHandlers.setHandler);
2871
3262
  app.post("/state/delete", stateHandlers.deleteHandler);
2872
3263
  }
3264
+ if (config.fs) {
3265
+ const dataDir = getDataDir(process.cwd());
3266
+ const fsHandlers = createFsHandlers(dbManager2.emulatorDb, dataDir);
3267
+ app.post("/fs/put", fsHandlers.putHandler);
3268
+ app.post("/fs/get", fsHandlers.getHandler);
3269
+ app.post("/fs/delete", fsHandlers.deleteHandler);
3270
+ app.post("/fs/list", fsHandlers.listHandler);
3271
+ }
2873
3272
  if (config.auth) {
2874
3273
  const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
2875
3274
  app.post("/auth/signup", authHandlers.signupHandler);
@@ -2901,44 +3300,18 @@ async function startMockServer(dbManager2, config, options = {}) {
2901
3300
  var DEFAULT_MOCK_SERVER_PORT;
2902
3301
  var init_mock_server = __esm({
2903
3302
  "../emulator/dist/services/mock-server.js"() {
3303
+ init_paths();
2904
3304
  init_auth_service();
2905
3305
  init_cache_service();
2906
3306
  init_dashboard_routes();
2907
3307
  init_db_service();
3308
+ init_fs_service();
2908
3309
  init_queue_service();
2909
3310
  init_state_service();
2910
3311
  init_workflow_service();
2911
3312
  DEFAULT_MOCK_SERVER_PORT = 4003;
2912
3313
  }
2913
3314
  });
2914
- function getProjectHash(projectDir) {
2915
- return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
2916
- }
2917
- function getTempDir(projectDir) {
2918
- const hash = getProjectHash(projectDir);
2919
- return join(tmpdir(), `ploy-emulator-${hash}`);
2920
- }
2921
- function getDataDir(projectDir) {
2922
- return join(projectDir, ".ploy");
2923
- }
2924
- function ensureDir(dir) {
2925
- mkdirSync(dir, { recursive: true });
2926
- }
2927
- function ensureTempDir(projectDir) {
2928
- const tempDir = getTempDir(projectDir);
2929
- ensureDir(tempDir);
2930
- return tempDir;
2931
- }
2932
- function ensureDataDir(projectDir) {
2933
- const dataDir = getDataDir(projectDir);
2934
- ensureDir(dataDir);
2935
- ensureDir(join(dataDir, "db"));
2936
- return dataDir;
2937
- }
2938
- var init_paths = __esm({
2939
- "../emulator/dist/utils/paths.js"() {
2940
- }
2941
- });
2942
3315
  function initializeDatabases(projectDir) {
2943
3316
  const dataDir = ensureDataDir(projectDir);
2944
3317
  const d1Databases = /* @__PURE__ */ new Map();
@@ -3080,6 +3453,16 @@ CREATE TABLE IF NOT EXISTS state_entries (
3080
3453
  value TEXT NOT NULL,
3081
3454
  PRIMARY KEY (state_name, key)
3082
3455
  );
3456
+
3457
+ -- File storage entries table (metadata for stored files)
3458
+ CREATE TABLE IF NOT EXISTS fs_entries (
3459
+ fs_name TEXT NOT NULL,
3460
+ key TEXT NOT NULL,
3461
+ content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
3462
+ size INTEGER NOT NULL DEFAULT 0,
3463
+ created_at INTEGER DEFAULT (strftime('%s', 'now')),
3464
+ PRIMARY KEY (fs_name, key)
3465
+ );
3083
3466
  `;
3084
3467
  }
3085
3468
  });
@@ -3093,6 +3476,7 @@ var init_emulator = __esm({
3093
3476
  "../emulator/dist/emulator.js"() {
3094
3477
  init_bundler();
3095
3478
  init_watcher();
3479
+ init_env();
3096
3480
  init_ploy_config2();
3097
3481
  init_workerd_config();
3098
3482
  init_mock_server();
@@ -3110,6 +3494,7 @@ var init_emulator = __esm({
3110
3494
  workerdProcess = null;
3111
3495
  fileWatcher = null;
3112
3496
  queueProcessor = null;
3497
+ resolvedEnvVars = {};
3113
3498
  constructor(options = {}) {
3114
3499
  const port = options.port ?? 8787;
3115
3500
  this.options = {
@@ -3127,6 +3512,10 @@ var init_emulator = __esm({
3127
3512
  try {
3128
3513
  this.config = readPloyConfig2(this.projectDir, this.options.configPath);
3129
3514
  debug(`Loaded config: ${JSON.stringify(this.config)}`, this.options.verbose);
3515
+ this.resolvedEnvVars = resolveEnvVars2(this.projectDir, this.config);
3516
+ if (Object.keys(this.resolvedEnvVars).length > 0) {
3517
+ debug(`Resolved env vars: ${Object.keys(this.resolvedEnvVars).join(", ")}`, this.options.verbose);
3518
+ }
3130
3519
  this.tempDir = ensureTempDir(this.projectDir);
3131
3520
  ensureDataDir(this.projectDir);
3132
3521
  debug(`Temp dir: ${this.tempDir}`, this.options.verbose);
@@ -3174,6 +3563,9 @@ var init_emulator = __esm({
3174
3563
  }
3175
3564
  success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
3176
3565
  log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
3566
+ if (Object.keys(this.resolvedEnvVars).length > 0) {
3567
+ log(` Env vars: ${Object.keys(this.resolvedEnvVars).join(", ")}`);
3568
+ }
3177
3569
  if (this.config.db) {
3178
3570
  log(` DB bindings: ${Object.keys(this.config.db).join(", ")}`);
3179
3571
  }
@@ -3183,6 +3575,9 @@ var init_emulator = __esm({
3183
3575
  if (this.config.cache) {
3184
3576
  log(` Cache bindings: ${Object.keys(this.config.cache).join(", ")}`);
3185
3577
  }
3578
+ if (this.config.fs) {
3579
+ log(` File Storage bindings: ${Object.keys(this.config.fs).join(", ")}`);
3580
+ }
3186
3581
  if (this.config.workflow) {
3187
3582
  log(` Workflow bindings: ${Object.keys(this.config.workflow).join(", ")}`);
3188
3583
  }
@@ -3202,7 +3597,8 @@ var init_emulator = __esm({
3202
3597
  tempDir: this.tempDir,
3203
3598
  entryPoint,
3204
3599
  config: this.config,
3205
- mockServiceUrl
3600
+ mockServiceUrl,
3601
+ envVars: this.resolvedEnvVars
3206
3602
  });
3207
3603
  }
3208
3604
  async startWorkerd(configPath) {
@@ -3228,9 +3624,7 @@ var init_emulator = __esm({
3228
3624
  let stderrOutput = "";
3229
3625
  this.workerdProcess.stdout?.on("data", (data) => {
3230
3626
  const output = data.toString();
3231
- if (this.options.verbose) {
3232
- process.stdout.write(output);
3233
- }
3627
+ log(`[workerd stdout] ${output.trim()}`);
3234
3628
  if (!started && (output.includes("Listening") || output.includes("running"))) {
3235
3629
  started = true;
3236
3630
  resolve(void 0);
@@ -5166,6 +5560,12 @@ async function updateTsConfigInclude(cwd, outputPath) {
5166
5560
  function generateEnvType(config) {
5167
5561
  const imports = [];
5168
5562
  const properties = [];
5563
+ if (config.env && Object.keys(config.env).length > 0) {
5564
+ const varProps = Object.keys(config.env).map((key) => ` ${key}: string;`).join("\n");
5565
+ properties.push(` vars: {
5566
+ ${varProps}
5567
+ };`);
5568
+ }
5169
5569
  if (config.ai) {
5170
5570
  properties.push(" AI_URL: string;");
5171
5571
  properties.push(" AI_TOKEN: string;");
@@ -5241,7 +5641,7 @@ async function typesCommand(options = {}) {
5241
5641
  console.error("Error: ploy.yaml not found in current directory");
5242
5642
  process.exit(1);
5243
5643
  }
5244
- const hasBindings2 = config.ai || config.db || config.queue || config.cache || config.state || config.workflow;
5644
+ const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow;
5245
5645
  if (!hasBindings2) {
5246
5646
  console.log("No bindings found in ploy.yaml. Generating empty Env.");
5247
5647
  }