@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 +49 -7
- package/package.json +2 -2
- package/templates/next/lib/storage.ts +33 -7
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
|
|
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
|
|
5289
|
+
import { join as join29 } from "path";
|
|
5248
5290
|
function expectedPackages(dir) {
|
|
5249
|
-
const npmLock =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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.
|
|
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.
|
|
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}/${
|
|
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
|
};
|