mpb-localkit 1.3.7 → 1.4.2
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/cli/index.js +303 -92
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.js +129 -3
- package/dist/core/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
5
|
-
import { resolve, join } from 'path';
|
|
6
|
-
import {
|
|
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,23 +22,6 @@ 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) {
|
|
@@ -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"
|
|
@@ -461,7 +445,7 @@ class_name = "WsSessions"
|
|
|
461
445
|
|
|
462
446
|
[[migrations]]
|
|
463
447
|
tag = "v1"
|
|
464
|
-
|
|
448
|
+
new_sqlite_classes = ["WsSessions"]
|
|
465
449
|
|
|
466
450
|
# Security: Set secrets via Cloudflare dashboard or CLI:
|
|
467
451
|
# wrangler secret put JWT_SECRET
|
|
@@ -470,7 +454,7 @@ new_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
|
|
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 = {
|
|
@@ -700,19 +685,6 @@ function registerBuild(program2) {
|
|
|
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,66 +701,305 @@ 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", {
|
|
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
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (
|
|
749
|
-
log.
|
|
750
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
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,
|