@meetploy/cli 1.15.0 → 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';
@@ -200,6 +200,7 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
200
200
  validateBindings(config.queue, "queue", configFile);
201
201
  validateBindings(config.cache, "cache", configFile);
202
202
  validateBindings(config.state, "state", configFile);
203
+ validateBindings(config.fs, "fs", configFile);
203
204
  validateBindings(config.workflow, "workflow", configFile);
204
205
  if (config.ai !== void 0 && typeof config.ai !== "boolean") {
205
206
  throw new Error(`'ai' in ${configFile} must be a boolean`);
@@ -276,7 +277,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
276
277
  return validatePloyConfig(config, configFile, validationOptions);
277
278
  }
278
279
  function hasBindings(config) {
279
- return !!(config.env ?? 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);
280
281
  }
281
282
  function parseDotEnv(content) {
282
283
  const result = {};
@@ -757,6 +758,124 @@ var init_db_runtime = __esm({
757
758
  }
758
759
  });
759
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
+
760
879
  // ../emulator/dist/runtime/queue-runtime.js
761
880
  var QUEUE_RUNTIME_CODE;
762
881
  var init_queue_runtime = __esm({
@@ -1121,6 +1240,12 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
1121
1240
  bindings.push(` ${bindingName}: initializeState("${stateName}", "${mockServiceUrl}"),`);
1122
1241
  }
1123
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
+ }
1124
1249
  if (config.workflow) {
1125
1250
  imports.push('import { initializeWorkflow, createStepContext, executeWorkflow } from "__ploy_workflow_runtime__";');
1126
1251
  for (const [bindingName, workflowName] of Object.entries(config.workflow)) {
@@ -1237,6 +1362,10 @@ function createRuntimePlugin(_config) {
1237
1362
  path: "__ploy_state_runtime__",
1238
1363
  namespace: "ploy-runtime"
1239
1364
  }));
1365
+ build2.onResolve({ filter: /^__ploy_fs_runtime__$/ }, () => ({
1366
+ path: "__ploy_fs_runtime__",
1367
+ namespace: "ploy-runtime"
1368
+ }));
1240
1369
  build2.onResolve({ filter: /^__ploy_workflow_runtime__$/ }, () => ({
1241
1370
  path: "__ploy_workflow_runtime__",
1242
1371
  namespace: "ploy-runtime"
@@ -1257,6 +1386,10 @@ function createRuntimePlugin(_config) {
1257
1386
  contents: STATE_RUNTIME_CODE,
1258
1387
  loader: "ts"
1259
1388
  }));
1389
+ build2.onLoad({ filter: /^__ploy_fs_runtime__$/, namespace: "ploy-runtime" }, () => ({
1390
+ contents: FS_RUNTIME_CODE,
1391
+ loader: "ts"
1392
+ }));
1260
1393
  build2.onLoad({ filter: /^__ploy_workflow_runtime__$/, namespace: "ploy-runtime" }, () => ({
1261
1394
  contents: WORKFLOW_RUNTIME_CODE,
1262
1395
  loader: "ts"
@@ -1299,6 +1432,7 @@ var init_bundler = __esm({
1299
1432
  "../emulator/dist/bundler/bundler.js"() {
1300
1433
  init_cache_runtime();
1301
1434
  init_db_runtime();
1435
+ init_fs_runtime();
1302
1436
  init_queue_runtime();
1303
1437
  init_state_runtime();
1304
1438
  init_workflow_runtime();
@@ -1559,6 +1693,34 @@ var init_workerd_config = __esm({
1559
1693
  "../emulator/dist/config/workerd-config.js"() {
1560
1694
  }
1561
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
+ });
1562
1724
  function generateId() {
1563
1725
  return randomBytes(16).toString("hex");
1564
1726
  }
@@ -1871,6 +2033,7 @@ function createDashboardRoutes(app, dbManager2, config) {
1871
2033
  queue: config.queue,
1872
2034
  cache: config.cache,
1873
2035
  state: config.state,
2036
+ fs: config.fs,
1874
2037
  workflow: config.workflow,
1875
2038
  auth: config.auth
1876
2039
  });
@@ -2365,6 +2528,37 @@ function createDashboardRoutes(app, dbManager2, config) {
2365
2528
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2366
2529
  }
2367
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
+ });
2368
2562
  if (hasDashboard) {
2369
2563
  app.get("/assets/*", (c) => {
2370
2564
  const path = c.req.path;
@@ -2515,6 +2709,125 @@ var init_db_service = __esm({
2515
2709
  "../emulator/dist/services/db-service.js"() {
2516
2710
  }
2517
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
+ });
2518
2831
  function createQueueHandlers(db) {
2519
2832
  const sendHandler = async (c) => {
2520
2833
  try {
@@ -2948,6 +3261,14 @@ async function startMockServer(dbManager2, config, options = {}) {
2948
3261
  app.post("/state/set", stateHandlers.setHandler);
2949
3262
  app.post("/state/delete", stateHandlers.deleteHandler);
2950
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
+ }
2951
3272
  if (config.auth) {
2952
3273
  const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
2953
3274
  app.post("/auth/signup", authHandlers.signupHandler);
@@ -2979,44 +3300,18 @@ async function startMockServer(dbManager2, config, options = {}) {
2979
3300
  var DEFAULT_MOCK_SERVER_PORT;
2980
3301
  var init_mock_server = __esm({
2981
3302
  "../emulator/dist/services/mock-server.js"() {
3303
+ init_paths();
2982
3304
  init_auth_service();
2983
3305
  init_cache_service();
2984
3306
  init_dashboard_routes();
2985
3307
  init_db_service();
3308
+ init_fs_service();
2986
3309
  init_queue_service();
2987
3310
  init_state_service();
2988
3311
  init_workflow_service();
2989
3312
  DEFAULT_MOCK_SERVER_PORT = 4003;
2990
3313
  }
2991
3314
  });
2992
- function getProjectHash(projectDir) {
2993
- return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
2994
- }
2995
- function getTempDir(projectDir) {
2996
- const hash = getProjectHash(projectDir);
2997
- return join(tmpdir(), `ploy-emulator-${hash}`);
2998
- }
2999
- function getDataDir(projectDir) {
3000
- return join(projectDir, ".ploy");
3001
- }
3002
- function ensureDir(dir) {
3003
- mkdirSync(dir, { recursive: true });
3004
- }
3005
- function ensureTempDir(projectDir) {
3006
- const tempDir = getTempDir(projectDir);
3007
- ensureDir(tempDir);
3008
- return tempDir;
3009
- }
3010
- function ensureDataDir(projectDir) {
3011
- const dataDir = getDataDir(projectDir);
3012
- ensureDir(dataDir);
3013
- ensureDir(join(dataDir, "db"));
3014
- return dataDir;
3015
- }
3016
- var init_paths = __esm({
3017
- "../emulator/dist/utils/paths.js"() {
3018
- }
3019
- });
3020
3315
  function initializeDatabases(projectDir) {
3021
3316
  const dataDir = ensureDataDir(projectDir);
3022
3317
  const d1Databases = /* @__PURE__ */ new Map();
@@ -3158,6 +3453,16 @@ CREATE TABLE IF NOT EXISTS state_entries (
3158
3453
  value TEXT NOT NULL,
3159
3454
  PRIMARY KEY (state_name, key)
3160
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
+ );
3161
3466
  `;
3162
3467
  }
3163
3468
  });
@@ -3270,6 +3575,9 @@ var init_emulator = __esm({
3270
3575
  if (this.config.cache) {
3271
3576
  log(` Cache bindings: ${Object.keys(this.config.cache).join(", ")}`);
3272
3577
  }
3578
+ if (this.config.fs) {
3579
+ log(` File Storage bindings: ${Object.keys(this.config.fs).join(", ")}`);
3580
+ }
3273
3581
  if (this.config.workflow) {
3274
3582
  log(` Workflow bindings: ${Object.keys(this.config.workflow).join(", ")}`);
3275
3583
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetploy/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",