@flaredev/cli 0.7.0 → 0.7.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/dist/index.js +40 -5
- package/package.json +2 -2
- package/templates/next/lib/storage.ts +33 -7
package/dist/index.js
CHANGED
|
@@ -5205,6 +5205,8 @@ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flar
|
|
|
5205
5205
|
}
|
|
5206
5206
|
|
|
5207
5207
|
// src/commands/user-role.ts
|
|
5208
|
+
import { rmSync as rmSync9, writeFileSync as writeFileSync21 } from "fs";
|
|
5209
|
+
import { join as join28 } from "path";
|
|
5208
5210
|
import pc23 from "picocolors";
|
|
5209
5211
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5210
5212
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
@@ -5217,6 +5219,7 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
5217
5219
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
5218
5220
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
5219
5221
|
const sql2 = userRoleSql(email, role);
|
|
5222
|
+
if (readStack(appRoot) === "next") return setUserRoleOnPrisma(appRoot, email, role, log2);
|
|
5220
5223
|
const roles = await listRoles(options2);
|
|
5221
5224
|
if (roles && !roles.includes(role)) {
|
|
5222
5225
|
log2(pc23.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
@@ -5240,13 +5243,45 @@ ${result.output.trim()}`);
|
|
|
5240
5243
|
log2(pc23.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
5241
5244
|
return 0;
|
|
5242
5245
|
}
|
|
5246
|
+
async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
5247
|
+
const runner = join28(appRoot, ".flare-role-runner.mts");
|
|
5248
|
+
const script = [
|
|
5249
|
+
`import "dotenv/config";`,
|
|
5250
|
+
`import { prisma } from "./lib/db";`,
|
|
5251
|
+
"",
|
|
5252
|
+
`const client = prisma as unknown as { user: { updateMany(args: unknown): Promise<{ count: number }> }; $disconnect(): Promise<void> };`,
|
|
5253
|
+
`const { count } = await client.user.updateMany({`,
|
|
5254
|
+
` where: { email: { equals: ${JSON.stringify(email)}, mode: "insensitive" } },`,
|
|
5255
|
+
` data: { role: ${JSON.stringify(role)} },`,
|
|
5256
|
+
`});`,
|
|
5257
|
+
`await client.$disconnect();`,
|
|
5258
|
+
`// 3 means "no account with that email", which the CLI turns into a useful message.`,
|
|
5259
|
+
`if (count === 0) process.exit(3);`,
|
|
5260
|
+
""
|
|
5261
|
+
].join("\n");
|
|
5262
|
+
writeFileSync21(runner, script);
|
|
5263
|
+
try {
|
|
5264
|
+
const tsx = resolveBin(appRoot, "tsx", "tsx");
|
|
5265
|
+
const code = await runNode([tsx, runner], appRoot);
|
|
5266
|
+
if (code === 3) {
|
|
5267
|
+
log2(pc23.red(`No account with the email ${email}.`));
|
|
5268
|
+
log2(pc23.dim("Sign up first, then set the role."));
|
|
5269
|
+
return 1;
|
|
5270
|
+
}
|
|
5271
|
+
if (code !== 0) return code;
|
|
5272
|
+
log2(pc23.green(`\u2714 ${email} is now ${role}.`));
|
|
5273
|
+
return 0;
|
|
5274
|
+
} finally {
|
|
5275
|
+
rmSync9(runner, { force: true });
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5243
5278
|
|
|
5244
5279
|
// src/utils/install.ts
|
|
5245
5280
|
import { spawn as spawn5 } from "child_process";
|
|
5246
5281
|
import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
|
|
5247
|
-
import { join as
|
|
5282
|
+
import { join as join29 } from "path";
|
|
5248
5283
|
function expectedPackages(dir) {
|
|
5249
|
-
const npmLock =
|
|
5284
|
+
const npmLock = join29(dir, "package-lock.json");
|
|
5250
5285
|
if (existsSync26(npmLock)) {
|
|
5251
5286
|
try {
|
|
5252
5287
|
const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
|
|
@@ -5255,7 +5290,7 @@ function expectedPackages(dir) {
|
|
|
5255
5290
|
return 0;
|
|
5256
5291
|
}
|
|
5257
5292
|
}
|
|
5258
|
-
const pnpmLock =
|
|
5293
|
+
const pnpmLock = join29(dir, "pnpm-lock.yaml");
|
|
5259
5294
|
if (existsSync26(pnpmLock)) {
|
|
5260
5295
|
try {
|
|
5261
5296
|
const text = readFileSync17(pnpmLock, "utf8");
|
|
@@ -5268,7 +5303,7 @@ function expectedPackages(dir) {
|
|
|
5268
5303
|
return 0;
|
|
5269
5304
|
}
|
|
5270
5305
|
function installedPackages(dir) {
|
|
5271
|
-
const root =
|
|
5306
|
+
const root = join29(dir, "node_modules");
|
|
5272
5307
|
if (!existsSync26(root)) return 0;
|
|
5273
5308
|
let total = 0;
|
|
5274
5309
|
try {
|
|
@@ -5276,7 +5311,7 @@ function installedPackages(dir) {
|
|
|
5276
5311
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
5277
5312
|
if (entry.name.startsWith("@")) {
|
|
5278
5313
|
try {
|
|
5279
|
-
total += readdirSync10(
|
|
5314
|
+
total += readdirSync10(join29(root, entry.name)).length;
|
|
5280
5315
|
} catch {
|
|
5281
5316
|
}
|
|
5282
5317
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flaredev/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "The flare command-line tool: create, generate, migrate, and deploy Flare apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Muke Johnbaptist",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
42
|
"jsonc-parser": "^3.3.1",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
|
-
"@flaredev/core": "0.7.
|
|
44
|
+
"@flaredev/core": "0.7.1"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22"
|
|
@@ -21,10 +21,26 @@ const client = new AwsClient({
|
|
|
21
21
|
|
|
22
22
|
const endpoint = `${process.env.R2_ENDPOINT ?? ""}/${process.env.R2_BUCKET ?? ""}`;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A key as a URL path.
|
|
26
|
+
*
|
|
27
|
+
* Each segment is escaped on its own, so the slashes in "products/2026/09/a.png" stay
|
|
28
|
+
* slashes. Escaping the whole key would turn them into %2F, which is a different object
|
|
29
|
+
* name to S3 even where a particular server is forgiving about it.
|
|
30
|
+
*/
|
|
31
|
+
const path = (key: string) => key.split("/").map(encodeURIComponent).join("/");
|
|
32
|
+
|
|
33
|
+
/** Raise a bucket error rather than letting a failed write look like a successful one. */
|
|
34
|
+
async function ok(response: Response, doing: string, key: string): Promise<Response> {
|
|
35
|
+
if (response.ok) return response;
|
|
36
|
+
const detail = await response.text().catch(() => "");
|
|
37
|
+
throw new Error(`Storage ${doing} failed for "${key}": ${response.status} ${detail.slice(0, 300)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/** The bucket, shaped the way @flaredev/core expects, over plain HTTP calls. */
|
|
25
41
|
const bucket = {
|
|
26
42
|
async get(key: string) {
|
|
27
|
-
const response = await client.fetch(`${endpoint}/${
|
|
43
|
+
const response = await client.fetch(`${endpoint}/${path(key)}`);
|
|
28
44
|
if (!response.ok || !response.body) return null;
|
|
29
45
|
return {
|
|
30
46
|
body: response.body,
|
|
@@ -35,15 +51,25 @@ const bucket = {
|
|
|
35
51
|
};
|
|
36
52
|
},
|
|
37
53
|
async put(key: string, value: ReadableStream | ArrayBuffer | string | null, options?: { httpMetadata?: { contentType?: string } }) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
// S3 needs a Content-Length, and a stream has no length to send — it answers
|
|
55
|
+
// "411 MissingContentLength" and stores nothing. Uploads are capped by the field's
|
|
56
|
+
// own maxBytes long before they reach here, so reading one into memory is safe and
|
|
57
|
+
// is what makes the length known.
|
|
58
|
+
//
|
|
59
|
+
// The length is set by hand rather than left to fetch. Next patches the global
|
|
60
|
+
// fetch, and in that runtime it does not derive content-length even from a buffered
|
|
61
|
+
// body — so S3 still answers 411 and stores nothing. Buffering everything through
|
|
62
|
+
// Response keeps one path for streams, buffers and strings alike.
|
|
63
|
+
const body = value === null ? new ArrayBuffer(0) : await new Response(value as BodyInit).arrayBuffer();
|
|
64
|
+
const headers: Record<string, string> = { "content-length": String(body.byteLength) };
|
|
65
|
+
if (options?.httpMetadata?.contentType) headers["content-type"] = options.httpMetadata.contentType;
|
|
66
|
+
await ok(await client.fetch(`${endpoint}/${path(key)}`, { method: "PUT", body, headers }), "upload", key);
|
|
43
67
|
},
|
|
44
68
|
async delete(keys: string | string[]) {
|
|
45
69
|
for (const key of [keys].flat()) {
|
|
46
|
-
await client.fetch(`${endpoint}/${
|
|
70
|
+
const response = await client.fetch(`${endpoint}/${path(key)}`, { method: "DELETE" });
|
|
71
|
+
// S3 answers 204 for a key that was never there, which is the outcome we wanted.
|
|
72
|
+
if (response.status !== 404) await ok(response, "delete", key);
|
|
47
73
|
}
|
|
48
74
|
},
|
|
49
75
|
};
|