@flaredev/cli 0.7.0 → 0.7.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/index.js CHANGED
@@ -4160,10 +4160,17 @@ ${result.output.trim()}`);
4160
4160
  import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync8, writeFileSync as writeFileSync15 } from "fs";
4161
4161
  import { dirname as dirname11, join as join21, relative as relative2 } from "path";
4162
4162
  import pc17 from "picocolors";
4163
- var TRACKED = ["lib/resource"];
4163
+ var TRACKED = ["lib", "components"];
4164
+ var PER_APP = /__APP_NAME__|__PM__|__THEME__|__COMPAT_DATE__/;
4165
+ var WRITTEN_PER_APP = /* @__PURE__ */ new Set(["lib/auth-config.ts"]);
4164
4166
  var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only) => path === only || path.startsWith(`${only}/`));
4165
4167
  function upstreamFile(path, stack) {
4166
- if (prunedOn(path, stack)) return void 0;
4168
+ if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
4169
+ const found = locate(path, stack);
4170
+ if (found && PER_APP.test(readFileSync14(found, "utf8"))) return void 0;
4171
+ return found;
4172
+ }
4173
+ function locate(path, stack) {
4167
4174
  const candidates = stack === "next" ? [join21(templatesDir, "next", path), join21(templatesDir, "app", path)] : [join21(templatesDir, "app", path)];
4168
4175
  return candidates.find((candidate) => existsSync22(candidate));
4169
4176
  }
@@ -5205,6 +5212,8 @@ ${pc22.red("Out of sync:")} ${stale} file(s) would change. Run ${pc22.bold("flar
5205
5212
  }
5206
5213
 
5207
5214
  // src/commands/user-role.ts
5215
+ import { rmSync as rmSync9, writeFileSync as writeFileSync21 } from "fs";
5216
+ import { join as join28 } from "path";
5208
5217
  import pc23 from "picocolors";
5209
5218
  var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
5210
5219
  var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
@@ -5217,6 +5226,7 @@ async function setUserRole(email, role, options2 = {}) {
5217
5226
  const log2 = options2.log ?? ((message) => console.log(message));
5218
5227
  const appRoot = findAppRoot(options2.cwd ?? process.cwd());
5219
5228
  const sql2 = userRoleSql(email, role);
5229
+ if (readStack(appRoot) === "next") return setUserRoleOnPrisma(appRoot, email, role, log2);
5220
5230
  const roles = await listRoles(options2);
5221
5231
  if (roles && !roles.includes(role)) {
5222
5232
  log2(pc23.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
@@ -5240,13 +5250,45 @@ ${result.output.trim()}`);
5240
5250
  log2(pc23.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
5241
5251
  return 0;
5242
5252
  }
5253
+ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
5254
+ const runner = join28(appRoot, ".flare-role-runner.mts");
5255
+ const script = [
5256
+ `import "dotenv/config";`,
5257
+ `import { prisma } from "./lib/db";`,
5258
+ "",
5259
+ `const client = prisma as unknown as { user: { updateMany(args: unknown): Promise<{ count: number }> }; $disconnect(): Promise<void> };`,
5260
+ `const { count } = await client.user.updateMany({`,
5261
+ ` where: { email: { equals: ${JSON.stringify(email)}, mode: "insensitive" } },`,
5262
+ ` data: { role: ${JSON.stringify(role)} },`,
5263
+ `});`,
5264
+ `await client.$disconnect();`,
5265
+ `// 3 means "no account with that email", which the CLI turns into a useful message.`,
5266
+ `if (count === 0) process.exit(3);`,
5267
+ ""
5268
+ ].join("\n");
5269
+ writeFileSync21(runner, script);
5270
+ try {
5271
+ const tsx = resolveBin(appRoot, "tsx", "tsx");
5272
+ const code = await runNode([tsx, runner], appRoot);
5273
+ if (code === 3) {
5274
+ log2(pc23.red(`No account with the email ${email}.`));
5275
+ log2(pc23.dim("Sign up first, then set the role."));
5276
+ return 1;
5277
+ }
5278
+ if (code !== 0) return code;
5279
+ log2(pc23.green(`\u2714 ${email} is now ${role}.`));
5280
+ return 0;
5281
+ } finally {
5282
+ rmSync9(runner, { force: true });
5283
+ }
5284
+ }
5243
5285
 
5244
5286
  // src/utils/install.ts
5245
5287
  import { spawn as spawn5 } from "child_process";
5246
5288
  import { existsSync as existsSync26, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
5247
- import { join as join28 } from "path";
5289
+ import { join as join29 } from "path";
5248
5290
  function expectedPackages(dir) {
5249
- const npmLock = join28(dir, "package-lock.json");
5291
+ const npmLock = join29(dir, "package-lock.json");
5250
5292
  if (existsSync26(npmLock)) {
5251
5293
  try {
5252
5294
  const lock = JSON.parse(readFileSync17(npmLock, "utf8"));
@@ -5255,7 +5297,7 @@ function expectedPackages(dir) {
5255
5297
  return 0;
5256
5298
  }
5257
5299
  }
5258
- const pnpmLock = join28(dir, "pnpm-lock.yaml");
5300
+ const pnpmLock = join29(dir, "pnpm-lock.yaml");
5259
5301
  if (existsSync26(pnpmLock)) {
5260
5302
  try {
5261
5303
  const text = readFileSync17(pnpmLock, "utf8");
@@ -5268,7 +5310,7 @@ function expectedPackages(dir) {
5268
5310
  return 0;
5269
5311
  }
5270
5312
  function installedPackages(dir) {
5271
- const root = join28(dir, "node_modules");
5313
+ const root = join29(dir, "node_modules");
5272
5314
  if (!existsSync26(root)) return 0;
5273
5315
  let total = 0;
5274
5316
  try {
@@ -5276,7 +5318,7 @@ function installedPackages(dir) {
5276
5318
  if (!entry.isDirectory() || entry.name === ".bin") continue;
5277
5319
  if (entry.name.startsWith("@")) {
5278
5320
  try {
5279
- total += readdirSync10(join28(root, entry.name)).length;
5321
+ total += readdirSync10(join29(root, entry.name)).length;
5280
5322
  } catch {
5281
5323
  }
5282
5324
  } 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.2",
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.2"
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
  };