@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 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 join28 } from "path";
5282
+ import { join as join29 } from "path";
5248
5283
  function expectedPackages(dir) {
5249
- const npmLock = join28(dir, "package-lock.json");
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 = join28(dir, "pnpm-lock.yaml");
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 = join28(dir, "node_modules");
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(join28(root, entry.name)).length;
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.0",
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.0"
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}/${encodeURIComponent(key)}`);
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
- await client.fetch(`${endpoint}/${encodeURIComponent(key)}`, {
39
- method: "PUT",
40
- body: value,
41
- headers: options?.httpMetadata?.contentType ? { "content-type": options.httpMetadata.contentType } : undefined,
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}/${encodeURIComponent(key)}`, { method: "DELETE" });
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
  };