mpb-localkit 1.3.5 → 1.4.1

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/README.md CHANGED
@@ -108,7 +108,7 @@ Passwords are hashed client-side (PBKDF2) before transmission. Sessions are JWTs
108
108
 
109
109
  ## Sync
110
110
 
111
- Sync happens automatically when configured with an `endpoint`. MPB LocalKit uses a Last-Write-Wins (LWW) protocol — the document with the highest `_updatedAt` timestamp wins conflicts.
111
+ Sync happens automatically when configured with an `endpoint`. LocalKit uses a Last-Write-Wins (LWW) protocol — the document with the highest `_updatedAt` timestamp wins conflicts.
112
112
 
113
113
  ```ts
114
114
  // Manual sync
@@ -226,7 +226,7 @@ Done! Your project is ready.
226
226
 
227
227
  ## Transport Configuration
228
228
 
229
- MPB LocalKit supports three sync transports, configurable at `createApp` time:
229
+ LocalKit supports three sync transports, configurable at `createApp` time:
230
230
 
231
231
  ### HTTP Transport (default)
232
232
 
@@ -271,11 +271,11 @@ const app = createApp({
271
271
  })
272
272
  ```
273
273
 
274
- With `auto`, MPB LocalKit upgrades to WebSocket when the server supports it and degrades gracefully on restricted networks.
274
+ With `auto`, LocalKit upgrades to WebSocket when the server supports it and degrades gracefully on restricted networks.
275
275
 
276
276
  ## WebSocket Sync
277
277
 
278
- When using `transport: 'websocket'` or `transport: 'auto'`, MPB LocalKit maintains a persistent WebSocket connection to your sync Worker. The Worker sends change events as they occur — no polling delay.
278
+ When using `transport: 'websocket'` or `transport: 'auto'`, LocalKit maintains a persistent WebSocket connection to your sync Worker. The Worker sends change events as they occur — no polling delay.
279
279
 
280
280
  ```ts
281
281
  import { createApp, collection, z } from 'mpb-localkit'
@@ -307,7 +307,7 @@ The WebSocket connection is managed automatically:
307
307
 
308
308
  ## Better Auth Integration
309
309
 
310
- MPB LocalKit integrates with [Better Auth](https://better-auth.com) for full-featured server-side authentication.
310
+ LocalKit integrates with [Better Auth](https://better-auth.com) for full-featured server-side authentication.
311
311
 
312
312
  ### Setup
313
313
 
package/dist/cli/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { createRequire } from 'module';
4
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
5
- import { resolve, join } from 'path';
6
- import { spawnSync, execSync } from 'child_process';
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { resolve, join, dirname } from 'path';
6
+ import { execSync, spawnSync } from 'child_process';
7
+ import { randomBytes } from 'crypto';
7
8
 
8
9
  var colors = {
9
10
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -21,29 +22,12 @@ var log = {
21
22
  bold: (msg) => console.log(colors.bold(msg)),
22
23
  dim: (msg) => console.log(colors.dim(msg))
23
24
  };
24
- var CONFIG_CANDIDATES = [
25
- "offlinekit.config.ts",
26
- "offlinekit.config.js",
27
- "offlinekit.schema.ts",
28
- "offlinekit.schema.js"
29
- ];
30
- function findSchemaFile(cwd = process.cwd()) {
31
- for (const candidate of CONFIG_CANDIDATES) {
32
- const p = resolve(cwd, candidate);
33
- if (existsSync(p)) return p;
34
- }
35
- return null;
36
- }
37
- async function loadSchema(filePath) {
38
- const mod = await import(filePath);
39
- return mod.default ?? mod;
40
- }
41
25
 
42
26
  // src/cli/commands/dev.ts
43
27
  function registerDev(program2) {
44
- program2.command("dev").description("Start OfflineKit in local-only mode (no cloud sync)").action(() => {
28
+ program2.command("dev").description("Start LocalKit in local-only mode (no cloud sync)").action(() => {
45
29
  process.env["OFFLINEKIT_MODE"] = "local";
46
- log.bold("OfflineKit Dev Mode");
30
+ log.bold("LocalKit Dev Mode");
47
31
  log.info("Running in local-only mode (no cloud sync)");
48
32
  log.dim("All data is stored locally. Sync is disabled.");
49
33
  });
@@ -114,7 +98,7 @@ export async function authMiddleware(c: any, next: () => Promise<void>) {
114
98
  const token = header.startsWith('Bearer ') ? header.slice(7) : null
115
99
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
116
100
  try {
117
- const payload = await verify(token, c.env.JWT_SECRET) as { sub: string }
101
+ const payload = await verify(token, c.env.JWT_SECRET, 'HS256') as { sub: string }
118
102
  c.set('userId', payload.sub)
119
103
  } catch {
120
104
  return c.json({ error: 'Invalid token' }, 401)
@@ -133,7 +117,7 @@ auth.post('/signup', async (c) => {
133
117
  const user: StoredUser = { userId, email, passwordHash }
134
118
  await c.env.KV.put(AUTH_KEY(email), JSON.stringify(user))
135
119
 
136
- const token = await sign({ sub: userId, email, exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 }, c.env.JWT_SECRET)
120
+ const token = await sign({ sub: userId, email, exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 }, c.env.JWT_SECRET, 'HS256')
137
121
  return c.json({ user: { id: userId, email }, token }, 201)
138
122
  })
139
123
 
@@ -147,7 +131,7 @@ auth.post('/signin', async (c) => {
147
131
  const user = JSON.parse(raw) as StoredUser
148
132
  if (!timingSafeEqual(user.passwordHash, passwordHash)) return c.json({ error: 'Invalid credentials' }, 401)
149
133
 
150
- const token = await sign({ sub: user.userId, email, exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 }, c.env.JWT_SECRET)
134
+ const token = await sign({ sub: user.userId, email, exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 }, c.env.JWT_SECRET, 'HS256')
151
135
  return c.json({ user: { id: user.userId, email }, token })
152
136
  })
153
137
 
@@ -299,7 +283,7 @@ export class WsSessions {
299
283
  private async handle(ws: WebSocket, session: { userId: string | null }, msg: WsMsg): Promise<void> {
300
284
  if (msg.type === 'auth') {
301
285
  try {
302
- const payload = await verify(msg.token ?? '', this.env.JWT_SECRET) as { sub?: string }
286
+ const payload = await verify(msg.token ?? '', this.env.JWT_SECRET, 'HS256') as { sub?: string }
303
287
  if (!payload.sub) throw new Error('Missing sub')
304
288
  session.userId = payload.sub
305
289
  ws.send(JSON.stringify({ type: 'auth_ack', id: msg.id }))
@@ -440,7 +424,7 @@ export interface StoredSession {
440
424
  }
441
425
 
442
426
  // src/cli/generator/templates/wrangler.ts
443
- function wranglerTemplate(appName) {
427
+ function wranglerTemplate(appName, kvNamespaceId) {
444
428
  const name = appName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
445
429
  return `name = "${name}-worker"
446
430
  main = "src/index.ts"
@@ -453,7 +437,7 @@ bucket_name = "${name}-storage"
453
437
 
454
438
  [[kv_namespaces]]
455
439
  binding = "KV"
456
- id = "REPLACE_WITH_KV_NAMESPACE_ID"
440
+ id = "${kvNamespaceId ?? "REPLACE_WITH_KV_NAMESPACE_ID"}"
457
441
 
458
442
  [[durable_objects.bindings]]
459
443
  name = "WS_SESSIONS"
@@ -470,7 +454,7 @@ new_sqlite_classes = ["WsSessions"]
470
454
 
471
455
  // src/cli/generator/index.ts
472
456
  function generateWorker(options) {
473
- const { appName, outDir } = options;
457
+ const { appName, outDir, kvNamespaceId } = options;
474
458
  const dirs = [
475
459
  outDir,
476
460
  join(outDir, "src"),
@@ -479,7 +463,7 @@ function generateWorker(options) {
479
463
  ];
480
464
  for (const dir of dirs) mkdirSync(dir, { recursive: true });
481
465
  const files = [
482
- [join(outDir, "wrangler.toml"), wranglerTemplate(appName)],
466
+ [join(outDir, "wrangler.toml"), wranglerTemplate(appName, kvNamespaceId)],
483
467
  [join(outDir, "src", "index.ts"), workerIndexTemplate()],
484
468
  [join(outDir, "src", "routes", "auth.ts"), authTemplate()],
485
469
  [join(outDir, "src", "routes", "sync.ts"), syncTemplate()],
@@ -494,8 +478,9 @@ function generateWorker(options) {
494
478
  for (const [path, content] of files) {
495
479
  writeFileSync(path, content, "utf8");
496
480
  }
481
+ const kvWarning = kvNamespaceId ? "" : " - KV namespace id \u2192 set your actual KV namespace ID\n";
497
482
  console.warn(
498
- '\n\u26A0 Remember to replace placeholder values in wrangler.toml before deploying:\n - JWT_SECRET = "REPLACE_WITH_SECRET" \u2192 set a strong random secret\n - KV namespace id \u2192 set your actual KV namespace ID\n'
483
+ '\n\u26A0 Remember to replace placeholder values in wrangler.toml before deploying:\n - JWT_SECRET = "REPLACE_WITH_SECRET" \u2192 set a strong random secret\n' + kvWarning
499
484
  );
500
485
  }
501
486
  function workerPackageJson(appName) {
@@ -545,8 +530,8 @@ function workerTsConfig() {
545
530
  // src/cli/targets/cloudflare.ts
546
531
  var cloudflareTarget = {
547
532
  name: "cloudflare",
548
- generate({ appName, outDir }) {
549
- generateWorker({ appName, outDir });
533
+ generate({ appName, outDir, kvNamespaceId }) {
534
+ generateWorker({ appName, outDir, kvNamespaceId });
550
535
  }
551
536
  };
552
537
  var nodeTarget = {
@@ -693,26 +678,13 @@ function nodeTsConfig() {
693
678
  // src/cli/commands/build.ts
694
679
  var targets = { cloudflare: cloudflareTarget, node: nodeTarget };
695
680
  function registerBuild(program2) {
696
- program2.command("build").description("Build the OfflineKit backend bundle").option("-o, --out <dir>", "Output directory", ".offlinekit/worker").option("-n, --name <name>", "App name for the worker", "offlinekit-app").option("-t, --target <target>", "Deploy target: cloudflare or node", "cloudflare").action(async (opts) => {
697
- log.bold("OfflineKit Build");
681
+ program2.command("build").description("Build the LocalKit backend bundle").option("-o, --out <dir>", "Output directory", ".offlinekit/worker").option("-n, --name <name>", "App name for the worker", "localkit-app").option("-t, --target <target>", "Deploy target: cloudflare or node", "cloudflare").action(async (opts) => {
682
+ log.bold("LocalKit Build");
698
683
  const target = targets[opts.target];
699
684
  if (!target) {
700
685
  log.error(`Unknown target: ${opts.target}. Valid targets: ${Object.keys(targets).join(", ")}`);
701
686
  process.exit(1);
702
687
  }
703
- const schemaFile = findSchemaFile();
704
- if (!schemaFile) {
705
- log.error("No schema file found. Create offlinekit.config.ts in your project root.");
706
- process.exit(1);
707
- }
708
- log.info(`Found schema: ${schemaFile}`);
709
- try {
710
- await loadSchema(schemaFile);
711
- log.success("Schema loaded successfully");
712
- } catch (err) {
713
- log.error(`Schema load failed: ${err instanceof Error ? err.message : String(err)}`);
714
- process.exit(1);
715
- }
716
688
  const outDir = resolve(process.cwd(), opts.out);
717
689
  log.info(`Generating ${target.name} backend to: ${outDir}`);
718
690
  try {
@@ -729,74 +701,313 @@ function registerBuild(program2) {
729
701
  }
730
702
  });
731
703
  }
704
+ function runWrangler(args, options) {
705
+ const result = spawnSync("wrangler", args, {
706
+ encoding: "utf8",
707
+ stdio: ["pipe", "pipe", "pipe"],
708
+ ...options
709
+ });
710
+ return {
711
+ stdout: result.stdout ?? "",
712
+ stderr: result.stderr ?? "",
713
+ status: result.status
714
+ };
715
+ }
732
716
  function checkWrangler() {
733
717
  try {
734
- const result = execSync("wrangler --version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
718
+ const result = execSync("wrangler --version", {
719
+ encoding: "utf8",
720
+ stdio: ["ignore", "pipe", "ignore"]
721
+ });
735
722
  return result.trim();
736
723
  } catch {
737
724
  return null;
738
725
  }
739
726
  }
740
- function registerDeploy(program2) {
741
- program2.command("deploy").description("Deploy the OfflineKit Worker to Cloudflare via Wrangler").option("-d, --dir <dir>", "Worker output directory", ".offlinekit/worker").option("-e, --env <env>", "Wrangler environment (e.g. production, staging)").option("-t, --target <target>", "Deploy target (default: cloudflare)", "cloudflare").option("--dry-run", "Print the wrangler command without running it").action((opts) => {
742
- log.bold("OfflineKit Deploy");
743
- const serverDir = resolve(process.cwd(), "server");
744
- if (existsSync(serverDir)) {
745
- log.info("Warning: This project has been ejected (./server/ exists).");
746
- log.dim("You can deploy the ejected code directly with `cd server && wrangler deploy`.");
747
- }
748
- if (opts.target !== "cloudflare") {
749
- log.error(`Deploy target "${opts.target}" is not supported yet. Only "cloudflare" is deployable.`);
750
- log.dim("For Node.js targets, run `offlinekit build --target node` then deploy the output manually.");
751
- process.exit(1);
752
- }
753
- const version = checkWrangler();
754
- if (!version) {
755
- log.error("wrangler is not installed or not in PATH.");
756
- log.dim("Install it with: npm install -g wrangler");
757
- process.exit(1);
727
+ function checkAuth() {
728
+ const result = runWrangler(["whoami"]);
729
+ return result.status === 0;
730
+ }
731
+ function createR2Bucket(name) {
732
+ log.info(`Creating R2 bucket: ${name}`);
733
+ const result = runWrangler(["r2", "bucket", "create", name]);
734
+ if (result.status !== 0) {
735
+ if (result.stderr.includes("already exists")) {
736
+ log.info(`R2 bucket "${name}" already exists, continuing`);
737
+ return;
758
738
  }
759
- log.success(`wrangler ${version}`);
760
- const outDir = resolve(process.cwd(), opts.dir);
761
- if (!existsSync(outDir)) {
762
- log.error(`Worker directory not found: ${outDir}`);
763
- log.dim("Run `offlinekit build` first to generate the worker.");
764
- process.exit(1);
739
+ if (result.stderr.includes("R2") || result.stderr.includes("not allowed") || result.stderr.includes("not enabled")) {
740
+ throw new Error(
741
+ `Failed to create R2 bucket: ${result.stderr}
742
+ Hint: Enable R2 in your Cloudflare dashboard at https://dash.cloudflare.com \u2192 R2 Object Storage before deploying.`
743
+ );
765
744
  }
766
- const tomlPath = resolve(outDir, "wrangler.toml");
767
- if (!existsSync(tomlPath)) {
768
- log.error(`wrangler.toml not found in ${outDir}`);
769
- log.dim("Run `offlinekit build` to regenerate the worker.");
770
- process.exit(1);
745
+ throw new Error(`Failed to create R2 bucket: ${result.stderr}`);
746
+ }
747
+ log.success(`R2 bucket "${name}" created`);
748
+ }
749
+ function kvNamespaceExists(id) {
750
+ const result = runWrangler(["kv", "namespace", "list", "--json"]);
751
+ if (result.status !== 0) return false;
752
+ try {
753
+ const namespaces = JSON.parse(result.stdout);
754
+ return namespaces.some((ns) => ns.id === id);
755
+ } catch {
756
+ return false;
757
+ }
758
+ }
759
+ function createKvNamespace(name) {
760
+ log.info(`Creating KV namespace: ${name}`);
761
+ const result = runWrangler(["kv", "namespace", "create", name]);
762
+ if (result.status === 0) {
763
+ const match = result.stdout.match(/id\s*=\s*"([a-f0-9]+)"/);
764
+ if (match) {
765
+ log.success(`KV namespace "${name}" created with ID: ${match[1]}`);
766
+ return match[1];
771
767
  }
772
- const args = ["deploy"];
773
- if (opts.env) args.push("--env", opts.env);
774
- log.info(`Deploying from: ${outDir}`);
775
- if (opts.env) log.info(`Environment: ${opts.env}`);
776
- if (opts.dryRun) {
777
- log.dim(`[dry-run] wrangler ${args.join(" ")}`);
778
- log.dim(`[dry-run] cwd: ${outDir}`);
779
- return;
768
+ try {
769
+ const parsed = JSON.parse(result.stdout);
770
+ if (parsed.id) return parsed.id;
771
+ } catch {
780
772
  }
781
- const result = spawnSync("wrangler", args, {
782
- cwd: outDir,
783
- stdio: "inherit",
784
- encoding: "utf8"
785
- });
786
- if (result.status !== 0) {
787
- log.error(`wrangler deploy failed (exit ${result.status ?? "unknown"})`);
788
- process.exit(result.status ?? 1);
773
+ }
774
+ if (result.stderr.includes("already exists") || result.status !== 0) {
775
+ log.info(`KV namespace "${name}" may already exist, looking up ID...`);
776
+ const listResult = runWrangler(["kv", "namespace", "list", "--json"]);
777
+ if (listResult.status === 0) {
778
+ try {
779
+ const namespaces = JSON.parse(listResult.stdout);
780
+ const found = namespaces.find((ns) => ns.title === name);
781
+ if (found) {
782
+ log.success(`Found existing KV namespace "${name}" with ID: ${found.id}`);
783
+ return found.id;
784
+ }
785
+ } catch {
786
+ log.warn("Failed to parse KV namespace list JSON, falling back to regex");
787
+ }
788
+ const idMatch = listResult.stdout.match(/id\s*=\s*"([a-f0-9]+)"/);
789
+ if (idMatch) return idMatch[1];
789
790
  }
790
- log.success("Deployed successfully!");
791
+ }
792
+ throw new Error(
793
+ `Failed to create or find KV namespace "${name}". stdout: ${result.stdout}, stderr: ${result.stderr}`
794
+ );
795
+ }
796
+ function setSecret(name, value, cwd) {
797
+ log.info(`Setting secret: ${name}`);
798
+ const result = runWrangler(["secret", "put", name], { cwd, input: value });
799
+ if (result.status !== 0) {
800
+ throw new Error(`Failed to set secret ${name}: ${result.stderr}`);
801
+ }
802
+ log.success(`Secret "${name}" set`);
803
+ }
804
+ function readDeployState(outDir) {
805
+ const statePath = resolve(outDir, "..", "deploy-state.json");
806
+ if (!existsSync(statePath)) return null;
807
+ try {
808
+ return JSON.parse(readFileSync(statePath, "utf8"));
809
+ } catch {
810
+ return null;
811
+ }
812
+ }
813
+ function writeDeployState(outDir, state) {
814
+ const statePath = resolve(outDir, "..", "deploy-state.json");
815
+ mkdirSync(dirname(statePath), { recursive: true });
816
+ writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
817
+ }
818
+ function provisionCloudflare(appName, _outDir, dryRun) {
819
+ const name = appName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
820
+ const r2BucketName = `${name}-storage`;
821
+ const kvTitle = `${name}-kv`;
822
+ const workerName = `${name}-worker`;
823
+ if (dryRun) {
824
+ log.dim("[dry-run] Would check wrangler auth (wrangler whoami)");
825
+ log.dim(`[dry-run] Would create R2 bucket: ${r2BucketName}`);
826
+ log.dim(`[dry-run] Would create KV namespace: ${kvTitle}`);
827
+ log.dim("[dry-run] Would set JWT_SECRET");
828
+ return { kvNamespaceId: "DRY_RUN_PLACEHOLDER", r2BucketName, workerName };
829
+ }
830
+ if (!checkAuth()) {
831
+ log.error("Not authenticated with Cloudflare.");
832
+ log.dim("Run `wrangler login` to authenticate, then try again.");
833
+ process.exit(1);
834
+ }
835
+ log.success("Authenticated with Cloudflare");
836
+ createR2Bucket(r2BucketName);
837
+ const kvNamespaceId = createKvNamespace(kvTitle);
838
+ return { kvNamespaceId, r2BucketName, workerName };
839
+ }
840
+ function generateAndDeploy(appName, outDir, kvNamespaceId, env, dryRun) {
841
+ if (dryRun) {
842
+ log.dim(`[dry-run] Would generate worker code to: ${outDir}`);
843
+ log.dim(`[dry-run] Would run: npm install (in ${outDir})`);
844
+ log.dim(`[dry-run] Would run: wrangler deploy${env ? ` --env ${env}` : ""}`);
845
+ return void 0;
846
+ }
847
+ log.info("Generating worker code...");
848
+ cloudflareTarget.generate({ appName, outDir, kvNamespaceId });
849
+ log.success("Worker code generated");
850
+ log.info("Installing dependencies...");
851
+ const installResult = spawnSync("npm", ["install"], {
852
+ cwd: outDir,
853
+ encoding: "utf8",
854
+ stdio: ["pipe", "pipe", "pipe"]
855
+ });
856
+ if (installResult.status !== 0) {
857
+ throw new Error(`npm install failed: ${installResult.stderr}`);
858
+ }
859
+ log.success("Dependencies installed");
860
+ log.info("Deploying to Cloudflare...");
861
+ const deployArgs = ["deploy"];
862
+ if (env) deployArgs.push("--env", env);
863
+ const deployResult = spawnSync("wrangler", deployArgs, {
864
+ cwd: outDir,
865
+ encoding: "utf8",
866
+ stdio: ["pipe", "pipe", "pipe"]
791
867
  });
868
+ if (deployResult.status !== 0) {
869
+ throw new Error(
870
+ `wrangler deploy failed (exit ${deployResult.status}): ${deployResult.stderr}`
871
+ );
872
+ }
873
+ const urlMatch = deployResult.stdout.match(/https:\/\/[^\s]+\.workers\.dev/);
874
+ const workerUrl = urlMatch ? urlMatch[0] : void 0;
875
+ if (workerUrl) {
876
+ log.success(`Deployed to: ${workerUrl}`);
877
+ } else {
878
+ log.success("Deployed successfully!");
879
+ }
880
+ return workerUrl;
881
+ }
882
+ function registerDeploy(program2) {
883
+ program2.command("deploy").description("Deploy the LocalKit Worker to Cloudflare via Wrangler").option("-d, --dir <dir>", "Worker output directory", ".offlinekit/worker").option("-n, --name <name>", "App name for the worker", "localkit-app").option("-e, --env <env>", "Wrangler environment (e.g. production, staging)").option("-t, --target <target>", "Deploy target (default: cloudflare)", "cloudflare").option("--dry-run", "Show what would happen without executing").action(
884
+ (opts) => {
885
+ log.bold("LocalKit Deploy");
886
+ const dryRun = opts.dryRun ?? false;
887
+ const serverDir = resolve(process.cwd(), "server");
888
+ if (existsSync(serverDir)) {
889
+ log.info(
890
+ "Warning: This project has been ejected (./server/ exists)."
891
+ );
892
+ log.dim(
893
+ "You can deploy the ejected code directly with `cd server && wrangler deploy`."
894
+ );
895
+ }
896
+ if (opts.target !== "cloudflare") {
897
+ log.error(
898
+ `Deploy target "${opts.target}" is not supported yet. Only "cloudflare" is deployable.`
899
+ );
900
+ log.dim(
901
+ "For Node.js targets, run `npx mpb-localkit build --target node` then deploy the output manually."
902
+ );
903
+ process.exit(1);
904
+ }
905
+ const version = checkWrangler();
906
+ if (!version) {
907
+ log.error("wrangler is not installed or not in PATH.");
908
+ log.dim("Install it with: npm install -g wrangler");
909
+ process.exit(1);
910
+ }
911
+ log.success(`wrangler ${version}`);
912
+ const outDir = resolve(process.cwd(), opts.dir);
913
+ const appName = opts.name;
914
+ const existingState = readDeployState(outDir);
915
+ let kvNamespaceId;
916
+ let r2BucketName;
917
+ let workerName;
918
+ if (existingState?.kvNamespaceId && existingState?.r2BucketName) {
919
+ log.info("Found existing deploy state, validating resources...");
920
+ r2BucketName = existingState.r2BucketName;
921
+ workerName = existingState.workerName;
922
+ if (dryRun) {
923
+ log.dim("[dry-run] Would validate KV namespace exists");
924
+ kvNamespaceId = existingState.kvNamespaceId;
925
+ } else if (kvNamespaceExists(existingState.kvNamespaceId)) {
926
+ log.success("KV namespace validated");
927
+ kvNamespaceId = existingState.kvNamespaceId;
928
+ } else {
929
+ log.warn("KV namespace no longer exists, re-creating...");
930
+ const name = appName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
931
+ kvNamespaceId = createKvNamespace(`${name}-kv`);
932
+ writeDeployState(outDir, {
933
+ ...existingState,
934
+ kvNamespaceId
935
+ });
936
+ log.success("Deploy state updated with new KV namespace ID");
937
+ }
938
+ } else {
939
+ const provisioned = provisionCloudflare(appName, outDir, dryRun);
940
+ kvNamespaceId = provisioned.kvNamespaceId;
941
+ r2BucketName = provisioned.r2BucketName;
942
+ workerName = provisioned.workerName;
943
+ if (!dryRun) {
944
+ writeDeployState(outDir, {
945
+ kvNamespaceId,
946
+ r2BucketName,
947
+ workerName,
948
+ jwtSecretSet: false
949
+ });
950
+ log.success("Deploy state saved to .offlinekit/deploy-state.json");
951
+ }
952
+ }
953
+ if (kvNamespaceId === "REPLACE_WITH_KV_NAMESPACE_ID" || !kvNamespaceId) {
954
+ log.error(
955
+ "KV namespace ID is missing or still a placeholder. Run `mpb-localkit deploy` (not `wrangler deploy` directly) to auto-provision resources, or create one manually: wrangler kv namespace create KV"
956
+ );
957
+ process.exit(1);
958
+ }
959
+ const alreadyHasSecret = existingState?.jwtSecretSet ?? false;
960
+ let workerUrl;
961
+ try {
962
+ workerUrl = generateAndDeploy(appName, outDir, kvNamespaceId, opts.env, dryRun);
963
+ } catch (err) {
964
+ log.error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
965
+ process.exit(1);
966
+ }
967
+ if (dryRun) {
968
+ if (!alreadyHasSecret) {
969
+ log.dim("[dry-run] Would set JWT_SECRET (first deploy only)");
970
+ }
971
+ log.bold("Dry run complete. No changes were made.");
972
+ return;
973
+ }
974
+ let jwtSecretSet = alreadyHasSecret;
975
+ if (!jwtSecretSet) {
976
+ try {
977
+ setSecret("JWT_SECRET", randomBytes(32).toString("hex"), outDir);
978
+ log.success("JWT_SECRET set");
979
+ jwtSecretSet = true;
980
+ } catch (err) {
981
+ log.warn(`Could not set JWT_SECRET: ${err instanceof Error ? err.message : String(err)}`);
982
+ log.dim("Set it manually: wrangler secret put JWT_SECRET");
983
+ }
984
+ }
985
+ writeDeployState(outDir, { kvNamespaceId, r2BucketName, workerName, workerUrl, jwtSecretSet });
986
+ if (workerUrl) {
987
+ const envPath = resolve(process.cwd(), ".env");
988
+ if (existsSync(envPath)) {
989
+ let envContent = readFileSync(envPath, "utf8");
990
+ if (envContent.includes("VITE_SYNC_URL=")) {
991
+ envContent = envContent.replace(/VITE_SYNC_URL=.*/, `VITE_SYNC_URL=${workerUrl}`);
992
+ } else {
993
+ envContent += `
994
+ VITE_SYNC_URL=${workerUrl}
995
+ `;
996
+ }
997
+ writeFileSync(envPath, envContent, "utf8");
998
+ log.info(`Updated .env with VITE_SYNC_URL=${workerUrl}`);
999
+ }
1000
+ }
1001
+ }
1002
+ );
792
1003
  }
793
1004
  var targets2 = {
794
1005
  cloudflare: cloudflareTarget,
795
1006
  node: nodeTarget
796
1007
  };
797
1008
  function registerEject(program2) {
798
- program2.command("eject").description("Eject generated Worker code into ./server/ for manual customization").option("-o, --out <dir>", "Output directory for ejected code", "server").option("-n, --name <name>", "App name", "offlinekit-app").option("-t, --target <target>", "Deploy target (cloudflare, node)", "cloudflare").action((opts) => {
799
- log.bold("OfflineKit Eject");
1009
+ program2.command("eject").description("Eject generated Worker code into ./server/ for manual customization").option("-o, --out <dir>", "Output directory for ejected code", "server").option("-n, --name <name>", "App name", "localkit-app").option("-t, --target <target>", "Deploy target (cloudflare, node)", "cloudflare").action((opts) => {
1010
+ log.bold("LocalKit Eject");
800
1011
  const target = targets2[opts.target];
801
1012
  if (!target) {
802
1013
  log.error(`Unknown target: "${opts.target}"`);
@@ -830,7 +1041,7 @@ function registerEject(program2) {
830
1041
  log.dim(" npm install");
831
1042
  log.dim(" wrangler deploy");
832
1043
  log.dim("");
833
- log.dim("Note: Running `offlinekit deploy` will warn that code has been ejected.");
1044
+ log.dim("Note: Running `npx mpb-localkit deploy` will warn that code has been ejected.");
834
1045
  }
835
1046
  } catch (err) {
836
1047
  log.error(`Eject failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -843,7 +1054,7 @@ function registerEject(program2) {
843
1054
  var require2 = createRequire(import.meta.url);
844
1055
  var pkg = require2("../../package.json");
845
1056
  var program = new Command();
846
- program.name("offlinekit").description(pkg.description).version(pkg.version, "-v, --version", "Print the current version");
1057
+ program.name("mpb-localkit").description(pkg.description).version(pkg.version, "-v, --version", "Print the current version");
847
1058
  registerDev(program);
848
1059
  registerBuild(program);
849
1060
  registerDeploy(program);