@porulle/cli 0.1.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/README.md +40 -0
- package/dist/commands/api-key.d.ts +34 -0
- package/dist/commands/api-key.d.ts.map +1 -0
- package/dist/commands/api-key.js +202 -0
- package/dist/commands/deploy.d.ts +13 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +45 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +173 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +413 -0
- package/dist/commands/generate-migration.d.ts +2 -0
- package/dist/commands/generate-migration.d.ts.map +1 -0
- package/dist/commands/generate-migration.js +20 -0
- package/dist/commands/import.d.ts +45 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +237 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +52 -0
- package/dist/commands/migrate.d.ts +2 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +17 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @porulle/cli
|
|
2
|
+
|
|
3
|
+
The command-line tool. Scaffolds new stores, runs migrations, mints API keys, doctors broken setups.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | What |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `porulle init <name>` | scaffold a new store from `templates/starter/` — `commerce.config.ts`, `drizzle.config.ts`, server entry, package.json wired to `@porulle/*` workspace deps |
|
|
10
|
+
| `porulle dev` | start the example server with reload (delegates to the app's own `dev` script when present) |
|
|
11
|
+
| `porulle migrate` | apply pending Drizzle migrations against `DATABASE_URL` |
|
|
12
|
+
| `porulle generate migration` | drizzle-kit generate — produce a new SQL migration from schema diffs |
|
|
13
|
+
| `porulle deploy` | thin wrapper around the app's deploy script |
|
|
14
|
+
| `porulle import <file>` | run an import adapter (Shopify CSV, WooCommerce XML, flat JSON) |
|
|
15
|
+
| `porulle api-key create --scope <scope>` | mint an API key with the given permission scope (replaces the deprecated `auth.devKey`) |
|
|
16
|
+
| `porulle doctor` | environment + config sanity check |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
When published:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add -g @porulle/cli
|
|
24
|
+
porulle init my-store
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
In the monorepo today (workspace dep):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd apps/<app> && bunx porulle init <name>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Starter template
|
|
34
|
+
|
|
35
|
+
`templates/starter/` is what `porulle init` copies. Adjust there to change the default scaffold.
|
|
36
|
+
|
|
37
|
+
## See also
|
|
38
|
+
|
|
39
|
+
- [Root README — Quick Start](../../README.md#quick-start)
|
|
40
|
+
- [`SECURITY.md`](../../SECURITY.md) — why `auth.devKey` was removed (`api-key create` replaces it)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const apiKeyCreateCommand: import("citty").CommandDef<{
|
|
2
|
+
scope: {
|
|
3
|
+
type: "string";
|
|
4
|
+
description: string;
|
|
5
|
+
required: true;
|
|
6
|
+
};
|
|
7
|
+
config: {
|
|
8
|
+
type: "string";
|
|
9
|
+
description: string;
|
|
10
|
+
default: string;
|
|
11
|
+
};
|
|
12
|
+
name: {
|
|
13
|
+
type: "string";
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
"save-env": {
|
|
17
|
+
type: "boolean";
|
|
18
|
+
description: string;
|
|
19
|
+
default: false;
|
|
20
|
+
};
|
|
21
|
+
"env-var": {
|
|
22
|
+
type: "string";
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
export declare const apiKeyListCommand: import("citty").CommandDef<{
|
|
27
|
+
config: {
|
|
28
|
+
type: "string";
|
|
29
|
+
description: string;
|
|
30
|
+
default: string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
export declare const apiKeyCommand: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
34
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../../src/commands/api-key.ts"],"names":[],"mappings":"AAgGA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;EAyH9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;EA+B5B,CAAC;AAEH,eAAO,MAAM,aAAa,qDASxB,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
/**
|
|
5
|
+
* Boots the commerce engine in-process and returns the auth instance.
|
|
6
|
+
* Uses dynamic import to avoid compile-time dependency on @porulle/core.
|
|
7
|
+
*/
|
|
8
|
+
async function bootEngine(configPath) {
|
|
9
|
+
const absPath = resolve(process.cwd(), configPath);
|
|
10
|
+
const configModule = await import(absPath);
|
|
11
|
+
const config = await configModule.default;
|
|
12
|
+
// Dynamic import — resolved at runtime from the consumer's node_modules
|
|
13
|
+
const corePkg = "@porulle/core";
|
|
14
|
+
const { createServer } = await import(/* webpackIgnore: true */ corePkg);
|
|
15
|
+
const { app, kernel } = await createServer(config);
|
|
16
|
+
const auth = kernel.auth;
|
|
17
|
+
return { config, auth, app, kernel };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Finds or creates an admin user for key ownership.
|
|
21
|
+
*/
|
|
22
|
+
async function findOrCreateAdminUser(app, config) {
|
|
23
|
+
// Try to create an admin user via the auth API
|
|
24
|
+
const email = "admin@local.dev";
|
|
25
|
+
const password = "admin-setup-" + Date.now();
|
|
26
|
+
const res = await app.request("http://localhost/api/auth/sign-up/email", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json", origin: "http://localhost" },
|
|
29
|
+
body: JSON.stringify({ email, password, name: "Admin" }),
|
|
30
|
+
});
|
|
31
|
+
if (res.ok) {
|
|
32
|
+
const data = (await res.json());
|
|
33
|
+
if (data?.user?.id)
|
|
34
|
+
return data.user.id;
|
|
35
|
+
}
|
|
36
|
+
// If signup fails (user exists), try sign-in
|
|
37
|
+
const signInRes = await app.request("http://localhost/api/auth/sign-in/email", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
origin: "http://localhost",
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ email, password }),
|
|
44
|
+
});
|
|
45
|
+
if (signInRes.ok) {
|
|
46
|
+
const data = (await signInRes.json());
|
|
47
|
+
if (data?.user?.id)
|
|
48
|
+
return data.user.id;
|
|
49
|
+
}
|
|
50
|
+
throw new Error("Could not find or create an admin user. Create one first via /api/auth/sign-up/email");
|
|
51
|
+
}
|
|
52
|
+
export const apiKeyCreateCommand = defineCommand({
|
|
53
|
+
meta: {
|
|
54
|
+
name: "create",
|
|
55
|
+
description: "Create an API key for a predefined scope",
|
|
56
|
+
},
|
|
57
|
+
args: {
|
|
58
|
+
scope: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Scope name (defined in commerce.config.ts auth.apiKeyScopes)",
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
config: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Path to commerce.config.ts",
|
|
66
|
+
default: "./commerce.config.ts",
|
|
67
|
+
},
|
|
68
|
+
name: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Key name (default: scope name)",
|
|
71
|
+
},
|
|
72
|
+
"save-env": {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description: "Save key to .env.local",
|
|
75
|
+
default: false,
|
|
76
|
+
},
|
|
77
|
+
"env-var": {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Env variable name (used with --save-env)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
async run({ args }) {
|
|
83
|
+
const { config, auth, app } = await bootEngine(args.config);
|
|
84
|
+
const scopes = config.auth?.apiKeyScopes;
|
|
85
|
+
if (!scopes || Object.keys(scopes).length === 0) {
|
|
86
|
+
console.error("No apiKeyScopes defined in commerce.config.ts");
|
|
87
|
+
console.error("");
|
|
88
|
+
console.error("Add scopes to your config:");
|
|
89
|
+
console.error(" auth: {");
|
|
90
|
+
console.error(" apiKeyScopes: {");
|
|
91
|
+
console.error(' storefront: { prefix: "uc_pub_", permissions: { catalog: ["read"] }, ... }');
|
|
92
|
+
console.error(" }");
|
|
93
|
+
console.error(" }");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const scopeDef = scopes[args.scope];
|
|
97
|
+
if (!scopeDef) {
|
|
98
|
+
console.error(`Unknown scope: "${args.scope}"`);
|
|
99
|
+
console.error("");
|
|
100
|
+
console.error("Available scopes:");
|
|
101
|
+
for (const [name, def] of Object.entries(scopes)) {
|
|
102
|
+
console.error(` ${name} — ${def.description}`);
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log(`Creating ${args.scope} API key...`);
|
|
107
|
+
// Find or create an admin user for key ownership
|
|
108
|
+
const userId = await findOrCreateAdminUser(app, config);
|
|
109
|
+
const result = await auth.api.createApiKey({
|
|
110
|
+
body: {
|
|
111
|
+
configId: args.scope,
|
|
112
|
+
userId,
|
|
113
|
+
name: args.name ?? `${args.scope}-key`,
|
|
114
|
+
permissions: scopeDef.permissions,
|
|
115
|
+
...(scopeDef.rateLimit
|
|
116
|
+
? {
|
|
117
|
+
rateLimitEnabled: true,
|
|
118
|
+
rateLimitMax: scopeDef.rateLimit.maxRequests,
|
|
119
|
+
rateLimitTimeWindow: scopeDef.rateLimit.timeWindow,
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(` Scope: ${args.scope}`);
|
|
126
|
+
console.log(` Description: ${scopeDef.description}`);
|
|
127
|
+
console.log(` Prefix: ${scopeDef.prefix}`);
|
|
128
|
+
console.log(` Key: ${result.key}`);
|
|
129
|
+
console.log(` ID: ${result.id}`);
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(" Permissions:");
|
|
132
|
+
for (const [resource, actions] of Object.entries(scopeDef.permissions)) {
|
|
133
|
+
console.log(` ${resource}: ${actions.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
if (scopeDef.rateLimit) {
|
|
136
|
+
console.log(` Rate limit: ${scopeDef.rateLimit.maxRequests} req / ${scopeDef.rateLimit.timeWindow}ms`);
|
|
137
|
+
}
|
|
138
|
+
if (args["save-env"]) {
|
|
139
|
+
const envFile = join(process.cwd(), ".env.local");
|
|
140
|
+
const varName = args["env-var"] ?? `UC_${args.scope.toUpperCase().replace(/-/g, "_")}_KEY`;
|
|
141
|
+
const line = `${varName}=${result.key}`;
|
|
142
|
+
if (existsSync(envFile)) {
|
|
143
|
+
const existing = readFileSync(envFile, "utf-8");
|
|
144
|
+
if (existing.includes(varName + "=")) {
|
|
145
|
+
// Replace existing
|
|
146
|
+
const updated = existing.replace(new RegExp(`^${varName}=.*$`, "m"), line);
|
|
147
|
+
writeFileSync(envFile, updated);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
writeFileSync(envFile, existing.trimEnd() + "\n" + line + "\n");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
writeFileSync(envFile, line + "\n");
|
|
155
|
+
}
|
|
156
|
+
console.log(` Saved to: .env.local as ${varName}`);
|
|
157
|
+
}
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log("This is the only time the full key is shown. Store it securely.");
|
|
160
|
+
process.exit(0);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
export const apiKeyListCommand = defineCommand({
|
|
164
|
+
meta: {
|
|
165
|
+
name: "list",
|
|
166
|
+
description: "List existing API keys",
|
|
167
|
+
},
|
|
168
|
+
args: {
|
|
169
|
+
config: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Path to commerce.config.ts",
|
|
172
|
+
default: "./commerce.config.ts",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
async run({ args }) {
|
|
176
|
+
const { config } = await bootEngine(args.config);
|
|
177
|
+
const scopes = config.auth?.apiKeyScopes ?? {};
|
|
178
|
+
console.log("Defined API key scopes:");
|
|
179
|
+
console.log("");
|
|
180
|
+
for (const [name, def] of Object.entries(scopes)) {
|
|
181
|
+
console.log(` ${name}`);
|
|
182
|
+
console.log(` ${def.description}`);
|
|
183
|
+
console.log(` Prefix: ${def.prefix}`);
|
|
184
|
+
console.log(` Permissions: ${Object.entries(def.permissions).map(([r, a]) => `${r}:${a.join(",")}`).join(" | ")}`);
|
|
185
|
+
if (def.rateLimit) {
|
|
186
|
+
console.log(` Rate limit: ${def.rateLimit.maxRequests} req / ${def.rateLimit.timeWindow}ms`);
|
|
187
|
+
}
|
|
188
|
+
console.log("");
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
export const apiKeyCommand = defineCommand({
|
|
194
|
+
meta: {
|
|
195
|
+
name: "api-key",
|
|
196
|
+
description: "Manage API keys (create, list)",
|
|
197
|
+
},
|
|
198
|
+
subCommands: {
|
|
199
|
+
create: apiKeyCreateCommand,
|
|
200
|
+
list: apiKeyListCommand,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const deployCommand: import("citty").CommandDef<{
|
|
2
|
+
target: {
|
|
3
|
+
type: "string";
|
|
4
|
+
required: true;
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
prod: {
|
|
8
|
+
type: "boolean";
|
|
9
|
+
default: false;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,aAAa;;;;;;;;;;;EA4CxB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
export const deployCommand = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "deploy",
|
|
7
|
+
description: "Deploy UnifiedCommerce app to a target environment.",
|
|
8
|
+
},
|
|
9
|
+
args: {
|
|
10
|
+
target: {
|
|
11
|
+
type: "string",
|
|
12
|
+
required: true,
|
|
13
|
+
description: "Deployment target. Supported: vercel",
|
|
14
|
+
},
|
|
15
|
+
prod: {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
default: false,
|
|
18
|
+
description: "Deploy production build",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
async run({ args }) {
|
|
22
|
+
const target = String(args.target).toLowerCase();
|
|
23
|
+
if (target !== "vercel") {
|
|
24
|
+
throw new Error(`Unsupported deploy target: ${target}. Currently supported: vercel.`);
|
|
25
|
+
}
|
|
26
|
+
const command = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
27
|
+
const deployArgs = ["vercel", "deploy"];
|
|
28
|
+
if (args.prod)
|
|
29
|
+
deployArgs.push("--prod");
|
|
30
|
+
consola.info(`Running deploy for target=${target} (${deployArgs.join(" ")})`);
|
|
31
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
32
|
+
const proc = spawn(command, deployArgs, {
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
shell: false,
|
|
35
|
+
});
|
|
36
|
+
proc.on("exit", (code) => {
|
|
37
|
+
if (code === 0)
|
|
38
|
+
resolvePromise();
|
|
39
|
+
else
|
|
40
|
+
rejectPromise(new Error(`Deploy command failed with code ${code}`));
|
|
41
|
+
});
|
|
42
|
+
proc.on("error", rejectPromise);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function loadDotenvFilesIfPresent(cwd: string): void;
|
|
2
|
+
export declare function isMissingUserTableError(error: unknown): boolean;
|
|
3
|
+
export declare function runDrizzleKitPush(cwd: string, env: NodeJS.ProcessEnv): Promise<void>;
|
|
4
|
+
export declare function maybeBootstrapDevDatabase(options: {
|
|
5
|
+
cwd: string;
|
|
6
|
+
nodeEnv: string | undefined;
|
|
7
|
+
databaseUrl: string | undefined;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare const devCommand: import("citty").CommandDef<{
|
|
10
|
+
port: {
|
|
11
|
+
type: "string";
|
|
12
|
+
default: string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAgBA,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAuB1D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM/D;AA4BD,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsChB;AAED,eAAO,MAAM,UAAU;;;;;EAgDrB,CAAC"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import consola from "consola";
|
|
7
|
+
import postgres from "postgres";
|
|
8
|
+
const DRIZZLE_CONFIG_CANDIDATES = [
|
|
9
|
+
"drizzle.config.ts",
|
|
10
|
+
"drizzle.config.mjs",
|
|
11
|
+
"drizzle.config.cjs",
|
|
12
|
+
"drizzle.config.js",
|
|
13
|
+
"drizzle.config.json",
|
|
14
|
+
];
|
|
15
|
+
export function loadDotenvFilesIfPresent(cwd) {
|
|
16
|
+
for (const name of [".env.local", ".env"]) {
|
|
17
|
+
const path = join(cwd, name);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
continue;
|
|
20
|
+
const content = readFileSync(path, "utf8");
|
|
21
|
+
for (const rawLine of content.split("\n")) {
|
|
22
|
+
const line = rawLine.trim();
|
|
23
|
+
if (!line || line.startsWith("#"))
|
|
24
|
+
continue;
|
|
25
|
+
const eq = line.indexOf("=");
|
|
26
|
+
if (eq <= 0)
|
|
27
|
+
continue;
|
|
28
|
+
const key = line.slice(0, eq).trim();
|
|
29
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
30
|
+
continue;
|
|
31
|
+
if (process.env[key] !== undefined)
|
|
32
|
+
continue;
|
|
33
|
+
let val = line.slice(eq + 1).trim();
|
|
34
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
35
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
36
|
+
val = val.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
process.env[key] = val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function isMissingUserTableError(error) {
|
|
43
|
+
if (!error || typeof error !== "object")
|
|
44
|
+
return false;
|
|
45
|
+
const err = error;
|
|
46
|
+
if (err.code === "42P01")
|
|
47
|
+
return true;
|
|
48
|
+
if (typeof err.message !== "string")
|
|
49
|
+
return false;
|
|
50
|
+
return err.message.includes('relation "user" does not exist');
|
|
51
|
+
}
|
|
52
|
+
function resolveDrizzleConfigFileName(cwd) {
|
|
53
|
+
for (const name of DRIZZLE_CONFIG_CANDIDATES) {
|
|
54
|
+
if (existsSync(join(cwd, name)))
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function spawnCwdForDrizzleKit(projectCwd) {
|
|
60
|
+
let dir = projectCwd;
|
|
61
|
+
for (;;) {
|
|
62
|
+
if (existsSync(join(dir, "node_modules", "drizzle-orm"))) {
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
const parent = dirname(dir);
|
|
66
|
+
if (parent === dir)
|
|
67
|
+
break;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
const cliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
71
|
+
const workspaceRoot = join(cliPackageRoot, "..", "..");
|
|
72
|
+
const corePkg = join(workspaceRoot, "packages", "core");
|
|
73
|
+
if (existsSync(join(corePkg, "node_modules", "drizzle-orm"))) {
|
|
74
|
+
return corePkg;
|
|
75
|
+
}
|
|
76
|
+
return projectCwd;
|
|
77
|
+
}
|
|
78
|
+
export async function runDrizzleKitPush(cwd, env) {
|
|
79
|
+
const configName = resolveDrizzleConfigFileName(cwd);
|
|
80
|
+
if (!configName) {
|
|
81
|
+
throw new Error("No drizzle.config.ts (or .mjs/.js/.json) found — cannot bootstrap the database.");
|
|
82
|
+
}
|
|
83
|
+
const configPath = join(cwd, configName);
|
|
84
|
+
const spawnCwd = spawnCwdForDrizzleKit(cwd);
|
|
85
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
86
|
+
const proc = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["drizzle-kit", "push", "--force", "--config", configPath], { cwd: spawnCwd, stdio: "inherit", shell: false, env });
|
|
87
|
+
proc.on("exit", (code) => {
|
|
88
|
+
if (code === 0)
|
|
89
|
+
resolvePromise();
|
|
90
|
+
else {
|
|
91
|
+
rejectPromise(new Error(`drizzle-kit push exited with code ${code ?? "unknown"}`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
proc.on("error", rejectPromise);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function maybeBootstrapDevDatabase(options) {
|
|
98
|
+
if (options.nodeEnv === "production")
|
|
99
|
+
return;
|
|
100
|
+
const databaseUrl = options.databaseUrl?.trim();
|
|
101
|
+
if (!databaseUrl)
|
|
102
|
+
return;
|
|
103
|
+
let sql;
|
|
104
|
+
try {
|
|
105
|
+
sql = postgres(databaseUrl, { max: 1 });
|
|
106
|
+
await sql `SELECT 1 FROM "user" LIMIT 1`;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!isMissingUserTableError(error))
|
|
110
|
+
throw error;
|
|
111
|
+
const configName = resolveDrizzleConfigFileName(options.cwd);
|
|
112
|
+
if (!configName) {
|
|
113
|
+
throw new Error("Core auth tables are missing and no Drizzle config was found. Add drizzle.config.ts or run `bun run db:push`.");
|
|
114
|
+
}
|
|
115
|
+
console.log("⚠ Tables missing — running drizzle-kit push to bootstrap…");
|
|
116
|
+
if (sql) {
|
|
117
|
+
await sql.end({ timeout: 5 });
|
|
118
|
+
sql = undefined;
|
|
119
|
+
}
|
|
120
|
+
await runDrizzleKitPush(options.cwd, {
|
|
121
|
+
...process.env,
|
|
122
|
+
DATABASE_URL: databaseUrl,
|
|
123
|
+
});
|
|
124
|
+
console.log("✓ Bootstrap complete. Starting dev server…");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (sql)
|
|
129
|
+
await sql.end({ timeout: 5 });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export const devCommand = defineCommand({
|
|
133
|
+
meta: {
|
|
134
|
+
name: "dev",
|
|
135
|
+
description: "Run local dev server with file watching",
|
|
136
|
+
},
|
|
137
|
+
args: {
|
|
138
|
+
port: {
|
|
139
|
+
type: "string",
|
|
140
|
+
default: "3000",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
async run({ args }) {
|
|
144
|
+
const port = String(args.port);
|
|
145
|
+
consola.info(`Starting dev server on port ${port}`);
|
|
146
|
+
loadDotenvFilesIfPresent(process.cwd());
|
|
147
|
+
try {
|
|
148
|
+
await maybeBootstrapDevDatabase({
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
nodeEnv: process.env.NODE_ENV,
|
|
151
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
consola.error(error);
|
|
156
|
+
consola.error("Try `bun run db:push` manually or check your DATABASE_URL.");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
const proc = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["tsx", "watch", "src/dev-server.ts", "--port", port], {
|
|
160
|
+
stdio: "inherit",
|
|
161
|
+
shell: false,
|
|
162
|
+
});
|
|
163
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
164
|
+
proc.on("exit", (code) => {
|
|
165
|
+
if (code === 0)
|
|
166
|
+
resolvePromise();
|
|
167
|
+
else
|
|
168
|
+
rejectPromise(new Error(`dev exited with code ${code}`));
|
|
169
|
+
});
|
|
170
|
+
proc.on("error", rejectPromise);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAyMA,eAAO,MAAM,aAAa;;;;;;EAiRxB,CAAC"}
|