@flaredev/cli 0.9.0 → 0.10.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/dist/index.js +241 -37
- package/package.json +2 -2
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +8 -8
- package/templates/app/lib/auth.ts +177 -154
- package/templates/app/lib/resource/store.ts +63 -8
- package/templates/locks/package-lock.json +73 -73
- package/templates/locks/pnpm-lock.yaml +99 -99
- package/templates/locks/yarn.lock +74 -74
- package/templates/next/lib/auth.ts +25 -2
package/dist/index.js
CHANGED
|
@@ -189,7 +189,11 @@ var APP_DEV_DEPENDENCIES = {
|
|
|
189
189
|
wrangler: "^4.132.0"
|
|
190
190
|
};
|
|
191
191
|
var NEXT_DEPENDENCIES = {
|
|
192
|
-
|
|
192
|
+
// 16.0.4 was carrying 36 advisories, three of them critical: unauthenticated RCE in the
|
|
193
|
+
// image optimizer and on Windows hosts (both fixed in 16.3.3), and RCE in the flight
|
|
194
|
+
// protocol. Vercel refuses to build it. A pin is only worth having if it is moved when
|
|
195
|
+
// one of these lands, so treat a critical against this line as a release of its own.
|
|
196
|
+
next: "16.3.8",
|
|
193
197
|
"@prisma/client": "7.10.0",
|
|
194
198
|
"@prisma/adapter-pg": "7.10.0",
|
|
195
199
|
pg: "^8.16.3",
|
|
@@ -1275,6 +1279,20 @@ function runNode(argv, cwd) {
|
|
|
1275
1279
|
child.on("exit", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
|
|
1276
1280
|
});
|
|
1277
1281
|
}
|
|
1282
|
+
function runNodeCapturing(argv, cwd) {
|
|
1283
|
+
return new Promise((resolvePromise, reject) => {
|
|
1284
|
+
const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"] });
|
|
1285
|
+
let output = "";
|
|
1286
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
1287
|
+
stream?.on("data", (chunk) => {
|
|
1288
|
+
output += chunk.toString();
|
|
1289
|
+
process.stdout.write(chunk);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
child.on("error", reject);
|
|
1293
|
+
child.on("exit", (code, signal) => resolvePromise({ code: code ?? (signal ? 1 : 0), output }));
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1278
1296
|
function runNodeWithTunnel(argv, cwd, opened) {
|
|
1279
1297
|
return new Promise((resolvePromise, reject) => {
|
|
1280
1298
|
const child = spawn3(process.execPath, argv, { cwd, stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" } });
|
|
@@ -2644,7 +2662,7 @@ function prismaType(resource, key, def) {
|
|
|
2644
2662
|
case "int":
|
|
2645
2663
|
return "Int";
|
|
2646
2664
|
case "float":
|
|
2647
|
-
return "Float";
|
|
2665
|
+
return def.format === "money" ? "BigInt" : "Float";
|
|
2648
2666
|
case "boolean":
|
|
2649
2667
|
return "Boolean";
|
|
2650
2668
|
case "date":
|
|
@@ -2773,7 +2791,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
2773
2791
|
expr = `integer(${col})`;
|
|
2774
2792
|
break;
|
|
2775
2793
|
case "float":
|
|
2776
|
-
expr = `real(${col})`;
|
|
2794
|
+
expr = def.format === "money" ? `integer(${col})` : `real(${col})`;
|
|
2777
2795
|
break;
|
|
2778
2796
|
case "boolean":
|
|
2779
2797
|
expr = `integer(${col}, { mode: "boolean" })`;
|
|
@@ -4314,10 +4332,88 @@ ${body}
|
|
|
4314
4332
|
}
|
|
4315
4333
|
|
|
4316
4334
|
// src/commands/gen-migration.ts
|
|
4317
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
4335
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
4318
4336
|
import { basename as basename2, join as join19 } from "path";
|
|
4319
4337
|
import { snakeCase } from "@flaredev/core";
|
|
4320
4338
|
import pc13 from "picocolors";
|
|
4339
|
+
|
|
4340
|
+
// src/generator/destructive.ts
|
|
4341
|
+
function stripNoise(sql2) {
|
|
4342
|
+
let out = "";
|
|
4343
|
+
let index = 0;
|
|
4344
|
+
while (index < sql2.length) {
|
|
4345
|
+
const two = sql2.slice(index, index + 2);
|
|
4346
|
+
if (two === "--") {
|
|
4347
|
+
const end = sql2.indexOf("\n", index);
|
|
4348
|
+
const stop = end === -1 ? sql2.length : end;
|
|
4349
|
+
out += " ".repeat(stop - index);
|
|
4350
|
+
index = stop;
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (two === "/*") {
|
|
4354
|
+
const end = sql2.indexOf("*/", index + 2);
|
|
4355
|
+
const skipped = sql2.slice(index, end === -1 ? sql2.length : end + 2);
|
|
4356
|
+
out += skipped.replaceAll(/[^\n]/g, " ");
|
|
4357
|
+
index = end === -1 ? sql2.length : end + 2;
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
const char = sql2[index];
|
|
4361
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
4362
|
+
let at = index + 1;
|
|
4363
|
+
while (at < sql2.length) {
|
|
4364
|
+
if (sql2[at] === char) {
|
|
4365
|
+
if (sql2[at + 1] === char) at += 2;
|
|
4366
|
+
else break;
|
|
4367
|
+
} else at += 1;
|
|
4368
|
+
}
|
|
4369
|
+
const literal2 = sql2.slice(index, Math.min(at + 1, sql2.length));
|
|
4370
|
+
out += literal2.replaceAll(/[^\n]/g, " ");
|
|
4371
|
+
index = at + 1;
|
|
4372
|
+
continue;
|
|
4373
|
+
}
|
|
4374
|
+
out += char;
|
|
4375
|
+
index += 1;
|
|
4376
|
+
}
|
|
4377
|
+
return out;
|
|
4378
|
+
}
|
|
4379
|
+
var PATTERNS = [
|
|
4380
|
+
[/\bDROP\s+COLUMN\b/i, "a column, and the values in it"],
|
|
4381
|
+
[/\bDROP\s+TABLE\b/i, "a table, and every row in it"],
|
|
4382
|
+
[/\bTRUNCATE\b/i, "every row in a table"],
|
|
4383
|
+
[/\bDROP\s+(?:MATERIALIZED\s+)?VIEW\b/i, "a view"],
|
|
4384
|
+
[/\bDROP\s+INDEX\b/i, "an index \u2014 if it was written by hand, Prisma cannot recreate it"],
|
|
4385
|
+
[/\bDROP\s+TRIGGER\b/i, "a trigger \u2014 Prisma does not know about it and will not put it back"],
|
|
4386
|
+
[/\bDROP\s+FUNCTION\b/i, "a function"],
|
|
4387
|
+
[/\bDROP\s+CONSTRAINT\b/i, "a constraint"],
|
|
4388
|
+
[/\bDROP\s+TYPE\b/i, "a type"],
|
|
4389
|
+
[/\bDROP\s+SEQUENCE\b/i, "a sequence"],
|
|
4390
|
+
[/\bDROP\s+SCHEMA\b/i, "a schema, and everything in it"],
|
|
4391
|
+
[/\bDROP\s+DATABASE\b/i, "the database"]
|
|
4392
|
+
];
|
|
4393
|
+
function destructiveStatements(sql2) {
|
|
4394
|
+
const cleaned = stripNoise(sql2);
|
|
4395
|
+
const found = [];
|
|
4396
|
+
let start = 0;
|
|
4397
|
+
for (let index = 0; index <= cleaned.length; index += 1) {
|
|
4398
|
+
if (index !== cleaned.length && cleaned[index] !== ";") continue;
|
|
4399
|
+
const statement = cleaned.slice(start, index);
|
|
4400
|
+
const trimmed = statement.trim();
|
|
4401
|
+
const from = start;
|
|
4402
|
+
start = index + 1;
|
|
4403
|
+
if (!trimmed) continue;
|
|
4404
|
+
const match = PATTERNS.find(([pattern]) => pattern.test(trimmed));
|
|
4405
|
+
if (!match) continue;
|
|
4406
|
+
const offset = from + statement.search(/\S/);
|
|
4407
|
+
found.push({
|
|
4408
|
+
statement: sql2.slice(offset, index).trim().replaceAll(/\s+/g, " ").slice(0, 160),
|
|
4409
|
+
kind: match[1],
|
|
4410
|
+
line: cleaned.slice(0, offset).split("\n").length
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
return found;
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// src/commands/gen-migration.ts
|
|
4321
4417
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
4322
4418
|
-- Write SQL that undoes the up migration, e.g.:
|
|
4323
4419
|
-- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
|
|
@@ -4328,6 +4424,7 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
4328
4424
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4329
4425
|
const name = snakeCase(rawName);
|
|
4330
4426
|
if (!name) throw new Error(`Invalid migration name "${rawName}". Use letters, digits, and underscores.`);
|
|
4427
|
+
if (readStack(appRoot) === "next") return genPrismaMigration(appRoot, name, options2);
|
|
4331
4428
|
const { files, repairs } = await generateSchemaMigration(appRoot, name, { custom: !options2.fromSchema });
|
|
4332
4429
|
if (files.length === 0) {
|
|
4333
4430
|
log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
|
|
@@ -4352,6 +4449,89 @@ Next: ${pc13.bold("flare migrate")}.`);
|
|
|
4352
4449
|
}
|
|
4353
4450
|
return files;
|
|
4354
4451
|
}
|
|
4452
|
+
async function genPrismaMigration(appRoot, name, options2) {
|
|
4453
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4454
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
4455
|
+
const migrations = join19(appRoot, "prisma", "migrations");
|
|
4456
|
+
const before = new Set(existsSync20(migrations) ? readdirSync7(migrations) : []);
|
|
4457
|
+
log2(`${pc13.cyan("prisma")} migrate dev --create-only --name ${name}`);
|
|
4458
|
+
const { code, output } = await runNodeCapturing([prisma, "migrate", "dev", "--create-only", "--name", name], appRoot);
|
|
4459
|
+
if (code !== 0) {
|
|
4460
|
+
if (/drift detected|we need to reset/i.test(output)) {
|
|
4461
|
+
log2("");
|
|
4462
|
+
log2(pc13.yellow("Prisma found something in the database that its schema does not describe."));
|
|
4463
|
+
log2(
|
|
4464
|
+
[
|
|
4465
|
+
"",
|
|
4466
|
+
`Nothing has been changed \u2014 ${pc13.bold("--create-only")} writes a migration, it does not apply one, and no`,
|
|
4467
|
+
"migration was written. Do not run `prisma migrate reset`: it drops the database, and what it",
|
|
4468
|
+
"would drop is the thing Prisma is complaining it cannot see.",
|
|
4469
|
+
"",
|
|
4470
|
+
"Drift above is reported as changes to get from your migrations to the database, so it lists",
|
|
4471
|
+
"what the database has and the migrations do not. Usually that is something added by hand:",
|
|
4472
|
+
"",
|
|
4473
|
+
` ${pc13.bold("It should be there")} \u2014 write it into a migration so the history matches. Add the SQL to a`,
|
|
4474
|
+
" new migration and mark it applied with `prisma migrate resolve --applied <name>`.",
|
|
4475
|
+
"",
|
|
4476
|
+
` ${pc13.bold("It should not be there")} \u2014 remove it from the database yourself, then run this again.`,
|
|
4477
|
+
"",
|
|
4478
|
+
"An index, a trigger or a function is not reported as drift at all; Prisma does not look at",
|
|
4479
|
+
"those. They survive a migration and are lost by a reset, which is the other reason not to."
|
|
4480
|
+
].join("\n")
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
return [];
|
|
4484
|
+
}
|
|
4485
|
+
const added = (existsSync20(migrations) ? readdirSync7(migrations) : []).filter((entry) => !before.has(entry));
|
|
4486
|
+
if (added.length === 0) {
|
|
4487
|
+
log2("No schema changes, so no migration was created.");
|
|
4488
|
+
return [];
|
|
4489
|
+
}
|
|
4490
|
+
const written = [];
|
|
4491
|
+
const destructive = [];
|
|
4492
|
+
for (const entry of added) {
|
|
4493
|
+
const file = join19(migrations, entry, "migration.sql");
|
|
4494
|
+
if (!existsSync20(file)) continue;
|
|
4495
|
+
const relative3 = `prisma/migrations/${entry}/migration.sql`;
|
|
4496
|
+
written.push(relative3);
|
|
4497
|
+
log2(`${pc13.green("create".padEnd(9))} ${relative3}`);
|
|
4498
|
+
const found = destructiveStatements(readFileSync13(file, "utf8"));
|
|
4499
|
+
if (found.length > 0) destructive.push({ file: relative3, found });
|
|
4500
|
+
}
|
|
4501
|
+
if (destructive.length > 0) {
|
|
4502
|
+
log2("");
|
|
4503
|
+
log2(pc13.yellow(`This migration throws things away, so it has not been applied:`));
|
|
4504
|
+
for (const { file, found } of destructive) {
|
|
4505
|
+
log2(pc13.dim(`
|
|
4506
|
+
${file}`));
|
|
4507
|
+
for (const entry of found) {
|
|
4508
|
+
log2(` ${pc13.red(`line ${entry.line}`)} ${entry.statement}`);
|
|
4509
|
+
log2(` ${pc13.dim(`drops ${entry.kind}`)}`);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
log2(
|
|
4513
|
+
[
|
|
4514
|
+
"",
|
|
4515
|
+
"There are two reasons Prisma writes a drop, and only you can tell them apart:",
|
|
4516
|
+
"",
|
|
4517
|
+
` ${pc13.bold("You asked for it")} \u2014 a field left a descriptor, or a resource was removed. Then the drop is`,
|
|
4518
|
+
` the point, and ${pc13.bold("flare migrate")} applies it.`,
|
|
4519
|
+
"",
|
|
4520
|
+
` ${pc13.bold("Prisma cannot see it")} \u2014 the column, index or constraint was added by hand, so the schema`,
|
|
4521
|
+
" does not describe it and the diff reads it as something to remove. Then applying this",
|
|
4522
|
+
" loses it, and Prisma will not put it back.",
|
|
4523
|
+
"",
|
|
4524
|
+
`Read ${pc13.bold(destructive[0].file)} before deciding. To keep an object Prisma does not`,
|
|
4525
|
+
"know about, delete those statements from the migration \u2014 and consider writing the object into",
|
|
4526
|
+
"a migration of its own so the next diff stops proposing to drop it."
|
|
4527
|
+
].join("\n")
|
|
4528
|
+
);
|
|
4529
|
+
return written;
|
|
4530
|
+
}
|
|
4531
|
+
log2(`
|
|
4532
|
+
Nothing destructive in it. Next: ${pc13.bold("flare migrate")} to apply it.`);
|
|
4533
|
+
return written;
|
|
4534
|
+
}
|
|
4355
4535
|
|
|
4356
4536
|
// src/commands/gen-policy.ts
|
|
4357
4537
|
import { existsSync as existsSync21, mkdirSync as mkdirSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
@@ -4429,7 +4609,7 @@ ${pc14.dim("Roles come from `flare role:add`; the API and the admin both enforce
|
|
|
4429
4609
|
}
|
|
4430
4610
|
|
|
4431
4611
|
// src/commands/migrate.ts
|
|
4432
|
-
import { existsSync as existsSync22, mkdtempSync, readFileSync as
|
|
4612
|
+
import { existsSync as existsSync22, mkdtempSync, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
4433
4613
|
import { tmpdir } from "os";
|
|
4434
4614
|
import { join as join21 } from "path";
|
|
4435
4615
|
import pc15 from "picocolors";
|
|
@@ -4570,7 +4750,7 @@ async function rollback(options2 = {}) {
|
|
|
4570
4750
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
4571
4751
|
const handWritten = join21(migrationsDir, "down", name);
|
|
4572
4752
|
if (existsSync22(handWritten)) {
|
|
4573
|
-
const sql2 =
|
|
4753
|
+
const sql2 = readFileSync14(handWritten, "utf8");
|
|
4574
4754
|
if (splitStatements(sql2).length === 0) {
|
|
4575
4755
|
throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
|
|
4576
4756
|
}
|
|
@@ -4579,7 +4759,7 @@ async function rollback(options2 = {}) {
|
|
|
4579
4759
|
}
|
|
4580
4760
|
const upPath = join21(migrationsDir, name);
|
|
4581
4761
|
if (!existsSync22(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
4582
|
-
const down = deriveDown(
|
|
4762
|
+
const down = deriveDown(readFileSync14(upPath, "utf8"));
|
|
4583
4763
|
if (!down.reversible) {
|
|
4584
4764
|
throw new Error(
|
|
4585
4765
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -4615,7 +4795,7 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
4615
4795
|
}
|
|
4616
4796
|
|
|
4617
4797
|
// src/commands/rm.ts
|
|
4618
|
-
import { existsSync as existsSync23, readdirSync as
|
|
4798
|
+
import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
4619
4799
|
import { dirname as dirname11, join as join22 } from "path";
|
|
4620
4800
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
4621
4801
|
import pc16 from "picocolors";
|
|
@@ -4638,7 +4818,7 @@ function removeEmptyDirs(appRoot, start) {
|
|
|
4638
4818
|
let dir = dirname11(join22(appRoot, start));
|
|
4639
4819
|
const roots = [join22(appRoot, "app", "api"), join22(appRoot, "app", "dashboard")];
|
|
4640
4820
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
4641
|
-
while (inside() && existsSync23(dir) &&
|
|
4821
|
+
while (inside() && existsSync23(dir) && readdirSync8(dir).length === 0) {
|
|
4642
4822
|
rmdirSync(dir);
|
|
4643
4823
|
dir = dirname11(dir);
|
|
4644
4824
|
}
|
|
@@ -4676,7 +4856,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
4676
4856
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
4677
4857
|
const findings = [];
|
|
4678
4858
|
for (const path of owned) {
|
|
4679
|
-
const source =
|
|
4859
|
+
const source = readFileSync15(join22(appRoot, path), "utf8");
|
|
4680
4860
|
let wrapper;
|
|
4681
4861
|
if (entry && path === entry.file) {
|
|
4682
4862
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -4704,8 +4884,8 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
4704
4884
|
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
4705
4885
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
4706
4886
|
if (table && existsSync23(join22(appRoot, "seeds"))) {
|
|
4707
|
-
const seeds =
|
|
4708
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
4887
|
+
const seeds = readdirSync8(join22(appRoot, "seeds")).filter(
|
|
4888
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync15(join22(appRoot, "seeds", file), "utf8"))
|
|
4709
4889
|
);
|
|
4710
4890
|
if (seeds.length) log2(pc16.yellow(`
|
|
4711
4891
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -4780,7 +4960,7 @@ ${result.output.trim()}`);
|
|
|
4780
4960
|
}
|
|
4781
4961
|
|
|
4782
4962
|
// src/commands/eject.ts
|
|
4783
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as
|
|
4963
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync9, writeFileSync as writeFileSync16 } from "fs";
|
|
4784
4964
|
import { dirname as dirname12, join as join23, relative as relative2 } from "path";
|
|
4785
4965
|
import pc18 from "picocolors";
|
|
4786
4966
|
var TRACKED = ["app", "lib", "components"];
|
|
@@ -4790,7 +4970,7 @@ var prunedOn = (path, stack) => stack === "next" && CLOUDFLARE_ONLY.some((only)
|
|
|
4790
4970
|
function upstreamFile(path, stack) {
|
|
4791
4971
|
if (prunedOn(path, stack) || WRITTEN_PER_APP.has(path)) return void 0;
|
|
4792
4972
|
const found = locate(path, stack);
|
|
4793
|
-
if (found && PER_APP.test(
|
|
4973
|
+
if (found && PER_APP.test(readFileSync16(found, "utf8"))) return void 0;
|
|
4794
4974
|
return found;
|
|
4795
4975
|
}
|
|
4796
4976
|
function locate(path, stack) {
|
|
@@ -4801,7 +4981,7 @@ function trackedPaths(appRoot, stack) {
|
|
|
4801
4981
|
const paths = /* @__PURE__ */ new Set();
|
|
4802
4982
|
const walk = (base, dir) => {
|
|
4803
4983
|
if (!existsSync24(join23(base, dir))) return;
|
|
4804
|
-
for (const entry of
|
|
4984
|
+
for (const entry of readdirSync9(join23(base, dir), { withFileTypes: true })) {
|
|
4805
4985
|
const next = `${dir}/${entry.name}`;
|
|
4806
4986
|
if (entry.isDirectory()) walk(base, next);
|
|
4807
4987
|
else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) paths.add(next);
|
|
@@ -4817,10 +4997,10 @@ function trackedPaths(appRoot, stack) {
|
|
|
4817
4997
|
function compareTracked(appRoot, stack, filter) {
|
|
4818
4998
|
return trackedPaths(appRoot, stack).filter((path) => !filter || path.includes(filter)).map((path) => {
|
|
4819
4999
|
const source = upstreamFile(path, stack);
|
|
4820
|
-
const theirs =
|
|
5000
|
+
const theirs = readFileSync16(source, "utf8");
|
|
4821
5001
|
const target = join23(appRoot, path);
|
|
4822
5002
|
if (!existsSync24(target)) return { path, state: "missing", theirs };
|
|
4823
|
-
const mine =
|
|
5003
|
+
const mine = readFileSync16(target, "utf8");
|
|
4824
5004
|
if (mine === theirs) return { path, state: "same", mine, theirs };
|
|
4825
5005
|
const merged = mergeGeneratedBlock(mine, theirs);
|
|
4826
5006
|
if (merged !== void 0) {
|
|
@@ -4912,9 +5092,9 @@ Updated ${changed.length} file(s).`));
|
|
|
4912
5092
|
}
|
|
4913
5093
|
|
|
4914
5094
|
// src/commands/seed.ts
|
|
4915
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as
|
|
5095
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync10, rmSync as rmSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
4916
5096
|
import { join as join25 } from "path";
|
|
4917
|
-
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
5097
|
+
import { camelCase as camelCase6, createFake, credentialUserRows, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
4918
5098
|
import { createJiti as createJiti4 } from "jiti";
|
|
4919
5099
|
import pc19 from "picocolors";
|
|
4920
5100
|
|
|
@@ -5010,7 +5190,7 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
5010
5190
|
}
|
|
5011
5191
|
|
|
5012
5192
|
// src/seed/local-d1.ts
|
|
5013
|
-
import { readFileSync as
|
|
5193
|
+
import { readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync17 } from "fs";
|
|
5014
5194
|
import { createRequire } from "module";
|
|
5015
5195
|
import { join as join24 } from "path";
|
|
5016
5196
|
import { pathToFileURL } from "url";
|
|
@@ -5043,7 +5223,7 @@ async function openLocalD1(appRoot, binding) {
|
|
|
5043
5223
|
}
|
|
5044
5224
|
function seedConfig(appRoot) {
|
|
5045
5225
|
const source = join24(appRoot, "wrangler.jsonc");
|
|
5046
|
-
const config = parseJsonc3(
|
|
5226
|
+
const config = parseJsonc3(readFileSync17(source, "utf8")) ?? {};
|
|
5047
5227
|
delete config.durable_objects;
|
|
5048
5228
|
delete config.migrations;
|
|
5049
5229
|
const path = join24(appRoot, ".flare-seed.wrangler.json");
|
|
@@ -5055,7 +5235,7 @@ function seedConfig(appRoot) {
|
|
|
5055
5235
|
var SEEDS_DIR = "seeds";
|
|
5056
5236
|
function seedFiles(appRoot, names) {
|
|
5057
5237
|
const dir = join25(appRoot, SEEDS_DIR);
|
|
5058
|
-
const all = existsSync25(dir) ?
|
|
5238
|
+
const all = existsSync25(dir) ? readdirSync10(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
5059
5239
|
if (!names?.length) return all;
|
|
5060
5240
|
return names.map((name) => {
|
|
5061
5241
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -5085,11 +5265,12 @@ async function runSeeds(options2 = {}) {
|
|
|
5085
5265
|
try {
|
|
5086
5266
|
const db = drizzle(local.db, { schema });
|
|
5087
5267
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
5268
|
+
const seedUser = drizzleSeedUser(db, schema);
|
|
5088
5269
|
for (const file of files) {
|
|
5089
5270
|
const mod = await jiti.import(join25(appRoot, SEEDS_DIR, file));
|
|
5090
5271
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
5091
5272
|
log2(`${pc19.cyan("seed")} ${file}`);
|
|
5092
|
-
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
5273
|
+
await mod.default({ db, env: local.env, insertMany, seedUser, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
5093
5274
|
}
|
|
5094
5275
|
log2(pc19.green(`
|
|
5095
5276
|
Ran ${files.length} seed(s) against the local database.`));
|
|
@@ -5097,6 +5278,17 @@ Ran ${files.length} seed(s) against the local database.`));
|
|
|
5097
5278
|
await local.dispose();
|
|
5098
5279
|
}
|
|
5099
5280
|
}
|
|
5281
|
+
function drizzleSeedUser(db, schema) {
|
|
5282
|
+
return async (input) => {
|
|
5283
|
+
const user = schema.user;
|
|
5284
|
+
const account = schema.account;
|
|
5285
|
+
if (!user || !account) throw new Error("db/schema.ts has no `user` and `account` tables, so there is nothing to sign in against.");
|
|
5286
|
+
const rows = await credentialUserRows(input);
|
|
5287
|
+
await db.insert(user).values(rows.user);
|
|
5288
|
+
await db.insert(account).values(rows.account);
|
|
5289
|
+
return rows.user;
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
5100
5292
|
function sampleValue(key, def) {
|
|
5101
5293
|
const name = key.toLowerCase();
|
|
5102
5294
|
switch (def.kind) {
|
|
@@ -5144,18 +5336,22 @@ function renderSeed(resource, stack = "cloudflare") {
|
|
|
5144
5336
|
if (!resource) {
|
|
5145
5337
|
return stack === "next" ? `import { defineSeed } from "@flaredev/core";
|
|
5146
5338
|
|
|
5147
|
-
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
5339
|
+
export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
|
|
5148
5340
|
// \`db\` is this app's Prisma client, e.g.:
|
|
5149
5341
|
// await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
|
|
5150
5342
|
// Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
|
|
5343
|
+
// Somebody you can sign in as, with the credential account Better Auth looks for:
|
|
5344
|
+
// await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
|
|
5151
5345
|
log("nothing to seed yet");
|
|
5152
5346
|
});
|
|
5153
5347
|
` : `import { defineSeed } from "@flaredev/core";
|
|
5154
5348
|
|
|
5155
|
-
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
5349
|
+
export default defineSeed(async ({ db, insertMany, seedUser, fake, log }) => {
|
|
5156
5350
|
// Insert rows with Drizzle, e.g.:
|
|
5157
5351
|
// await db.insert(contacts).values([{ name: "Ada", email: "ada@example.com" }]);
|
|
5158
5352
|
// Or many at once, batched: insertMany(contacts, 10_000, () => ({ name: fake.fullName() }));
|
|
5353
|
+
// Somebody you can sign in as, with the credential account Better Auth looks for:
|
|
5354
|
+
// await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
|
|
5159
5355
|
log("nothing to seed yet");
|
|
5160
5356
|
});
|
|
5161
5357
|
`;
|
|
@@ -5228,7 +5424,7 @@ async function runPrismaSeeds(appRoot, files, log2) {
|
|
|
5228
5424
|
const runner = join25(appRoot, ".flare-seed-runner.mts");
|
|
5229
5425
|
const script = [
|
|
5230
5426
|
`import "dotenv/config";`,
|
|
5231
|
-
`import { createFake } from "@flaredev/core";`,
|
|
5427
|
+
`import { createFake, credentialUserRows } from "@flaredev/core";`,
|
|
5232
5428
|
`import { prisma } from "./lib/db";`,
|
|
5233
5429
|
...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
|
|
5234
5430
|
"",
|
|
@@ -5246,6 +5442,13 @@ async function runPrismaSeeds(appRoot, files, log2) {
|
|
|
5246
5442
|
` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
|
|
5247
5443
|
` await delegate.createMany({ data: rows });`,
|
|
5248
5444
|
` },`,
|
|
5445
|
+
` seedUser: async (input: Parameters<typeof credentialUserRows>[0]) => {`,
|
|
5446
|
+
` const rows = await credentialUserRows(input);`,
|
|
5447
|
+
` const models = prisma as unknown as Record<string, { create(args: { data: unknown }): Promise<unknown> }>;`,
|
|
5448
|
+
` await models.user.create({ data: rows.user });`,
|
|
5449
|
+
` await models.account.create({ data: rows.account });`,
|
|
5450
|
+
` return rows.user;`,
|
|
5451
|
+
` },`,
|
|
5249
5452
|
` fake: createFake(),`,
|
|
5250
5453
|
` log: (message: string) => console.log(" " + message),`,
|
|
5251
5454
|
` });`,
|
|
@@ -5381,7 +5584,7 @@ import { join as join27 } from "path";
|
|
|
5381
5584
|
import pc21 from "picocolors";
|
|
5382
5585
|
|
|
5383
5586
|
// src/seed/rows.ts
|
|
5384
|
-
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
5587
|
+
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4, toMinorUnits } from "@flaredev/core";
|
|
5385
5588
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
5386
5589
|
function stringValue(key, def, fake, resource, index) {
|
|
5387
5590
|
switch (def.format) {
|
|
@@ -5435,7 +5638,8 @@ function numberValue(key, def, fake) {
|
|
|
5435
5638
|
min = min ?? low;
|
|
5436
5639
|
max = max ?? high;
|
|
5437
5640
|
}
|
|
5438
|
-
|
|
5641
|
+
const amount = def.kind === "int" ? fake.int(Math.ceil(min), Math.floor(max)) : fake.float(min, max, 2);
|
|
5642
|
+
return def.format === "money" ? toMinorUnits(amount, def.currency) : amount;
|
|
5439
5643
|
}
|
|
5440
5644
|
function fieldValue(key, def, fake, resource, parentIds, index) {
|
|
5441
5645
|
const name = key.toLowerCase();
|
|
@@ -5665,7 +5869,7 @@ ${result.output.trim()}`);
|
|
|
5665
5869
|
|
|
5666
5870
|
// src/commands/billing-sync.ts
|
|
5667
5871
|
import { createRequire as createRequire2 } from "module";
|
|
5668
|
-
import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as
|
|
5872
|
+
import { existsSync as existsSync26, mkdtempSync as mkdtempSync4, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync21 } from "fs";
|
|
5669
5873
|
import { tmpdir as tmpdir4 } from "os";
|
|
5670
5874
|
import { join as join28 } from "path";
|
|
5671
5875
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
@@ -5675,7 +5879,7 @@ function stripeKeys(appRoot) {
|
|
|
5675
5879
|
const map = {};
|
|
5676
5880
|
const path = join28(appRoot, ".dev.vars");
|
|
5677
5881
|
if (existsSync26(path)) {
|
|
5678
|
-
for (const line of
|
|
5882
|
+
for (const line of readFileSync18(path, "utf8").split(/\r?\n/)) {
|
|
5679
5883
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
5680
5884
|
if (match) map[match[1]] = match[2].trim();
|
|
5681
5885
|
}
|
|
@@ -5761,7 +5965,7 @@ async function syncPlans(options2 = {}) {
|
|
|
5761
5965
|
const appRequire = createRequire2(join28(appRoot, "package.json"));
|
|
5762
5966
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
5763
5967
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
5764
|
-
const appName = JSON.parse(
|
|
5968
|
+
const appName = JSON.parse(readFileSync18(join28(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
5765
5969
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
5766
5970
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
5767
5971
|
if (products.length === 0) {
|
|
@@ -5936,13 +6140,13 @@ async function setUserRoleOnPrisma(appRoot, email, role, log2) {
|
|
|
5936
6140
|
|
|
5937
6141
|
// src/utils/install.ts
|
|
5938
6142
|
import { spawn as spawn5 } from "child_process";
|
|
5939
|
-
import { existsSync as existsSync28, readFileSync as
|
|
6143
|
+
import { existsSync as existsSync28, readFileSync as readFileSync19, readdirSync as readdirSync11 } from "fs";
|
|
5940
6144
|
import { join as join31 } from "path";
|
|
5941
6145
|
function expectedPackages(dir) {
|
|
5942
6146
|
const npmLock = join31(dir, "package-lock.json");
|
|
5943
6147
|
if (existsSync28(npmLock)) {
|
|
5944
6148
|
try {
|
|
5945
|
-
const lock = JSON.parse(
|
|
6149
|
+
const lock = JSON.parse(readFileSync19(npmLock, "utf8"));
|
|
5946
6150
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
5947
6151
|
} catch {
|
|
5948
6152
|
return 0;
|
|
@@ -5951,7 +6155,7 @@ function expectedPackages(dir) {
|
|
|
5951
6155
|
const pnpmLock = join31(dir, "pnpm-lock.yaml");
|
|
5952
6156
|
if (existsSync28(pnpmLock)) {
|
|
5953
6157
|
try {
|
|
5954
|
-
const text =
|
|
6158
|
+
const text = readFileSync19(pnpmLock, "utf8");
|
|
5955
6159
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
5956
6160
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
5957
6161
|
} catch {
|
|
@@ -5965,11 +6169,11 @@ function installedPackages(dir) {
|
|
|
5965
6169
|
if (!existsSync28(root)) return 0;
|
|
5966
6170
|
let total = 0;
|
|
5967
6171
|
try {
|
|
5968
|
-
for (const entry of
|
|
6172
|
+
for (const entry of readdirSync11(root, { withFileTypes: true })) {
|
|
5969
6173
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
5970
6174
|
if (entry.name.startsWith("@")) {
|
|
5971
6175
|
try {
|
|
5972
|
-
total +=
|
|
6176
|
+
total += readdirSync11(join31(root, entry.name)).length;
|
|
5973
6177
|
} catch {
|
|
5974
6178
|
}
|
|
5975
6179
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -6051,7 +6255,7 @@ function createCli() {
|
|
|
6051
6255
|
printNextSteps(result, installed);
|
|
6052
6256
|
}
|
|
6053
6257
|
);
|
|
6054
|
-
cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
|
|
6258
|
+
cli.command("gen <generator> [name] [second]", `Generate code. Generators: ${GENERATORS.join(", ")}`).option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--soft-delete", "resource: keep deleted rows and hide them, with a Trash view in the dashboard").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration (Cloudflare only)").example("flare gen migration add_phone --from-schema # Cloudflare: diff the Drizzle tables").example("flare gen migration add_phone # Next.js: prisma migrate dev, refusing a drop").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--own <field>", "policy: confine each user to rows whose belongsTo(User) field holds their id, e.g. --own userId").option("--own-except <roles>", "policy: roles that see every row despite --own, e.g. admin").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing/apikeys: write files without adding the dependency").option("--skip-migration", "billing/security/apikeys: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen resource Invoice --fields 'number:string!' --soft-delete # a delete goes to the Trash").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen policy Invoice --roles staff --own userId --own-except admin # each user sees only their own").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").example("flare gen apikeys # API keys for cron jobs, scripts and mobile apps").example("flare gen apikeys # bearer keys for crons, scripts and mobile apps").action(
|
|
6055
6259
|
async (generator, name, second, options2) => {
|
|
6056
6260
|
if (generator === "billing")
|
|
6057
6261
|
return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flaredev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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.
|
|
44
|
+
"@flaredev/core": "0.10.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22"
|
|
@@ -96,8 +96,8 @@ export function DashboardSidebar({
|
|
|
96
96
|
</SidebarMenu>
|
|
97
97
|
</SidebarHeader>
|
|
98
98
|
|
|
99
|
-
<SidebarContent>
|
|
100
|
-
<SidebarGroup>
|
|
99
|
+
<SidebarContent className="gap-0">
|
|
100
|
+
<SidebarGroup className="px-2 py-1">
|
|
101
101
|
<SidebarGroupContent>
|
|
102
102
|
<SidebarMenu>
|
|
103
103
|
<SidebarMenuItem>
|
|
@@ -113,8 +113,8 @@ export function DashboardSidebar({
|
|
|
113
113
|
</SidebarGroup>
|
|
114
114
|
|
|
115
115
|
{byGroup(resources).map(([heading, group]) => (
|
|
116
|
-
<SidebarGroup key={heading}>
|
|
117
|
-
<SidebarGroupLabel>{heading}</SidebarGroupLabel>
|
|
116
|
+
<SidebarGroup key={heading} className="px-2 py-1">
|
|
117
|
+
<SidebarGroupLabel className="mt-1 h-6">{heading}</SidebarGroupLabel>
|
|
118
118
|
<SidebarGroupContent>
|
|
119
119
|
<SidebarMenu>
|
|
120
120
|
{group.map((resource) => {
|
|
@@ -149,8 +149,8 @@ export function DashboardSidebar({
|
|
|
149
149
|
</SidebarGroup>
|
|
150
150
|
))}
|
|
151
151
|
|
|
152
|
-
<SidebarGroup>
|
|
153
|
-
<SidebarGroupLabel>Manage</SidebarGroupLabel>
|
|
152
|
+
<SidebarGroup className="px-2 py-1">
|
|
153
|
+
<SidebarGroupLabel className="mt-1 h-6">Manage</SidebarGroupLabel>
|
|
154
154
|
<SidebarGroupContent>
|
|
155
155
|
<SidebarMenu>
|
|
156
156
|
{manage.map((link) => (
|
|
@@ -168,8 +168,8 @@ export function DashboardSidebar({
|
|
|
168
168
|
</SidebarGroup>
|
|
169
169
|
|
|
170
170
|
{links.length > 0 && (
|
|
171
|
-
<SidebarGroup>
|
|
172
|
-
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
|
171
|
+
<SidebarGroup className="px-2 py-1">
|
|
172
|
+
<SidebarGroupLabel className="mt-1 h-6">Platform</SidebarGroupLabel>
|
|
173
173
|
<SidebarGroupContent>
|
|
174
174
|
<SidebarMenu>
|
|
175
175
|
{links.map((link) => {
|