spfn 0.2.0-beta.60 → 0.2.0-beta.61
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 +705 -555
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -869,7 +869,7 @@ var init_deployment_config = __esm({
|
|
|
869
869
|
|
|
870
870
|
// src/utils/version.ts
|
|
871
871
|
function getCliVersion() {
|
|
872
|
-
return "0.2.0-beta.
|
|
872
|
+
return "0.2.0-beta.61";
|
|
873
873
|
}
|
|
874
874
|
function getTagFromVersion(version) {
|
|
875
875
|
const match = version.match(/-([a-z]+)\./i);
|
|
@@ -1342,39 +1342,39 @@ __export(function_migrations_exports, {
|
|
|
1342
1342
|
loadFunctionMigrationPlans: () => loadFunctionMigrationPlans,
|
|
1343
1343
|
readMigrationEntries: () => readMigrationEntries
|
|
1344
1344
|
});
|
|
1345
|
-
import
|
|
1345
|
+
import chalk6 from "chalk";
|
|
1346
1346
|
import { createHash } from "crypto";
|
|
1347
|
-
import { join as
|
|
1348
|
-
import { env
|
|
1349
|
-
import { loadEnv
|
|
1350
|
-
import { existsSync as
|
|
1347
|
+
import { join as join15 } from "path";
|
|
1348
|
+
import { env } from "@spfn/core/config";
|
|
1349
|
+
import { loadEnv } from "@spfn/core/server";
|
|
1350
|
+
import { existsSync as existsSync14, readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
1351
1351
|
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1352
|
-
const nodeModulesPath =
|
|
1353
|
-
if (!
|
|
1352
|
+
const nodeModulesPath = join15(cwd, "node_modules");
|
|
1353
|
+
if (!existsSync14(nodeModulesPath)) {
|
|
1354
1354
|
return [];
|
|
1355
1355
|
}
|
|
1356
1356
|
const functions = [];
|
|
1357
|
-
const spfnDir =
|
|
1358
|
-
if (!
|
|
1357
|
+
const spfnDir = join15(nodeModulesPath, "@spfn");
|
|
1358
|
+
if (!existsSync14(spfnDir)) {
|
|
1359
1359
|
return [];
|
|
1360
1360
|
}
|
|
1361
|
-
const packages =
|
|
1361
|
+
const packages = readdirSync(spfnDir);
|
|
1362
1362
|
for (const pkg of packages) {
|
|
1363
|
-
const packagePath =
|
|
1364
|
-
const packageJsonPath =
|
|
1365
|
-
if (!
|
|
1363
|
+
const packagePath = join15(spfnDir, pkg);
|
|
1364
|
+
const packageJsonPath = join15(packagePath, "package.json");
|
|
1365
|
+
if (!existsSync14(packageJsonPath)) {
|
|
1366
1366
|
continue;
|
|
1367
1367
|
}
|
|
1368
1368
|
try {
|
|
1369
|
-
const packageJson = JSON.parse(
|
|
1369
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
|
|
1370
1370
|
const spfnConfig = packageJson.spfn;
|
|
1371
1371
|
if (!spfnConfig?.migrations) {
|
|
1372
1372
|
continue;
|
|
1373
1373
|
}
|
|
1374
|
-
const migrationsDir =
|
|
1375
|
-
if (!
|
|
1374
|
+
const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
|
|
1375
|
+
if (!existsSync14(migrationsDir)) {
|
|
1376
1376
|
console.warn(
|
|
1377
|
-
|
|
1377
|
+
chalk6.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
|
|
1378
1378
|
);
|
|
1379
1379
|
continue;
|
|
1380
1380
|
}
|
|
@@ -1384,14 +1384,14 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
|
1384
1384
|
packagePath
|
|
1385
1385
|
});
|
|
1386
1386
|
} catch (error) {
|
|
1387
|
-
console.warn(
|
|
1387
|
+
console.warn(chalk6.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
|
|
1388
1388
|
}
|
|
1389
1389
|
}
|
|
1390
1390
|
return functions;
|
|
1391
1391
|
}
|
|
1392
1392
|
function readMigrationEntries(migrationsDir, packageName) {
|
|
1393
|
-
const journalPath =
|
|
1394
|
-
return
|
|
1393
|
+
const journalPath = join15(migrationsDir, "meta", "_journal.json");
|
|
1394
|
+
return existsSync14(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
|
|
1395
1395
|
}
|
|
1396
1396
|
function loadFunctionMigrationPlans(functionMigrations) {
|
|
1397
1397
|
return functionMigrations.map((func) => ({
|
|
@@ -1402,7 +1402,7 @@ function loadFunctionMigrationPlans(functionMigrations) {
|
|
|
1402
1402
|
function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
1403
1403
|
let journal;
|
|
1404
1404
|
try {
|
|
1405
|
-
journal = JSON.parse(
|
|
1405
|
+
journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
|
|
1406
1406
|
} catch {
|
|
1407
1407
|
journal = {};
|
|
1408
1408
|
}
|
|
@@ -1415,18 +1415,18 @@ function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
|
1415
1415
|
if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
|
|
1416
1416
|
throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
|
|
1417
1417
|
}
|
|
1418
|
-
const sqlPath =
|
|
1419
|
-
if (!
|
|
1418
|
+
const sqlPath = join15(migrationsDir, `${entry.tag}.sql`);
|
|
1419
|
+
if (!existsSync14(sqlPath)) {
|
|
1420
1420
|
throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
|
|
1421
1421
|
}
|
|
1422
|
-
return toEntry(entry.tag,
|
|
1422
|
+
return toEntry(entry.tag, readFileSync7(sqlPath, "utf-8"), entry.when);
|
|
1423
1423
|
});
|
|
1424
1424
|
}
|
|
1425
1425
|
function readFolderEntries(migrationsDir, packageName) {
|
|
1426
|
-
const folders =
|
|
1426
|
+
const folders = readdirSync(migrationsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).filter((name) => existsSync14(join15(migrationsDir, name, "migration.sql"))).sort((a, b) => a.localeCompare(b));
|
|
1427
1427
|
return folders.map((name) => toEntry(
|
|
1428
1428
|
name,
|
|
1429
|
-
|
|
1429
|
+
readFileSync7(join15(migrationsDir, name, "migration.sql"), "utf-8"),
|
|
1430
1430
|
folderTimestampMillis(name, packageName)
|
|
1431
1431
|
));
|
|
1432
1432
|
}
|
|
@@ -1466,7 +1466,7 @@ async function migrateLegacyTable(db, plans) {
|
|
|
1466
1466
|
if (!legacyCheck[0]?.exists) {
|
|
1467
1467
|
return;
|
|
1468
1468
|
}
|
|
1469
|
-
console.log(
|
|
1469
|
+
console.log(chalk6.dim("\n Migrating legacy shared migration table to per-package tables..."));
|
|
1470
1470
|
const legacyRows = await db.query(
|
|
1471
1471
|
`SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
|
|
1472
1472
|
);
|
|
@@ -1496,11 +1496,11 @@ async function migrateLegacyTable(db, plans) {
|
|
|
1496
1496
|
copied++;
|
|
1497
1497
|
}
|
|
1498
1498
|
if (copied > 0) {
|
|
1499
|
-
console.log(
|
|
1499
|
+
console.log(chalk6.dim(` \u2713 ${plan.packageName}: copied ${copied} migration record(s)`));
|
|
1500
1500
|
}
|
|
1501
1501
|
}
|
|
1502
1502
|
await db.query(`DROP TABLE drizzle."__spfn_fn_migrations"`);
|
|
1503
|
-
console.log(
|
|
1503
|
+
console.log(chalk6.dim(" \u2713 Legacy migration table removed\n"));
|
|
1504
1504
|
}
|
|
1505
1505
|
async function ensureMigrationsTable(db, tableName) {
|
|
1506
1506
|
await db.query(`CREATE SCHEMA IF NOT EXISTS drizzle`);
|
|
@@ -1544,20 +1544,20 @@ function createPostgresJsMigrationDb(client) {
|
|
|
1544
1544
|
}
|
|
1545
1545
|
async function executeFunctionMigrations(plans) {
|
|
1546
1546
|
const postgres = await import("postgres");
|
|
1547
|
-
|
|
1548
|
-
if (!
|
|
1547
|
+
loadEnv();
|
|
1548
|
+
if (!env.DATABASE_URL) {
|
|
1549
1549
|
throw new Error("DATABASE_URL not found in environment");
|
|
1550
1550
|
}
|
|
1551
|
-
const connection = postgres.default(
|
|
1551
|
+
const connection = postgres.default(env.DATABASE_URL, { max: 1 });
|
|
1552
1552
|
const db = createPostgresJsMigrationDb(connection);
|
|
1553
1553
|
let appliedCount = 0;
|
|
1554
1554
|
try {
|
|
1555
1555
|
await migrateLegacyTable(db, plans);
|
|
1556
1556
|
for (const plan of plans) {
|
|
1557
|
-
console.log(
|
|
1557
|
+
console.log(chalk6.blue(`
|
|
1558
1558
|
\u{1F4E6} Running ${plan.packageName} migrations...`));
|
|
1559
1559
|
const applied = await applyFunctionMigrationPlan(db, plan);
|
|
1560
|
-
console.log(applied > 0 ?
|
|
1560
|
+
console.log(applied > 0 ? chalk6.green(` \u2713 ${plan.packageName}: ${applied} migration(s) applied`) : chalk6.dim(` \u2013 ${plan.packageName}: up to date`));
|
|
1561
1561
|
appliedCount += applied;
|
|
1562
1562
|
}
|
|
1563
1563
|
} finally {
|
|
@@ -1571,6 +1571,112 @@ var init_function_migrations = __esm({
|
|
|
1571
1571
|
}
|
|
1572
1572
|
});
|
|
1573
1573
|
|
|
1574
|
+
// src/utils/migration-status.ts
|
|
1575
|
+
var migration_status_exports = {};
|
|
1576
|
+
__export(migration_status_exports, {
|
|
1577
|
+
filterPendingEntries: () => filterPendingEntries,
|
|
1578
|
+
functionMigrationsTable: () => functionMigrationsTable2,
|
|
1579
|
+
getMigrationStatus: () => getMigrationStatus,
|
|
1580
|
+
warnPendingMigrations: () => warnPendingMigrations
|
|
1581
|
+
});
|
|
1582
|
+
import { join as join16 } from "path";
|
|
1583
|
+
import { existsSync as existsSync15 } from "fs";
|
|
1584
|
+
import chalk7 from "chalk";
|
|
1585
|
+
import { env as env2 } from "@spfn/core/config";
|
|
1586
|
+
import { loadEnv as loadEnv2 } from "@spfn/core/server";
|
|
1587
|
+
function functionMigrationsTable2(packageName) {
|
|
1588
|
+
return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
|
|
1589
|
+
}
|
|
1590
|
+
function filterPendingEntries(entries, lastAppliedMillis, appliedNames) {
|
|
1591
|
+
return entries.filter((entry) => entry.millis > lastAppliedMillis && !appliedNames.has(entry.name));
|
|
1592
|
+
}
|
|
1593
|
+
async function readAppliedNames(sql2, tableName) {
|
|
1594
|
+
const columnCheck = await sql2`
|
|
1595
|
+
SELECT EXISTS (
|
|
1596
|
+
SELECT 1 FROM information_schema.columns
|
|
1597
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName} AND column_name = 'name'
|
|
1598
|
+
) AS "exists"`;
|
|
1599
|
+
if (!columnCheck[0]?.exists) {
|
|
1600
|
+
return /* @__PURE__ */ new Set();
|
|
1601
|
+
}
|
|
1602
|
+
const rows = await sql2`
|
|
1603
|
+
SELECT name FROM drizzle.${sql2(tableName)} WHERE name IS NOT NULL`;
|
|
1604
|
+
return new Set(rows.map((row) => row.name));
|
|
1605
|
+
}
|
|
1606
|
+
async function collectTargetStatus(sql2, name, migrationsDir, tableName) {
|
|
1607
|
+
const entries = readMigrationEntries(migrationsDir, name);
|
|
1608
|
+
const tableCheck = await sql2`
|
|
1609
|
+
SELECT EXISTS (
|
|
1610
|
+
SELECT 1 FROM information_schema.tables
|
|
1611
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName}
|
|
1612
|
+
) AS "exists"`;
|
|
1613
|
+
let lastApplied = 0;
|
|
1614
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1615
|
+
if (tableCheck[0]?.exists) {
|
|
1616
|
+
const rows = await sql2`
|
|
1617
|
+
SELECT created_at FROM drizzle.${sql2(tableName)}
|
|
1618
|
+
ORDER BY created_at DESC LIMIT 1`;
|
|
1619
|
+
lastApplied = rows[0]?.created_at ? Number(rows[0].created_at) : 0;
|
|
1620
|
+
appliedNames = await readAppliedNames(sql2, tableName);
|
|
1621
|
+
}
|
|
1622
|
+
const pendingEntries = filterPendingEntries(entries, lastApplied, appliedNames);
|
|
1623
|
+
return {
|
|
1624
|
+
name,
|
|
1625
|
+
total: entries.length,
|
|
1626
|
+
applied: entries.length - pendingEntries.length,
|
|
1627
|
+
pending: pendingEntries.length,
|
|
1628
|
+
pendingTags: pendingEntries.map((e) => e.name)
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
async function getMigrationStatus(cwd = process.cwd(), databaseUrl) {
|
|
1632
|
+
loadEnv2();
|
|
1633
|
+
const url = databaseUrl ?? env2.DATABASE_URL;
|
|
1634
|
+
if (!url) {
|
|
1635
|
+
throw new Error("DATABASE_URL not found in environment");
|
|
1636
|
+
}
|
|
1637
|
+
const postgres = await import("postgres");
|
|
1638
|
+
const sql2 = postgres.default(url, { max: 1, connect_timeout: 5 });
|
|
1639
|
+
try {
|
|
1640
|
+
const packages = [];
|
|
1641
|
+
for (const func of discoverFunctionMigrations(cwd)) {
|
|
1642
|
+
packages.push(await collectTargetStatus(
|
|
1643
|
+
sql2,
|
|
1644
|
+
func.packageName,
|
|
1645
|
+
func.migrationsDir,
|
|
1646
|
+
functionMigrationsTable2(func.packageName)
|
|
1647
|
+
));
|
|
1648
|
+
}
|
|
1649
|
+
const projectDir = join16(cwd, "src", "server", "drizzle");
|
|
1650
|
+
const project = existsSync15(projectDir) ? await collectTargetStatus(sql2, "project (src/server/drizzle)", projectDir, "__drizzle_migrations") : null;
|
|
1651
|
+
return { packages, project: project && project.total > 0 ? project : null };
|
|
1652
|
+
} finally {
|
|
1653
|
+
await sql2.end();
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
async function warnPendingMigrations(cwd, databaseUrl) {
|
|
1657
|
+
let status;
|
|
1658
|
+
try {
|
|
1659
|
+
status = await getMigrationStatus(cwd, databaseUrl);
|
|
1660
|
+
} catch {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const targets = [...status.packages, ...status.project ? [status.project] : []].filter((t) => t.pending > 0);
|
|
1664
|
+
if (targets.length === 0) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
console.warn("");
|
|
1668
|
+
for (const target of targets) {
|
|
1669
|
+
console.warn(chalk7.yellow(`\u26A0\uFE0F ${target.name}: ${target.pending} pending migration(s)`));
|
|
1670
|
+
}
|
|
1671
|
+
console.warn(chalk7.yellow(" Missing tables will fail at request time \u2014 run: pnpm spfn db migrate\n"));
|
|
1672
|
+
}
|
|
1673
|
+
var init_migration_status = __esm({
|
|
1674
|
+
"src/utils/migration-status.ts"() {
|
|
1675
|
+
"use strict";
|
|
1676
|
+
init_function_migrations();
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1574
1680
|
// src/index.ts
|
|
1575
1681
|
import { Command as Command13 } from "commander";
|
|
1576
1682
|
|
|
@@ -1762,8 +1868,8 @@ init_init();
|
|
|
1762
1868
|
init_logger();
|
|
1763
1869
|
init_package_manager();
|
|
1764
1870
|
import { Command as Command4 } from "commander";
|
|
1765
|
-
import { existsSync as
|
|
1766
|
-
import { join as
|
|
1871
|
+
import { existsSync as existsSync16, readFileSync as readFileSync8, writeFileSync as writeFileSync10, mkdirSync, unlinkSync, rmSync, watch } from "fs";
|
|
1872
|
+
import { join as join17, relative, sep } from "path";
|
|
1767
1873
|
import { execa as execa6 } from "execa";
|
|
1768
1874
|
import chokidar from "chokidar";
|
|
1769
1875
|
|
|
@@ -1939,10 +2045,10 @@ async function resolveKeychainEnv(cwd) {
|
|
|
1939
2045
|
return { env: {}, missing: [] };
|
|
1940
2046
|
}
|
|
1941
2047
|
const store = detectStore();
|
|
1942
|
-
const
|
|
2048
|
+
const env9 = {};
|
|
1943
2049
|
const missing = [];
|
|
1944
2050
|
if (!await store.isAvailable()) {
|
|
1945
|
-
return { env:
|
|
2051
|
+
return { env: env9, missing: refs.map(([key]) => key) };
|
|
1946
2052
|
}
|
|
1947
2053
|
for (const [key, value] of refs) {
|
|
1948
2054
|
const name = value.slice(KEYCHAIN_REF_PREFIX.length);
|
|
@@ -1950,10 +2056,10 @@ async function resolveKeychainEnv(cwd) {
|
|
|
1950
2056
|
if (resolved === null) {
|
|
1951
2057
|
missing.push(key);
|
|
1952
2058
|
} else {
|
|
1953
|
-
|
|
2059
|
+
env9[key] = resolved;
|
|
1954
2060
|
}
|
|
1955
2061
|
}
|
|
1956
|
-
return { env:
|
|
2062
|
+
return { env: env9, missing };
|
|
1957
2063
|
}
|
|
1958
2064
|
|
|
1959
2065
|
// src/commands/dev.ts
|
|
@@ -1962,20 +2068,20 @@ function ignoreDotfilesUnder(root) {
|
|
|
1962
2068
|
}
|
|
1963
2069
|
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
1964
2070
|
return new Promise((resolve3, reject) => {
|
|
1965
|
-
if (
|
|
2071
|
+
if (existsSync16(filePath)) {
|
|
1966
2072
|
unlinkSync(filePath);
|
|
1967
2073
|
}
|
|
1968
2074
|
const timer = setTimeout(() => {
|
|
1969
2075
|
watcher.close();
|
|
1970
2076
|
reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
|
|
1971
2077
|
}, timeoutMs);
|
|
1972
|
-
const dir =
|
|
2078
|
+
const dir = join17(filePath, "..");
|
|
1973
2079
|
const fileName = filePath.split("/").pop();
|
|
1974
2080
|
const watcher = watch(dir, (_event, name) => {
|
|
1975
|
-
if (name === fileName &&
|
|
2081
|
+
if (name === fileName && existsSync16(filePath)) {
|
|
1976
2082
|
watcher.close();
|
|
1977
2083
|
clearTimeout(timer);
|
|
1978
|
-
resolve3(
|
|
2084
|
+
resolve3(readFileSync8(filePath, "utf-8").trim());
|
|
1979
2085
|
}
|
|
1980
2086
|
});
|
|
1981
2087
|
});
|
|
@@ -1986,8 +2092,8 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1986
2092
|
process.env.NODE_ENV = "development";
|
|
1987
2093
|
}
|
|
1988
2094
|
const cwd = process.cwd();
|
|
1989
|
-
const serverDir =
|
|
1990
|
-
if (!
|
|
2095
|
+
const serverDir = join17(cwd, "src", "server");
|
|
2096
|
+
if (!existsSync16(serverDir)) {
|
|
1991
2097
|
logger.error("src/server directory not found.");
|
|
1992
2098
|
logger.info('Run "spfn init" first to initialize SPFN in your project.');
|
|
1993
2099
|
process.exit(1);
|
|
@@ -2001,23 +2107,25 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
2001
2107
|
logger.warn(`[SPFN] Could not resolve keychain secret(s): ${keychainMissing.join(", ")} \u2014 run \`spfn secret set <KEY>\``);
|
|
2002
2108
|
}
|
|
2003
2109
|
const serverEnv = { ...process.env, ...keychainEnv };
|
|
2004
|
-
const
|
|
2110
|
+
const { warnPendingMigrations: warnPendingMigrations2 } = await Promise.resolve().then(() => (init_migration_status(), migration_status_exports));
|
|
2111
|
+
await warnPendingMigrations2(cwd, serverEnv.DATABASE_URL);
|
|
2112
|
+
const packageJsonPath = join17(cwd, "package.json");
|
|
2005
2113
|
let hasNext = false;
|
|
2006
|
-
if (
|
|
2007
|
-
const packageJson = JSON.parse(
|
|
2114
|
+
if (existsSync16(packageJsonPath)) {
|
|
2115
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
|
|
2008
2116
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2009
2117
|
}
|
|
2010
|
-
const tempDir =
|
|
2011
|
-
const serverEntry =
|
|
2012
|
-
const watcherEntry =
|
|
2118
|
+
const tempDir = join17(cwd, ".spfn");
|
|
2119
|
+
const serverEntry = join17(tempDir, "server.mjs");
|
|
2120
|
+
const watcherEntry = join17(tempDir, "watcher.mjs");
|
|
2013
2121
|
mkdirSync(tempDir, { recursive: true });
|
|
2014
|
-
const serverBuildDir =
|
|
2015
|
-
if (
|
|
2122
|
+
const serverBuildDir = join17(tempDir, "server");
|
|
2123
|
+
if (existsSync16(serverBuildDir)) {
|
|
2016
2124
|
rmSync(serverBuildDir, { recursive: true, force: true });
|
|
2017
2125
|
logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
|
|
2018
2126
|
}
|
|
2019
|
-
const readySignal =
|
|
2020
|
-
if (
|
|
2127
|
+
const readySignal = join17(tempDir, "server-ready");
|
|
2128
|
+
if (existsSync16(readySignal)) {
|
|
2021
2129
|
unlinkSync(readySignal);
|
|
2022
2130
|
}
|
|
2023
2131
|
const configParts = [];
|
|
@@ -2025,7 +2133,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
2025
2133
|
if (options.host) configParts.push(`host: '${options.host}'`);
|
|
2026
2134
|
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
2027
2135
|
configParts.push("debug: true");
|
|
2028
|
-
const readyFile =
|
|
2136
|
+
const readyFile = join17(tempDir, "server-ready");
|
|
2029
2137
|
writeFileSync10(serverEntry, `
|
|
2030
2138
|
import { writeFileSync } from 'fs';
|
|
2031
2139
|
|
|
@@ -2315,11 +2423,11 @@ catch (error)
|
|
|
2315
2423
|
init_logger();
|
|
2316
2424
|
init_package_manager();
|
|
2317
2425
|
import { Command as Command5 } from "commander";
|
|
2318
|
-
import { existsSync as
|
|
2319
|
-
import { join as
|
|
2426
|
+
import { existsSync as existsSync17, writeFileSync as writeFileSync11, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
|
|
2427
|
+
import { join as join18 } from "path";
|
|
2320
2428
|
import { execa as execa7 } from "execa";
|
|
2321
2429
|
import ora5 from "ora";
|
|
2322
|
-
import
|
|
2430
|
+
import chalk8 from "chalk";
|
|
2323
2431
|
import { build } from "tsup";
|
|
2324
2432
|
async function buildProject(options) {
|
|
2325
2433
|
if (!process.env.NODE_ENV) {
|
|
@@ -2330,17 +2438,17 @@ async function buildProject(options) {
|
|
|
2330
2438
|
}
|
|
2331
2439
|
const cwd = process.cwd();
|
|
2332
2440
|
const pm = detectPackageManager(cwd);
|
|
2333
|
-
const packageJsonPath =
|
|
2441
|
+
const packageJsonPath = join18(cwd, "package.json");
|
|
2334
2442
|
let hasNext = false;
|
|
2335
|
-
if (
|
|
2443
|
+
if (existsSync17(packageJsonPath)) {
|
|
2336
2444
|
const packageJson = JSON.parse(await import("fs").then(
|
|
2337
2445
|
(fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
|
|
2338
2446
|
));
|
|
2339
2447
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2340
2448
|
}
|
|
2341
|
-
const serverDir =
|
|
2342
|
-
const hasServer =
|
|
2343
|
-
console.log(
|
|
2449
|
+
const serverDir = join18(cwd, "src", "server");
|
|
2450
|
+
const hasServer = existsSync17(serverDir);
|
|
2451
|
+
console.log(chalk8.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
|
|
2344
2452
|
if (hasServer) {
|
|
2345
2453
|
const spinner = ora5("Generating API client...").start();
|
|
2346
2454
|
try {
|
|
@@ -2376,10 +2484,10 @@ async function buildProject(options) {
|
|
|
2376
2484
|
if (hasServer && !options.nextOnly) {
|
|
2377
2485
|
const spinner = ora5("Building SPFN server...").start();
|
|
2378
2486
|
try {
|
|
2379
|
-
const outputDir =
|
|
2487
|
+
const outputDir = join18(cwd, ".spfn", "server");
|
|
2380
2488
|
mkdirSync2(outputDir, { recursive: true });
|
|
2381
|
-
const serverDir2 =
|
|
2382
|
-
if (!
|
|
2489
|
+
const serverDir2 = join18(cwd, "src", "server");
|
|
2490
|
+
if (!existsSync17(serverDir2)) {
|
|
2383
2491
|
spinner.fail("SPFN server build failed");
|
|
2384
2492
|
logger.error("src/server/ directory not found");
|
|
2385
2493
|
logger.error('Please run "spfn init" to initialize the project.');
|
|
@@ -2407,7 +2515,7 @@ async function buildProject(options) {
|
|
|
2407
2515
|
onSuccess: async () => {
|
|
2408
2516
|
}
|
|
2409
2517
|
});
|
|
2410
|
-
const prodServerPath =
|
|
2518
|
+
const prodServerPath = join18(cwd, ".spfn", "prod-server.mjs");
|
|
2411
2519
|
const prodServerContent = `// Load environment variables FIRST (before any imports that depend on them)
|
|
2412
2520
|
// Use centralized environment loader for standard dotenv priority
|
|
2413
2521
|
const { env } = await import('@spfn/core/config');
|
|
@@ -2433,25 +2541,25 @@ await startServer({
|
|
|
2433
2541
|
`;
|
|
2434
2542
|
writeFileSync11(prodServerPath, prodServerContent);
|
|
2435
2543
|
spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
|
|
2436
|
-
const routesDir =
|
|
2437
|
-
if (
|
|
2544
|
+
const routesDir = join18(cwd, ".spfn", "server", "routes");
|
|
2545
|
+
if (existsSync17(routesDir)) {
|
|
2438
2546
|
console.log();
|
|
2439
|
-
console.log(
|
|
2547
|
+
console.log(chalk8.bold("Route (api)"));
|
|
2440
2548
|
try {
|
|
2441
|
-
const routes =
|
|
2549
|
+
const routes = readdirSync2(routesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
|
|
2442
2550
|
if (routes.length > 0) {
|
|
2443
2551
|
routes.forEach((route, index) => {
|
|
2444
2552
|
const isLast = index === routes.length - 1;
|
|
2445
2553
|
const prefix = isLast ? "\u2514" : "\u251C";
|
|
2446
2554
|
const routePath = `/api/${route}`;
|
|
2447
2555
|
if (route === "health") {
|
|
2448
|
-
console.log(`${prefix} ${
|
|
2556
|
+
console.log(`${prefix} ${chalk8.green("GET")} ${routePath}`);
|
|
2449
2557
|
} else {
|
|
2450
|
-
console.log(`${prefix} ${
|
|
2558
|
+
console.log(`${prefix} ${chalk8.cyan("*")} ${routePath}`);
|
|
2451
2559
|
}
|
|
2452
2560
|
});
|
|
2453
2561
|
console.log();
|
|
2454
|
-
console.log(
|
|
2562
|
+
console.log(chalk8.cyan("*") + " (Hono) multiple methods supported");
|
|
2455
2563
|
}
|
|
2456
2564
|
} catch (error) {
|
|
2457
2565
|
}
|
|
@@ -2459,9 +2567,9 @@ await startServer({
|
|
|
2459
2567
|
} catch (error) {
|
|
2460
2568
|
spinner.fail("SPFN server build failed");
|
|
2461
2569
|
if (error instanceof Error) {
|
|
2462
|
-
console.error("\n" +
|
|
2570
|
+
console.error("\n" + chalk8.red(error.message));
|
|
2463
2571
|
if (error.stack) {
|
|
2464
|
-
console.error(
|
|
2572
|
+
console.error(chalk8.dim("\n" + error.stack));
|
|
2465
2573
|
}
|
|
2466
2574
|
} else {
|
|
2467
2575
|
logger.error(String(error));
|
|
@@ -2473,13 +2581,13 @@ await startServer({
|
|
|
2473
2581
|
logger.error("No Next.js or SPFN server found in this project.");
|
|
2474
2582
|
process.exit(1);
|
|
2475
2583
|
}
|
|
2476
|
-
console.log("\n" +
|
|
2477
|
-
console.log(
|
|
2478
|
-
console.log(" " +
|
|
2479
|
-
console.log(` ${
|
|
2584
|
+
console.log("\n" + chalk8.green.bold("\u2713 Build completed successfully!\n"));
|
|
2585
|
+
console.log(chalk8.bold("Next steps:\n"));
|
|
2586
|
+
console.log(" " + chalk8.cyan("Start production server:"));
|
|
2587
|
+
console.log(` ${chalk8.cyan(getRunCommand(pm))} spfn:start ${chalk8.gray("# Start SPFN + Next.js")}
|
|
2480
2588
|
`);
|
|
2481
|
-
console.log(" " +
|
|
2482
|
-
console.log(` ${
|
|
2589
|
+
console.log(" " + chalk8.cyan("Or deploy with Docker:"));
|
|
2590
|
+
console.log(` ${chalk8.cyan("docker compose -f docker-compose.production.yml up --build -d")}
|
|
2483
2591
|
`);
|
|
2484
2592
|
}
|
|
2485
2593
|
var buildCommand = new Command5("build").description("Build SPFN project for production (Next.js + Server)").option("--server-only", "Build only SPFN server (skip Next.js)").option("--next-only", "Build only Next.js (skip SPFN server)").option("--turbo", "Use Turbopack for Next.js build").action(async (options) => {
|
|
@@ -2490,25 +2598,25 @@ var buildCommand = new Command5("build").description("Build SPFN project for pro
|
|
|
2490
2598
|
init_logger();
|
|
2491
2599
|
init_package_manager();
|
|
2492
2600
|
import { Command as Command6 } from "commander";
|
|
2493
|
-
import { existsSync as
|
|
2494
|
-
import { join as
|
|
2601
|
+
import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
|
|
2602
|
+
import { join as join19 } from "path";
|
|
2495
2603
|
import { execa as execa8 } from "execa";
|
|
2496
|
-
import
|
|
2604
|
+
import chalk9 from "chalk";
|
|
2497
2605
|
var startCommand = new Command6("start").description("Start SPFN production server (Next.js + Hono)").option("--server-only", "Run only SPFN server (skip Next.js)").option("--next-only", "Run only Next.js (skip SPFN server)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "0.0.0.0").action(async (options) => {
|
|
2498
2606
|
if (!process.env.NODE_ENV) {
|
|
2499
2607
|
process.env.NODE_ENV = "production";
|
|
2500
2608
|
}
|
|
2501
2609
|
const cwd = process.cwd();
|
|
2502
|
-
const packageJsonPath =
|
|
2610
|
+
const packageJsonPath = join19(cwd, "package.json");
|
|
2503
2611
|
let hasNext = false;
|
|
2504
|
-
if (
|
|
2505
|
-
const packageJson = JSON.parse(
|
|
2612
|
+
if (existsSync18(packageJsonPath)) {
|
|
2613
|
+
const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
|
|
2506
2614
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2507
2615
|
}
|
|
2508
|
-
const builtServerDir =
|
|
2509
|
-
const hasBuiltServer =
|
|
2510
|
-
const nextBuildDir =
|
|
2511
|
-
const hasNextBuild =
|
|
2616
|
+
const builtServerDir = join19(cwd, ".spfn", "server");
|
|
2617
|
+
const hasBuiltServer = existsSync18(builtServerDir);
|
|
2618
|
+
const nextBuildDir = join19(cwd, ".next");
|
|
2619
|
+
const hasNextBuild = existsSync18(nextBuildDir);
|
|
2512
2620
|
if (!options.nextOnly && !hasBuiltServer) {
|
|
2513
2621
|
logger.error('.spfn/server directory not found. Please run "spfn build" first.');
|
|
2514
2622
|
process.exit(1);
|
|
@@ -2518,8 +2626,8 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2518
2626
|
process.exit(1);
|
|
2519
2627
|
}
|
|
2520
2628
|
const pm = detectPackageManager(cwd);
|
|
2521
|
-
const serverEntry =
|
|
2522
|
-
if (!
|
|
2629
|
+
const serverEntry = join19(cwd, ".spfn", "prod-server.mjs");
|
|
2630
|
+
if (!existsSync18(serverEntry)) {
|
|
2523
2631
|
logger.error('.spfn/prod-server.mjs not found. Please run "spfn build" first.');
|
|
2524
2632
|
process.exit(1);
|
|
2525
2633
|
}
|
|
@@ -2555,7 +2663,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2555
2663
|
}
|
|
2556
2664
|
const nextCmd = "next start -H 0.0.0.0 -p 3790";
|
|
2557
2665
|
const serverCmd = `node "${serverEntry}"`;
|
|
2558
|
-
console.log(
|
|
2666
|
+
console.log(chalk9.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
|
|
2559
2667
|
logger.info("Next.js: http://0.0.0.0:3790");
|
|
2560
2668
|
logger.info(`SPFN API: http://${options.host}:${options.port}
|
|
2561
2669
|
`);
|
|
@@ -2582,13 +2690,13 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2582
2690
|
// src/commands/codegen.ts
|
|
2583
2691
|
init_logger();
|
|
2584
2692
|
import { Command as Command7 } from "commander";
|
|
2585
|
-
import { existsSync as
|
|
2586
|
-
import { join as
|
|
2587
|
-
import
|
|
2693
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
2694
|
+
import { join as join20 } from "path";
|
|
2695
|
+
import chalk10 from "chalk";
|
|
2588
2696
|
async function initCodegen(options) {
|
|
2589
2697
|
const cwd = process.cwd();
|
|
2590
|
-
const rcPath =
|
|
2591
|
-
if (
|
|
2698
|
+
const rcPath = join20(cwd, ".spfnrc.ts");
|
|
2699
|
+
if (existsSync19(rcPath)) {
|
|
2592
2700
|
logger.warn(".spfnrc.ts already exists");
|
|
2593
2701
|
logger.info("Edit manually to add custom generators");
|
|
2594
2702
|
process.exit(0);
|
|
@@ -2614,13 +2722,13 @@ export default defineConfig({
|
|
|
2614
2722
|
});
|
|
2615
2723
|
`;
|
|
2616
2724
|
writeFileSync12(rcPath, content);
|
|
2617
|
-
console.log("\n" +
|
|
2618
|
-
console.log(
|
|
2725
|
+
console.log("\n" + chalk10.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2726
|
+
console.log(chalk10.gray("Configured the @spfn/core:route-map generator."));
|
|
2619
2727
|
if (options.withExample) {
|
|
2620
|
-
console.log("\n" +
|
|
2728
|
+
console.log("\n" + chalk10.yellow("\u{1F4DD} To add custom generators:"));
|
|
2621
2729
|
console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
|
|
2622
2730
|
console.log(" 2. Add to .spfnrc.ts:");
|
|
2623
|
-
console.log(
|
|
2731
|
+
console.log(chalk10.gray(`
|
|
2624
2732
|
{
|
|
2625
2733
|
"codegen": {
|
|
2626
2734
|
"generators": [
|
|
@@ -2630,11 +2738,11 @@ export default defineConfig({
|
|
|
2630
2738
|
}
|
|
2631
2739
|
}
|
|
2632
2740
|
`));
|
|
2633
|
-
console.log(" 3. Run: " +
|
|
2741
|
+
console.log(" 3. Run: " + chalk10.cyan("spfn dev") + " (generators run automatically)");
|
|
2634
2742
|
} else {
|
|
2635
|
-
console.log("\n" +
|
|
2743
|
+
console.log("\n" + chalk10.yellow("\u{1F4DD} Next steps:"));
|
|
2636
2744
|
console.log(" \u2022 Add custom generators to .spfnrc.ts");
|
|
2637
|
-
console.log(" \u2022 Run: " +
|
|
2745
|
+
console.log(" \u2022 Run: " + chalk10.cyan("spfn dev") + " to start development with code generation");
|
|
2638
2746
|
}
|
|
2639
2747
|
}
|
|
2640
2748
|
async function listGenerators() {
|
|
@@ -2647,10 +2755,10 @@ async function listGenerators() {
|
|
|
2647
2755
|
logger.info('Run "spfn codegen init" to initialize configuration');
|
|
2648
2756
|
return;
|
|
2649
2757
|
}
|
|
2650
|
-
console.log("\n" +
|
|
2758
|
+
console.log("\n" + chalk10.bold("Registered Generators:"));
|
|
2651
2759
|
generators.forEach((gen, index) => {
|
|
2652
|
-
console.log(` ${index + 1}. ${
|
|
2653
|
-
console.log(` Patterns: ${
|
|
2760
|
+
console.log(` ${index + 1}. ${chalk10.cyan(gen.name)}`);
|
|
2761
|
+
console.log(` Patterns: ${chalk10.gray(gen.watchPatterns.join(", "))}`);
|
|
2654
2762
|
});
|
|
2655
2763
|
console.log("");
|
|
2656
2764
|
}
|
|
@@ -2671,7 +2779,7 @@ async function runGenerators() {
|
|
|
2671
2779
|
debug: true
|
|
2672
2780
|
});
|
|
2673
2781
|
await orchestrator.generateAll();
|
|
2674
|
-
console.log("\n" +
|
|
2782
|
+
console.log("\n" + chalk10.green.bold("\u2713 Code generation completed"));
|
|
2675
2783
|
}
|
|
2676
2784
|
var codegenCommand = new Command7("codegen").description("Code generation management");
|
|
2677
2785
|
codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
|
|
@@ -2682,7 +2790,7 @@ codegenCommand.command("run").description("Run code generators once (no watch mo
|
|
|
2682
2790
|
init_logger();
|
|
2683
2791
|
import { Command as Command8 } from "commander";
|
|
2684
2792
|
import { execSync } from "child_process";
|
|
2685
|
-
import
|
|
2793
|
+
import chalk11 from "chalk";
|
|
2686
2794
|
|
|
2687
2795
|
// src/utils/secret-gen.ts
|
|
2688
2796
|
import { randomBytes, randomUUID } from "crypto";
|
|
@@ -2757,41 +2865,41 @@ function copyToClipboard(text) {
|
|
|
2757
2865
|
function generateSecret(bytes, preset, envVarName, copy) {
|
|
2758
2866
|
const key = randomBase64Url(bytes);
|
|
2759
2867
|
const config = preset ? PRESETS[preset] : null;
|
|
2760
|
-
console.log("\n" +
|
|
2868
|
+
console.log("\n" + chalk11.green.bold("\u2713 Generated secret key:"));
|
|
2761
2869
|
if (config) {
|
|
2762
|
-
console.log(
|
|
2870
|
+
console.log(chalk11.dim(` ${config.description} (${bytes * 8} bits)`));
|
|
2763
2871
|
} else {
|
|
2764
|
-
console.log(
|
|
2872
|
+
console.log(chalk11.dim(` ${bytes * 8}-bit secret`));
|
|
2765
2873
|
}
|
|
2766
|
-
console.log("\n" +
|
|
2874
|
+
console.log("\n" + chalk11.cyan(key) + "\n");
|
|
2767
2875
|
const varName = envVarName || config?.envVar || "SECRET_KEY";
|
|
2768
|
-
console.log(
|
|
2769
|
-
console.log(
|
|
2876
|
+
console.log(chalk11.dim("Add to your .env file:"));
|
|
2877
|
+
console.log(chalk11.yellow(`${varName}=${key}
|
|
2770
2878
|
`));
|
|
2771
2879
|
if (config?.usage) {
|
|
2772
|
-
console.log(
|
|
2773
|
-
console.log(
|
|
2880
|
+
console.log(chalk11.dim("Usage:"));
|
|
2881
|
+
console.log(chalk11.gray(` ${config.usage}
|
|
2774
2882
|
`));
|
|
2775
2883
|
}
|
|
2776
2884
|
if (copy) {
|
|
2777
2885
|
if (copyToClipboard(key)) {
|
|
2778
|
-
console.log(
|
|
2886
|
+
console.log(chalk11.green("\u2713 Copied to clipboard!\n"));
|
|
2779
2887
|
} else {
|
|
2780
2888
|
logger.warn("Could not copy to clipboard");
|
|
2781
2889
|
}
|
|
2782
2890
|
}
|
|
2783
2891
|
}
|
|
2784
2892
|
function listPresets() {
|
|
2785
|
-
console.log("\n" +
|
|
2893
|
+
console.log("\n" + chalk11.bold("Available presets:"));
|
|
2786
2894
|
console.log();
|
|
2787
2895
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
2788
|
-
console.log(` ${
|
|
2789
|
-
console.log(` ${" ".repeat(20)} ${
|
|
2896
|
+
console.log(` ${chalk11.cyan(name.padEnd(20))} ${chalk11.dim(config.description)}`);
|
|
2897
|
+
console.log(` ${" ".repeat(20)} ${chalk11.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
|
|
2790
2898
|
console.log();
|
|
2791
2899
|
});
|
|
2792
|
-
console.log(
|
|
2793
|
-
console.log(
|
|
2794
|
-
console.log(
|
|
2900
|
+
console.log(chalk11.dim("Usage:"));
|
|
2901
|
+
console.log(chalk11.gray(" spfn key <preset>"));
|
|
2902
|
+
console.log(chalk11.gray(" spfn key auth-encryption --copy"));
|
|
2795
2903
|
console.log();
|
|
2796
2904
|
}
|
|
2797
2905
|
var generateValueCommand = new Command8("generate").alias("gen").description("Generate random value (simple output, no metadata)").option("-b, --bytes <number>", "Number of random bytes", "32").option("-c, --copy", "Copy to clipboard").action((options) => {
|
|
@@ -2804,9 +2912,9 @@ var generateValueCommand = new Command8("generate").alias("gen").description("Ge
|
|
|
2804
2912
|
console.log(value);
|
|
2805
2913
|
if (options.copy) {
|
|
2806
2914
|
if (copyToClipboard(value)) {
|
|
2807
|
-
console.error(
|
|
2915
|
+
console.error(chalk11.green("\u2713 Copied to clipboard"));
|
|
2808
2916
|
} else {
|
|
2809
|
-
console.error(
|
|
2917
|
+
console.error(chalk11.yellow("\u26A0 Could not copy to clipboard"));
|
|
2810
2918
|
}
|
|
2811
2919
|
}
|
|
2812
2920
|
});
|
|
@@ -2824,9 +2932,9 @@ var keyCommand = new Command8("key").alias("k").description("Generate secure ran
|
|
|
2824
2932
|
logger.error(`Unknown preset: ${preset}`);
|
|
2825
2933
|
console.log("\nAvailable presets:");
|
|
2826
2934
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
2827
|
-
console.log(` ${
|
|
2935
|
+
console.log(` ${chalk11.cyan(name)}: ${config.description}`);
|
|
2828
2936
|
});
|
|
2829
|
-
console.log("\nUse " +
|
|
2937
|
+
console.log("\nUse " + chalk11.cyan("--list") + " to see detailed information");
|
|
2830
2938
|
process.exit(1);
|
|
2831
2939
|
}
|
|
2832
2940
|
generateSecret(
|
|
@@ -2845,16 +2953,16 @@ init_setup();
|
|
|
2845
2953
|
import { Command as Command9 } from "commander";
|
|
2846
2954
|
|
|
2847
2955
|
// src/commands/db/generate.ts
|
|
2848
|
-
import
|
|
2956
|
+
import chalk13 from "chalk";
|
|
2849
2957
|
|
|
2850
2958
|
// src/commands/db/utils/drizzle.ts
|
|
2851
|
-
import { existsSync as
|
|
2959
|
+
import { existsSync as existsSync20, writeFileSync as writeFileSync13, unlinkSync as unlinkSync2 } from "fs";
|
|
2852
2960
|
import { spawn } from "child_process";
|
|
2853
2961
|
import { pathToFileURL } from "url";
|
|
2854
|
-
import
|
|
2962
|
+
import chalk12 from "chalk";
|
|
2855
2963
|
import ora6 from "ora";
|
|
2856
|
-
import { env } from "@spfn/core/config";
|
|
2857
|
-
import { loadEnv } from "@spfn/core/server";
|
|
2964
|
+
import { env as env3 } from "@spfn/core/config";
|
|
2965
|
+
import { loadEnv as loadEnv3 } from "@spfn/core/server";
|
|
2858
2966
|
var TLS_SSL_MODES = /* @__PURE__ */ new Set(["no-verify", "prefer", "require", "verify-ca", "verify-full"]);
|
|
2859
2967
|
function parseDatabaseUrl(databaseUrl) {
|
|
2860
2968
|
if (!databaseUrl) {
|
|
@@ -2907,22 +3015,22 @@ function resolvePushConnectionConfig(databaseUrl) {
|
|
|
2907
3015
|
return { connectionString: parsedUrl.toString() };
|
|
2908
3016
|
}
|
|
2909
3017
|
function validateDatabasePrerequisites() {
|
|
2910
|
-
|
|
2911
|
-
if (!
|
|
2912
|
-
console.error(
|
|
2913
|
-
console.log(
|
|
3018
|
+
loadEnv3();
|
|
3019
|
+
if (!env3.DATABASE_URL) {
|
|
3020
|
+
console.error(chalk12.red("\u274C DATABASE_URL not found in environment"));
|
|
3021
|
+
console.log(chalk12.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2914
3022
|
throw new Error("DATABASE_URL is required for database operations");
|
|
2915
3023
|
}
|
|
2916
3024
|
}
|
|
2917
3025
|
async function runDrizzleCommand(command) {
|
|
2918
|
-
const hasUserConfig =
|
|
3026
|
+
const hasUserConfig = existsSync20("./drizzle.config.ts");
|
|
2919
3027
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2920
3028
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2921
3029
|
if (!hasUserConfig) {
|
|
2922
|
-
|
|
2923
|
-
if (!
|
|
2924
|
-
console.error(
|
|
2925
|
-
console.log(
|
|
3030
|
+
loadEnv3();
|
|
3031
|
+
if (!env3.DATABASE_URL) {
|
|
3032
|
+
console.error(chalk12.red("\u274C DATABASE_URL not found in environment"));
|
|
3033
|
+
console.log(chalk12.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2926
3034
|
process.exit(1);
|
|
2927
3035
|
}
|
|
2928
3036
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -2935,7 +3043,7 @@ async function runDrizzleCommand(command) {
|
|
|
2935
3043
|
autoDetectSchemas: true
|
|
2936
3044
|
});
|
|
2937
3045
|
writeFileSync13(tempConfigPath, configContent);
|
|
2938
|
-
console.log(
|
|
3046
|
+
console.log(chalk12.dim("Using auto-generated Drizzle config\n"));
|
|
2939
3047
|
}
|
|
2940
3048
|
const args = command.split(" ");
|
|
2941
3049
|
args.push(`--config=${configPath}`);
|
|
@@ -2944,10 +3052,10 @@ async function runDrizzleCommand(command) {
|
|
|
2944
3052
|
stdio: "inherit",
|
|
2945
3053
|
// Allow interactive input
|
|
2946
3054
|
shell: true,
|
|
2947
|
-
env: shouldRelaxDbTls(
|
|
3055
|
+
env: shouldRelaxDbTls(env3.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
2948
3056
|
});
|
|
2949
3057
|
const cleanup = () => {
|
|
2950
|
-
if (!hasUserConfig &&
|
|
3058
|
+
if (!hasUserConfig && existsSync20(tempConfigPath)) {
|
|
2951
3059
|
unlinkSync2(tempConfigPath);
|
|
2952
3060
|
}
|
|
2953
3061
|
};
|
|
@@ -2970,10 +3078,10 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
|
|
|
2970
3078
|
try {
|
|
2971
3079
|
spinner.stop();
|
|
2972
3080
|
await runDrizzleCommand(command);
|
|
2973
|
-
console.log(
|
|
3081
|
+
console.log(chalk12.green(`\u2705 ${successMessage}`));
|
|
2974
3082
|
} catch (error) {
|
|
2975
3083
|
spinner.fail(failMessage);
|
|
2976
|
-
console.error(
|
|
3084
|
+
console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2977
3085
|
process.exit(1);
|
|
2978
3086
|
}
|
|
2979
3087
|
}
|
|
@@ -2997,13 +3105,13 @@ async function loadSchemaImports(schemaFiles) {
|
|
|
2997
3105
|
return imports;
|
|
2998
3106
|
}
|
|
2999
3107
|
async function createPushConnection() {
|
|
3000
|
-
|
|
3001
|
-
if (!
|
|
3108
|
+
loadEnv3();
|
|
3109
|
+
if (!env3.DATABASE_URL) {
|
|
3002
3110
|
throw new Error("DATABASE_URL is required");
|
|
3003
3111
|
}
|
|
3004
3112
|
const pg = await import("pg");
|
|
3005
3113
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
3006
|
-
const connectionConfig = resolvePushConnectionConfig(
|
|
3114
|
+
const connectionConfig = resolvePushConnectionConfig(env3.DATABASE_URL);
|
|
3007
3115
|
const pool = new pg.default.Pool({
|
|
3008
3116
|
...connectionConfig,
|
|
3009
3117
|
max: 1
|
|
@@ -3019,19 +3127,19 @@ async function createPushConnection() {
|
|
|
3019
3127
|
async function dbGenerate() {
|
|
3020
3128
|
try {
|
|
3021
3129
|
await runDrizzleCommand("generate");
|
|
3022
|
-
console.log(
|
|
3130
|
+
console.log(chalk13.green("\n\u2705 Migrations generated successfully"));
|
|
3023
3131
|
} catch (error) {
|
|
3024
|
-
console.error(
|
|
3025
|
-
console.error(
|
|
3132
|
+
console.error(chalk13.red("\n\u274C Failed to generate migrations"));
|
|
3133
|
+
console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3026
3134
|
process.exit(1);
|
|
3027
3135
|
}
|
|
3028
3136
|
}
|
|
3029
3137
|
|
|
3030
3138
|
// src/commands/db/push.ts
|
|
3031
|
-
import
|
|
3139
|
+
import chalk15 from "chalk";
|
|
3032
3140
|
import prompts3 from "prompts";
|
|
3033
3141
|
import "@spfn/core/config";
|
|
3034
|
-
import { loadEnv as
|
|
3142
|
+
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
3035
3143
|
import { sql } from "drizzle-orm";
|
|
3036
3144
|
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
3037
3145
|
|
|
@@ -3075,7 +3183,7 @@ function classifyStatements(statements) {
|
|
|
3075
3183
|
}
|
|
3076
3184
|
|
|
3077
3185
|
// src/commands/db/utils/push-display.ts
|
|
3078
|
-
import
|
|
3186
|
+
import chalk14 from "chalk";
|
|
3079
3187
|
function formatSql(sql2) {
|
|
3080
3188
|
return sql2.replace(/\s+/g, " ").trim();
|
|
3081
3189
|
}
|
|
@@ -3083,30 +3191,30 @@ function printStatements(statements, color) {
|
|
|
3083
3191
|
for (const stmt of statements) {
|
|
3084
3192
|
console.log(color(` ${formatSql(stmt.sql)}`));
|
|
3085
3193
|
if (stmt.reason) {
|
|
3086
|
-
console.log(
|
|
3194
|
+
console.log(chalk14.dim(` \u2192 ${stmt.reason}`));
|
|
3087
3195
|
}
|
|
3088
3196
|
}
|
|
3089
3197
|
}
|
|
3090
3198
|
function displayClassifiedStatements(result) {
|
|
3091
3199
|
if (result.safe.length > 0) {
|
|
3092
|
-
console.log(
|
|
3200
|
+
console.log(chalk14.green(`
|
|
3093
3201
|
\u2705 Safe changes (${result.safe.length}):`));
|
|
3094
|
-
printStatements(result.safe,
|
|
3202
|
+
printStatements(result.safe, chalk14.green);
|
|
3095
3203
|
}
|
|
3096
3204
|
if (result.warning.length > 0) {
|
|
3097
|
-
console.log(
|
|
3205
|
+
console.log(chalk14.yellow(`
|
|
3098
3206
|
\u26A0\uFE0F Warnings (${result.warning.length}):`));
|
|
3099
|
-
printStatements(result.warning,
|
|
3207
|
+
printStatements(result.warning, chalk14.yellow);
|
|
3100
3208
|
}
|
|
3101
3209
|
if (result.destructive.length > 0) {
|
|
3102
|
-
console.log(
|
|
3210
|
+
console.log(chalk14.red(`
|
|
3103
3211
|
\u274C Destructive changes (${result.destructive.length}):`));
|
|
3104
|
-
printStatements(result.destructive,
|
|
3212
|
+
printStatements(result.destructive, chalk14.red);
|
|
3105
3213
|
}
|
|
3106
3214
|
}
|
|
3107
3215
|
function displayDryRunSummary(result) {
|
|
3108
3216
|
const total = result.safe.length + result.warning.length + result.destructive.length;
|
|
3109
|
-
console.log(
|
|
3217
|
+
console.log(chalk14.cyan(`
|
|
3110
3218
|
\u{1F50D} Dry-run: ${total} statement(s) detected
|
|
3111
3219
|
`));
|
|
3112
3220
|
displayClassifiedStatements(result);
|
|
@@ -3114,11 +3222,11 @@ function displayDryRunSummary(result) {
|
|
|
3114
3222
|
}
|
|
3115
3223
|
function displayApplySummary(applied, skipped) {
|
|
3116
3224
|
if (applied > 0) {
|
|
3117
|
-
console.log(
|
|
3225
|
+
console.log(chalk14.green(`
|
|
3118
3226
|
\u2705 Applied ${applied} statement(s)`));
|
|
3119
3227
|
}
|
|
3120
3228
|
if (skipped > 0) {
|
|
3121
|
-
console.log(
|
|
3229
|
+
console.log(chalk14.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
|
|
3122
3230
|
}
|
|
3123
3231
|
console.log("");
|
|
3124
3232
|
}
|
|
@@ -3153,7 +3261,7 @@ async function applyStatements(db, statements) {
|
|
|
3153
3261
|
}
|
|
3154
3262
|
async function dbPush(options = {}) {
|
|
3155
3263
|
validateDatabasePrerequisites();
|
|
3156
|
-
|
|
3264
|
+
loadEnv4();
|
|
3157
3265
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3158
3266
|
const config = getDrizzleConfig({
|
|
3159
3267
|
cwd: process.cwd(),
|
|
@@ -3163,10 +3271,10 @@ async function dbPush(options = {}) {
|
|
|
3163
3271
|
});
|
|
3164
3272
|
const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
|
|
3165
3273
|
if (schemaFiles.length === 0) {
|
|
3166
|
-
console.log(
|
|
3274
|
+
console.log(chalk15.yellow("No schema files found."));
|
|
3167
3275
|
return;
|
|
3168
3276
|
}
|
|
3169
|
-
console.log(
|
|
3277
|
+
console.log(chalk15.dim(`Found ${schemaFiles.length} schema file(s)
|
|
3170
3278
|
`));
|
|
3171
3279
|
const imports = await loadSchemaImports(schemaFiles);
|
|
3172
3280
|
const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
|
|
@@ -3185,10 +3293,10 @@ async function dbPush(options = {}) {
|
|
|
3185
3293
|
try {
|
|
3186
3294
|
const { statements, hints } = await resolvePushPlan(imports, db, schemaFilter);
|
|
3187
3295
|
for (const hint of hints) {
|
|
3188
|
-
console.log(
|
|
3296
|
+
console.log(chalk15.yellow(`\u26A0\uFE0F ${hint.hint}`));
|
|
3189
3297
|
}
|
|
3190
3298
|
if (statements.length === 0) {
|
|
3191
|
-
console.log(
|
|
3299
|
+
console.log(chalk15.green("\u2705 No changes detected \u2014 database is up to date\n"));
|
|
3192
3300
|
await applyFunctionMigrations(functionPlans);
|
|
3193
3301
|
return;
|
|
3194
3302
|
}
|
|
@@ -3199,7 +3307,7 @@ async function dbPush(options = {}) {
|
|
|
3199
3307
|
}
|
|
3200
3308
|
displayClassifiedStatements(result);
|
|
3201
3309
|
if (options.force) {
|
|
3202
|
-
console.log(
|
|
3310
|
+
console.log(chalk15.dim("\n--force: applying all changes..."));
|
|
3203
3311
|
await applyStatements(db, statements);
|
|
3204
3312
|
displayApplySummary(statements.length, 0);
|
|
3205
3313
|
} else if (result.destructive.length === 0) {
|
|
@@ -3208,11 +3316,11 @@ async function dbPush(options = {}) {
|
|
|
3208
3316
|
} else {
|
|
3209
3317
|
const destructiveSet = new Set(result.destructive.map((s) => s.sql));
|
|
3210
3318
|
const nonDestructive = statements.filter((s) => !destructiveSet.has(s));
|
|
3211
|
-
console.log(
|
|
3319
|
+
console.log(chalk15.red(`
|
|
3212
3320
|
\u274C ${result.destructive.length} destructive change(s) require confirmation:`));
|
|
3213
3321
|
for (const stmt of result.destructive) {
|
|
3214
|
-
console.log(
|
|
3215
|
-
console.log(
|
|
3322
|
+
console.log(chalk15.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
3323
|
+
console.log(chalk15.dim(` \u2192 ${stmt.reason}`));
|
|
3216
3324
|
}
|
|
3217
3325
|
const { confirm } = await prompts3({
|
|
3218
3326
|
type: "confirm",
|
|
@@ -3228,7 +3336,7 @@ async function dbPush(options = {}) {
|
|
|
3228
3336
|
await applyStatements(db, nonDestructive);
|
|
3229
3337
|
}
|
|
3230
3338
|
displayApplySummary(nonDestructive.length, result.destructive.length);
|
|
3231
|
-
console.log(
|
|
3339
|
+
console.log(chalk15.dim("Tip: Use --force to apply all changes without prompting.\n"));
|
|
3232
3340
|
}
|
|
3233
3341
|
}
|
|
3234
3342
|
await applyFunctionMigrations(functionPlans);
|
|
@@ -3241,8 +3349,8 @@ function loadFunctionPlansOrExit() {
|
|
|
3241
3349
|
try {
|
|
3242
3350
|
return loadFunctionMigrationPlans(functions);
|
|
3243
3351
|
} catch (error) {
|
|
3244
|
-
console.error(
|
|
3245
|
-
console.error(
|
|
3352
|
+
console.error(chalk15.red("\n\u274C Invalid function package migrations \u2014 nothing was applied"));
|
|
3353
|
+
console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3246
3354
|
process.exit(1);
|
|
3247
3355
|
}
|
|
3248
3356
|
}
|
|
@@ -3250,36 +3358,36 @@ async function applyFunctionMigrations(plans) {
|
|
|
3250
3358
|
if (plans.length === 0) {
|
|
3251
3359
|
return;
|
|
3252
3360
|
}
|
|
3253
|
-
console.log(
|
|
3361
|
+
console.log(chalk15.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
3254
3362
|
plans.forEach((plan) => {
|
|
3255
|
-
console.log(
|
|
3363
|
+
console.log(chalk15.dim(` - ${plan.packageName}`));
|
|
3256
3364
|
});
|
|
3257
3365
|
try {
|
|
3258
3366
|
await executeFunctionMigrations(plans);
|
|
3259
|
-
console.log(
|
|
3367
|
+
console.log(chalk15.green("\n\u2705 All function migrations applied\n"));
|
|
3260
3368
|
} catch (error) {
|
|
3261
|
-
console.error(
|
|
3262
|
-
console.error(
|
|
3263
|
-
console.error(
|
|
3369
|
+
console.error(chalk15.red("\n\u274C Failed to apply function package migrations"));
|
|
3370
|
+
console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3371
|
+
console.error(chalk15.yellow("Project schema changes (if any) were already applied \u2014 only function package migrations failed."));
|
|
3264
3372
|
process.exit(1);
|
|
3265
3373
|
}
|
|
3266
3374
|
}
|
|
3267
3375
|
|
|
3268
3376
|
// src/commands/db/migrate.ts
|
|
3269
|
-
import
|
|
3270
|
-
import { join as
|
|
3271
|
-
import { existsSync as
|
|
3377
|
+
import chalk20 from "chalk";
|
|
3378
|
+
import { join as join21 } from "path";
|
|
3379
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3272
3380
|
|
|
3273
3381
|
// src/commands/db/backup.ts
|
|
3274
3382
|
import { promises as fs3 } from "fs";
|
|
3275
3383
|
import path3 from "path";
|
|
3276
3384
|
import { spawn as spawn2 } from "child_process";
|
|
3277
|
-
import
|
|
3385
|
+
import chalk19 from "chalk";
|
|
3278
3386
|
import ora7 from "ora";
|
|
3279
3387
|
|
|
3280
3388
|
// src/commands/db/utils/database.ts
|
|
3281
3389
|
import net from "net";
|
|
3282
|
-
import
|
|
3390
|
+
import chalk16 from "chalk";
|
|
3283
3391
|
import prompts4 from "prompts";
|
|
3284
3392
|
function parseDatabaseUrl2(dbUrl) {
|
|
3285
3393
|
try {
|
|
@@ -3310,7 +3418,7 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
3310
3418
|
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
3311
3419
|
});
|
|
3312
3420
|
if (typed !== dbInfo.database) {
|
|
3313
|
-
console.log(
|
|
3421
|
+
console.log(chalk16.gray("Cancelled (database name mismatch)."));
|
|
3314
3422
|
process.exit(0);
|
|
3315
3423
|
}
|
|
3316
3424
|
}
|
|
@@ -3361,16 +3469,16 @@ function formatTimestamp() {
|
|
|
3361
3469
|
|
|
3362
3470
|
// src/commands/db/utils/backup-files.ts
|
|
3363
3471
|
import { promises as fs2 } from "fs";
|
|
3364
|
-
import { existsSync as
|
|
3472
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3365
3473
|
import path2 from "path";
|
|
3366
|
-
import
|
|
3474
|
+
import chalk18 from "chalk";
|
|
3367
3475
|
|
|
3368
3476
|
// src/commands/db/utils/metadata.ts
|
|
3369
3477
|
import { promises as fs } from "fs";
|
|
3370
3478
|
import path from "path";
|
|
3371
3479
|
import { promisify } from "util";
|
|
3372
3480
|
import { exec } from "child_process";
|
|
3373
|
-
import
|
|
3481
|
+
import chalk17 from "chalk";
|
|
3374
3482
|
var execAsync = promisify(exec);
|
|
3375
3483
|
async function collectGitInfo() {
|
|
3376
3484
|
try {
|
|
@@ -3433,7 +3541,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
3433
3541
|
await pool.end();
|
|
3434
3542
|
}
|
|
3435
3543
|
} catch (error) {
|
|
3436
|
-
console.log(
|
|
3544
|
+
console.log(chalk17.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
3437
3545
|
return void 0;
|
|
3438
3546
|
}
|
|
3439
3547
|
}
|
|
@@ -3441,9 +3549,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
3441
3549
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
3442
3550
|
try {
|
|
3443
3551
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
3444
|
-
console.log(
|
|
3552
|
+
console.log(chalk17.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
3445
3553
|
} catch (error) {
|
|
3446
|
-
console.log(
|
|
3554
|
+
console.log(chalk17.dim("\u26A0\uFE0F Could not save metadata"));
|
|
3447
3555
|
}
|
|
3448
3556
|
}
|
|
3449
3557
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -3461,7 +3569,7 @@ async function ensureBackupInGitignore() {
|
|
|
3461
3569
|
const gitignorePath = path2.join(process.cwd(), ".gitignore");
|
|
3462
3570
|
try {
|
|
3463
3571
|
let content = "";
|
|
3464
|
-
let exists =
|
|
3572
|
+
let exists = existsSync21(gitignorePath);
|
|
3465
3573
|
if (exists) {
|
|
3466
3574
|
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
3467
3575
|
}
|
|
@@ -3472,10 +3580,10 @@ async function ensureBackupInGitignore() {
|
|
|
3472
3580
|
if (!hasBackupsIgnore) {
|
|
3473
3581
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
3474
3582
|
await fs2.appendFile(gitignorePath, entry);
|
|
3475
|
-
console.log(
|
|
3583
|
+
console.log(chalk18.dim("\u2713 Added backups/ to .gitignore"));
|
|
3476
3584
|
}
|
|
3477
3585
|
} catch (error) {
|
|
3478
|
-
console.log(
|
|
3586
|
+
console.log(chalk18.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
3479
3587
|
}
|
|
3480
3588
|
}
|
|
3481
3589
|
async function ensureBackupDir() {
|
|
@@ -3483,7 +3591,7 @@ async function ensureBackupDir() {
|
|
|
3483
3591
|
try {
|
|
3484
3592
|
await fs2.mkdir(backupDir, { recursive: true });
|
|
3485
3593
|
const gitignorePath = path2.join(backupDir, ".gitignore");
|
|
3486
|
-
const gitignoreExists =
|
|
3594
|
+
const gitignoreExists = existsSync21(gitignorePath);
|
|
3487
3595
|
if (!gitignoreExists) {
|
|
3488
3596
|
await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
|
|
3489
3597
|
}
|
|
@@ -3522,15 +3630,15 @@ async function listBackupFiles() {
|
|
|
3522
3630
|
}
|
|
3523
3631
|
|
|
3524
3632
|
// src/commands/db/backup.ts
|
|
3525
|
-
import { env as
|
|
3526
|
-
import { loadEnv as
|
|
3633
|
+
import { env as env4 } from "@spfn/core/config";
|
|
3634
|
+
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
3527
3635
|
async function dbBackup(options) {
|
|
3528
|
-
console.log(
|
|
3529
|
-
|
|
3530
|
-
const dbUrl =
|
|
3636
|
+
console.log(chalk19.blue("\u{1F4BE} Creating database backup...\n"));
|
|
3637
|
+
loadEnv5();
|
|
3638
|
+
const dbUrl = env4.DATABASE_URL;
|
|
3531
3639
|
if (!dbUrl) {
|
|
3532
|
-
console.error(
|
|
3533
|
-
console.log(
|
|
3640
|
+
console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
|
|
3641
|
+
console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3534
3642
|
process.exit(1);
|
|
3535
3643
|
}
|
|
3536
3644
|
const dbInfo = parseDatabaseUrl2(dbUrl);
|
|
@@ -3540,7 +3648,7 @@ async function dbBackup(options) {
|
|
|
3540
3648
|
const ext = format === "sql" ? "sql" : "dump";
|
|
3541
3649
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
3542
3650
|
if (options.dataOnly && options.schemaOnly) {
|
|
3543
|
-
console.error(
|
|
3651
|
+
console.error(chalk19.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3544
3652
|
process.exit(1);
|
|
3545
3653
|
}
|
|
3546
3654
|
const args = [
|
|
@@ -3586,11 +3694,11 @@ async function dbBackup(options) {
|
|
|
3586
3694
|
const stats = await fs3.stat(filename);
|
|
3587
3695
|
const size = formatBytes(stats.size);
|
|
3588
3696
|
spinner.succeed("Backup created");
|
|
3589
|
-
console.log(
|
|
3697
|
+
console.log(chalk19.green(`
|
|
3590
3698
|
\u2705 Backup created successfully`));
|
|
3591
|
-
console.log(
|
|
3592
|
-
console.log(
|
|
3593
|
-
console.log(
|
|
3699
|
+
console.log(chalk19.gray(` File: ${filename}`));
|
|
3700
|
+
console.log(chalk19.gray(` Size: ${size}`));
|
|
3701
|
+
console.log(chalk19.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
3594
3702
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
3595
3703
|
collectGitInfo(),
|
|
3596
3704
|
collectMigrationInfo(dbUrl)
|
|
@@ -3630,19 +3738,19 @@ async function dbBackup(options) {
|
|
|
3630
3738
|
reject(error);
|
|
3631
3739
|
});
|
|
3632
3740
|
}).catch((error) => {
|
|
3633
|
-
console.error(
|
|
3741
|
+
console.error(chalk19.red("\n\u274C Failed to create backup"));
|
|
3634
3742
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
3635
|
-
console.error(
|
|
3743
|
+
console.error(chalk19.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
3636
3744
|
} else {
|
|
3637
|
-
console.error(
|
|
3745
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3638
3746
|
}
|
|
3639
3747
|
process.exit(1);
|
|
3640
3748
|
});
|
|
3641
3749
|
}
|
|
3642
3750
|
|
|
3643
3751
|
// src/commands/db/migrate.ts
|
|
3644
|
-
import { env as
|
|
3645
|
-
import { loadEnv as
|
|
3752
|
+
import { env as env5 } from "@spfn/core/config";
|
|
3753
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
3646
3754
|
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
3647
3755
|
async function dbMigrate(options = {}) {
|
|
3648
3756
|
try {
|
|
@@ -3651,7 +3759,7 @@ async function dbMigrate(options = {}) {
|
|
|
3651
3759
|
process.exit(1);
|
|
3652
3760
|
}
|
|
3653
3761
|
if (options.withBackup) {
|
|
3654
|
-
console.log(
|
|
3762
|
+
console.log(chalk20.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
3655
3763
|
await dbBackup({
|
|
3656
3764
|
format: "custom",
|
|
3657
3765
|
tag: "pre-migration",
|
|
@@ -3662,68 +3770,108 @@ async function dbMigrate(options = {}) {
|
|
|
3662
3770
|
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
3663
3771
|
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
3664
3772
|
const postgres = await import("postgres");
|
|
3665
|
-
|
|
3666
|
-
if (!
|
|
3667
|
-
console.error(
|
|
3773
|
+
loadEnv6();
|
|
3774
|
+
if (!env5.DATABASE_URL) {
|
|
3775
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
3668
3776
|
process.exit(1);
|
|
3669
3777
|
}
|
|
3670
3778
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
3671
3779
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
3672
3780
|
if (functions.length > 0) {
|
|
3673
|
-
console.log(
|
|
3781
|
+
console.log(chalk20.blue("\u{1F4E6} Applying function package migrations:"));
|
|
3674
3782
|
functions.forEach((func) => {
|
|
3675
|
-
console.log(
|
|
3783
|
+
console.log(chalk20.dim(` - ${func.packageName}`));
|
|
3676
3784
|
});
|
|
3677
3785
|
await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
|
|
3678
|
-
console.log(
|
|
3786
|
+
console.log(chalk20.green("\u2705 Function migrations applied\n"));
|
|
3679
3787
|
}
|
|
3680
|
-
const projectMigrationsDir =
|
|
3681
|
-
if (
|
|
3682
|
-
const projConn = postgres.default(
|
|
3788
|
+
const projectMigrationsDir = join21(process.cwd(), "src/server/drizzle");
|
|
3789
|
+
if (existsSync22(projectMigrationsDir)) {
|
|
3790
|
+
const projConn = postgres.default(env5.DATABASE_URL, { max: 1 });
|
|
3683
3791
|
const projDb = drizzle({ client: projConn });
|
|
3684
3792
|
try {
|
|
3685
|
-
console.log(
|
|
3793
|
+
console.log(chalk20.blue("\u{1F4E6} Running project migrations..."));
|
|
3686
3794
|
await migrate(projDb, {
|
|
3687
3795
|
migrationsFolder: projectMigrationsDir,
|
|
3688
3796
|
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
3689
3797
|
});
|
|
3690
|
-
console.log(
|
|
3798
|
+
console.log(chalk20.green("\u2705 Project migrations applied successfully"));
|
|
3691
3799
|
} finally {
|
|
3692
3800
|
await projConn.end();
|
|
3693
3801
|
}
|
|
3694
3802
|
} else {
|
|
3695
|
-
console.log(
|
|
3803
|
+
console.log(chalk20.dim("No project migrations found (src/server/drizzle)"));
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// src/commands/db/status.ts
|
|
3808
|
+
import chalk21 from "chalk";
|
|
3809
|
+
init_migration_status();
|
|
3810
|
+
async function dbStatus() {
|
|
3811
|
+
try {
|
|
3812
|
+
validateDatabasePrerequisites();
|
|
3813
|
+
} catch (error) {
|
|
3814
|
+
process.exit(1);
|
|
3815
|
+
}
|
|
3816
|
+
let status;
|
|
3817
|
+
try {
|
|
3818
|
+
status = await getMigrationStatus(process.cwd());
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
console.error(chalk21.red("\u274C Failed to read migration status"));
|
|
3821
|
+
console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3822
|
+
process.exit(1);
|
|
3823
|
+
}
|
|
3824
|
+
const targets = [...status.packages, ...status.project ? [status.project] : []];
|
|
3825
|
+
if (targets.length === 0) {
|
|
3826
|
+
console.log(chalk21.dim("No migrations found (no @spfn packages with migrations, no src/server/drizzle)."));
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
console.log(chalk21.blue("\u{1F4E6} Migration status:\n"));
|
|
3830
|
+
for (const target of targets) {
|
|
3831
|
+
const state = target.pending === 0 ? chalk21.green(`${target.applied}/${target.total} applied`) : chalk21.yellow(`${target.applied}/${target.total} applied, ${target.pending} pending`);
|
|
3832
|
+
console.log(` ${target.name.padEnd(32)} ${state}`);
|
|
3833
|
+
for (const tag of target.pendingTags) {
|
|
3834
|
+
console.log(chalk21.dim(` - ${tag}`));
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const pendingTotal = targets.reduce((sum, t) => sum + t.pending, 0);
|
|
3838
|
+
if (pendingTotal === 0) {
|
|
3839
|
+
console.log(chalk21.green("\n\u2705 Database is up to date\n"));
|
|
3840
|
+
} else {
|
|
3841
|
+
console.log(chalk21.yellow(`
|
|
3842
|
+
\u26A0\uFE0F ${pendingTotal} pending migration(s) \u2014 run: pnpm spfn db migrate
|
|
3843
|
+
`));
|
|
3696
3844
|
}
|
|
3697
3845
|
}
|
|
3698
3846
|
|
|
3699
3847
|
// src/commands/db/studio.ts
|
|
3700
|
-
import
|
|
3701
|
-
import { existsSync as
|
|
3848
|
+
import chalk22 from "chalk";
|
|
3849
|
+
import { existsSync as existsSync23, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3 } from "fs";
|
|
3702
3850
|
import { spawn as spawn3 } from "child_process";
|
|
3703
|
-
import { env as
|
|
3851
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3704
3852
|
async function dbStudio(requestedPort) {
|
|
3705
|
-
console.log(
|
|
3853
|
+
console.log(chalk22.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
3706
3854
|
const defaultPort = 4983;
|
|
3707
3855
|
const startPort = requestedPort || defaultPort;
|
|
3708
3856
|
let port;
|
|
3709
3857
|
try {
|
|
3710
3858
|
port = await findAvailablePort(startPort);
|
|
3711
3859
|
if (port !== startPort) {
|
|
3712
|
-
console.log(
|
|
3860
|
+
console.log(chalk22.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
3713
3861
|
`));
|
|
3714
3862
|
}
|
|
3715
3863
|
} catch (error) {
|
|
3716
|
-
console.error(
|
|
3864
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
3717
3865
|
process.exit(1);
|
|
3718
3866
|
}
|
|
3719
|
-
const hasUserConfig =
|
|
3867
|
+
const hasUserConfig = existsSync23("./drizzle.config.ts");
|
|
3720
3868
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
3721
3869
|
try {
|
|
3722
3870
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3723
3871
|
if (!hasUserConfig) {
|
|
3724
|
-
if (!
|
|
3725
|
-
console.error(
|
|
3726
|
-
console.log(
|
|
3872
|
+
if (!env6.DATABASE_URL) {
|
|
3873
|
+
console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
|
|
3874
|
+
console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3727
3875
|
process.exit(1);
|
|
3728
3876
|
}
|
|
3729
3877
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -3734,34 +3882,34 @@ async function dbStudio(requestedPort) {
|
|
|
3734
3882
|
// Expand glob patterns for Studio compatibility
|
|
3735
3883
|
});
|
|
3736
3884
|
writeFileSync14(tempConfigPath, configContent);
|
|
3737
|
-
console.log(
|
|
3885
|
+
console.log(chalk22.dim("Using auto-generated Drizzle config\n"));
|
|
3738
3886
|
}
|
|
3739
3887
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
3740
3888
|
stdio: "inherit",
|
|
3741
3889
|
shell: true,
|
|
3742
|
-
env: shouldRelaxDbTls(
|
|
3890
|
+
env: shouldRelaxDbTls(env6.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
3743
3891
|
});
|
|
3744
3892
|
const cleanup = () => {
|
|
3745
|
-
if (!hasUserConfig &&
|
|
3893
|
+
if (!hasUserConfig && existsSync23(tempConfigPath)) {
|
|
3746
3894
|
unlinkSync3(tempConfigPath);
|
|
3747
3895
|
}
|
|
3748
3896
|
};
|
|
3749
3897
|
studioProcess.on("exit", (code) => {
|
|
3750
3898
|
cleanup();
|
|
3751
3899
|
if (code !== 0 && code !== null) {
|
|
3752
|
-
console.error(
|
|
3900
|
+
console.error(chalk22.red(`
|
|
3753
3901
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
3754
3902
|
process.exit(code);
|
|
3755
3903
|
}
|
|
3756
3904
|
});
|
|
3757
3905
|
studioProcess.on("error", (error) => {
|
|
3758
3906
|
cleanup();
|
|
3759
|
-
console.error(
|
|
3760
|
-
console.error(
|
|
3907
|
+
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
3908
|
+
console.error(chalk22.red(error.message));
|
|
3761
3909
|
process.exit(1);
|
|
3762
3910
|
});
|
|
3763
3911
|
process.on("SIGINT", () => {
|
|
3764
|
-
console.log(
|
|
3912
|
+
console.log(chalk22.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
3765
3913
|
studioProcess.kill("SIGTERM");
|
|
3766
3914
|
cleanup();
|
|
3767
3915
|
process.exit(0);
|
|
@@ -3772,30 +3920,30 @@ async function dbStudio(requestedPort) {
|
|
|
3772
3920
|
process.exit(0);
|
|
3773
3921
|
});
|
|
3774
3922
|
} catch (error) {
|
|
3775
|
-
if (!hasUserConfig &&
|
|
3923
|
+
if (!hasUserConfig && existsSync23(tempConfigPath)) {
|
|
3776
3924
|
unlinkSync3(tempConfigPath);
|
|
3777
3925
|
}
|
|
3778
|
-
console.error(
|
|
3779
|
-
console.error(
|
|
3926
|
+
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
3927
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3780
3928
|
process.exit(1);
|
|
3781
3929
|
}
|
|
3782
3930
|
}
|
|
3783
3931
|
|
|
3784
3932
|
// src/commands/db/drop.ts
|
|
3785
|
-
import
|
|
3933
|
+
import chalk23 from "chalk";
|
|
3786
3934
|
import prompts5 from "prompts";
|
|
3787
|
-
import { env as
|
|
3788
|
-
import { loadEnv as
|
|
3935
|
+
import { env as env7 } from "@spfn/core/config";
|
|
3936
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3789
3937
|
async function dbDrop() {
|
|
3790
|
-
|
|
3791
|
-
if (!
|
|
3792
|
-
console.error(
|
|
3793
|
-
console.log(
|
|
3938
|
+
loadEnv7();
|
|
3939
|
+
if (!env7.DATABASE_URL) {
|
|
3940
|
+
console.error(chalk23.red("\u274C DATABASE_URL not found in environment"));
|
|
3941
|
+
console.log(chalk23.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3794
3942
|
process.exit(1);
|
|
3795
3943
|
}
|
|
3796
|
-
const dbInfo = parseDatabaseUrl2(
|
|
3797
|
-
console.log(
|
|
3798
|
-
console.log(
|
|
3944
|
+
const dbInfo = parseDatabaseUrl2(env7.DATABASE_URL);
|
|
3945
|
+
console.log(chalk23.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3946
|
+
console.log(chalk23.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
3799
3947
|
`));
|
|
3800
3948
|
const { confirm } = await prompts5({
|
|
3801
3949
|
type: "confirm",
|
|
@@ -3804,7 +3952,7 @@ async function dbDrop() {
|
|
|
3804
3952
|
initial: false
|
|
3805
3953
|
});
|
|
3806
3954
|
if (!confirm) {
|
|
3807
|
-
console.log(
|
|
3955
|
+
console.log(chalk23.gray("Cancelled."));
|
|
3808
3956
|
process.exit(0);
|
|
3809
3957
|
}
|
|
3810
3958
|
await confirmDangerousTarget(dbInfo);
|
|
@@ -3817,16 +3965,16 @@ async function dbDrop() {
|
|
|
3817
3965
|
}
|
|
3818
3966
|
|
|
3819
3967
|
// src/commands/db/check.ts
|
|
3820
|
-
import
|
|
3968
|
+
import chalk24 from "chalk";
|
|
3821
3969
|
import ora8 from "ora";
|
|
3822
3970
|
async function dbCheck() {
|
|
3823
|
-
const spinner = ora8("Checking
|
|
3971
|
+
const spinner = ora8("Checking migration files...").start();
|
|
3824
3972
|
try {
|
|
3825
3973
|
await runDrizzleCommand("check");
|
|
3826
|
-
spinner.succeed("
|
|
3974
|
+
spinner.succeed("Migration files consistent");
|
|
3827
3975
|
} catch (error) {
|
|
3828
|
-
spinner.fail("
|
|
3829
|
-
console.error(
|
|
3976
|
+
spinner.fail("Migration file check failed");
|
|
3977
|
+
console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3830
3978
|
process.exit(1);
|
|
3831
3979
|
}
|
|
3832
3980
|
}
|
|
@@ -3834,25 +3982,25 @@ async function dbCheck() {
|
|
|
3834
3982
|
// src/commands/db/restore.ts
|
|
3835
3983
|
import path4 from "path";
|
|
3836
3984
|
import { spawn as spawn4 } from "child_process";
|
|
3837
|
-
import
|
|
3985
|
+
import chalk25 from "chalk";
|
|
3838
3986
|
import ora9 from "ora";
|
|
3839
3987
|
import prompts6 from "prompts";
|
|
3840
|
-
import { env as
|
|
3841
|
-
import { loadEnv as
|
|
3988
|
+
import { env as env8 } from "@spfn/core/config";
|
|
3989
|
+
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
3842
3990
|
async function dbRestore(backupFile, options = {}) {
|
|
3843
|
-
console.log(
|
|
3844
|
-
|
|
3845
|
-
const dbUrl =
|
|
3991
|
+
console.log(chalk25.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
3992
|
+
loadEnv8();
|
|
3993
|
+
const dbUrl = env8.DATABASE_URL;
|
|
3846
3994
|
if (!dbUrl) {
|
|
3847
|
-
console.error(
|
|
3848
|
-
console.log(
|
|
3995
|
+
console.error(chalk25.red("\u274C DATABASE_URL not found in environment"));
|
|
3996
|
+
console.log(chalk25.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3849
3997
|
process.exit(1);
|
|
3850
3998
|
}
|
|
3851
3999
|
let file = backupFile;
|
|
3852
4000
|
if (!file) {
|
|
3853
4001
|
const backups = await listBackupFiles();
|
|
3854
4002
|
if (backups.length === 0) {
|
|
3855
|
-
console.log(
|
|
4003
|
+
console.log(chalk25.yellow("No backups found in ./backups directory"));
|
|
3856
4004
|
process.exit(0);
|
|
3857
4005
|
}
|
|
3858
4006
|
const { selected } = await prompts6({
|
|
@@ -3865,31 +4013,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3865
4013
|
}))
|
|
3866
4014
|
});
|
|
3867
4015
|
if (!selected) {
|
|
3868
|
-
console.log(
|
|
4016
|
+
console.log(chalk25.gray("Cancelled"));
|
|
3869
4017
|
process.exit(0);
|
|
3870
4018
|
}
|
|
3871
4019
|
file = selected;
|
|
3872
4020
|
}
|
|
3873
4021
|
if (!file) {
|
|
3874
|
-
console.error(
|
|
4022
|
+
console.error(chalk25.red("\u274C No backup file selected"));
|
|
3875
4023
|
process.exit(1);
|
|
3876
4024
|
}
|
|
3877
4025
|
const metadata = await loadBackupMetadata(file);
|
|
3878
4026
|
if (metadata) {
|
|
3879
|
-
console.log(
|
|
3880
|
-
console.log(
|
|
3881
|
-
console.log(
|
|
4027
|
+
console.log(chalk25.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
4028
|
+
console.log(chalk25.dim(` Database: ${metadata.database}`));
|
|
4029
|
+
console.log(chalk25.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
3882
4030
|
if (metadata.environment) {
|
|
3883
|
-
console.log(
|
|
4031
|
+
console.log(chalk25.dim(` Environment: ${metadata.environment}`));
|
|
3884
4032
|
}
|
|
3885
4033
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3886
|
-
console.log(
|
|
4034
|
+
console.log(chalk25.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
3887
4035
|
}
|
|
3888
4036
|
if (metadata.backup.dataOnly) {
|
|
3889
|
-
console.log(
|
|
4037
|
+
console.log(chalk25.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
3890
4038
|
}
|
|
3891
4039
|
if (metadata.backup.schemaOnly) {
|
|
3892
|
-
console.log(
|
|
4040
|
+
console.log(chalk25.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
3893
4041
|
}
|
|
3894
4042
|
const warnings2 = [];
|
|
3895
4043
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
@@ -3912,8 +4060,8 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3912
4060
|
}
|
|
3913
4061
|
}
|
|
3914
4062
|
if (warnings2.length > 0) {
|
|
3915
|
-
console.log(
|
|
3916
|
-
warnings2.forEach((warning) => console.log(
|
|
4063
|
+
console.log(chalk25.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
4064
|
+
warnings2.forEach((warning) => console.log(chalk25.yellow(` - ${warning}`)));
|
|
3917
4065
|
console.log("");
|
|
3918
4066
|
}
|
|
3919
4067
|
}
|
|
@@ -3921,16 +4069,16 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3921
4069
|
const { confirm } = await prompts6({
|
|
3922
4070
|
type: "confirm",
|
|
3923
4071
|
name: "confirm",
|
|
3924
|
-
message:
|
|
4072
|
+
message: chalk25.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
3925
4073
|
initial: false
|
|
3926
4074
|
});
|
|
3927
4075
|
if (!confirm) {
|
|
3928
|
-
console.log(
|
|
4076
|
+
console.log(chalk25.gray("Cancelled"));
|
|
3929
4077
|
process.exit(0);
|
|
3930
4078
|
}
|
|
3931
4079
|
await confirmDangerousTarget(dbInfo);
|
|
3932
4080
|
if (options.dataOnly && options.schemaOnly) {
|
|
3933
|
-
console.error(
|
|
4081
|
+
console.error(chalk25.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3934
4082
|
process.exit(1);
|
|
3935
4083
|
}
|
|
3936
4084
|
const ext = path4.extname(file);
|
|
@@ -3958,11 +4106,11 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3958
4106
|
args.push(file);
|
|
3959
4107
|
} else {
|
|
3960
4108
|
if (options.drop) {
|
|
3961
|
-
console.log(
|
|
4109
|
+
console.log(chalk25.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
|
|
3962
4110
|
}
|
|
3963
4111
|
if (options.dataOnly || options.schemaOnly) {
|
|
3964
|
-
console.log(
|
|
3965
|
-
console.log(
|
|
4112
|
+
console.log(chalk25.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
4113
|
+
console.log(chalk25.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
3966
4114
|
}
|
|
3967
4115
|
args.push("-h", dbInfo.host);
|
|
3968
4116
|
args.push("-p", dbInfo.port);
|
|
@@ -4005,7 +4153,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4005
4153
|
}
|
|
4006
4154
|
if (verbose) {
|
|
4007
4155
|
spinner.stop();
|
|
4008
|
-
console.log(
|
|
4156
|
+
console.log(chalk25.dim(` ${line.trim()}`));
|
|
4009
4157
|
spinner.start();
|
|
4010
4158
|
}
|
|
4011
4159
|
}
|
|
@@ -4013,7 +4161,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4013
4161
|
restoreProcess.stdout?.on("data", (data) => {
|
|
4014
4162
|
if (verbose) {
|
|
4015
4163
|
spinner.stop();
|
|
4016
|
-
console.log(
|
|
4164
|
+
console.log(chalk25.dim(` ${data.toString().trim()}`));
|
|
4017
4165
|
spinner.start();
|
|
4018
4166
|
}
|
|
4019
4167
|
});
|
|
@@ -4023,31 +4171,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4023
4171
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
4024
4172
|
spinner.succeed(`Restore completed${summary}`);
|
|
4025
4173
|
if (warnings.length > 0) {
|
|
4026
|
-
console.log(
|
|
4174
|
+
console.log(chalk25.yellow(`
|
|
4027
4175
|
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
4028
4176
|
`));
|
|
4029
4177
|
for (const w of warnings) {
|
|
4030
|
-
console.log(
|
|
4178
|
+
console.log(chalk25.yellow(` - ${w}`));
|
|
4031
4179
|
}
|
|
4032
4180
|
}
|
|
4033
|
-
console.log(
|
|
4181
|
+
console.log(chalk25.green("\n\u2705 Database restored successfully"));
|
|
4034
4182
|
resolve3();
|
|
4035
4183
|
} else {
|
|
4036
4184
|
spinner.fail("Restore failed");
|
|
4037
4185
|
if (errors.length > 0) {
|
|
4038
|
-
console.error(
|
|
4186
|
+
console.error(chalk25.red(`
|
|
4039
4187
|
\u274C Errors (${errors.length}):
|
|
4040
4188
|
`));
|
|
4041
4189
|
for (const e of errors) {
|
|
4042
|
-
console.error(
|
|
4190
|
+
console.error(chalk25.red(` - ${e}`));
|
|
4043
4191
|
}
|
|
4044
4192
|
}
|
|
4045
4193
|
if (warnings.length > 0) {
|
|
4046
|
-
console.log(
|
|
4194
|
+
console.log(chalk25.yellow(`
|
|
4047
4195
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
4048
4196
|
`));
|
|
4049
4197
|
for (const w of warnings) {
|
|
4050
|
-
console.log(
|
|
4198
|
+
console.log(chalk25.yellow(` - ${w}`));
|
|
4051
4199
|
}
|
|
4052
4200
|
}
|
|
4053
4201
|
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
@@ -4061,7 +4209,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4061
4209
|
}).catch((error) => {
|
|
4062
4210
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4063
4211
|
if (msg) {
|
|
4064
|
-
console.error(
|
|
4212
|
+
console.error(chalk25.red(`
|
|
4065
4213
|
\u274C ${msg}`));
|
|
4066
4214
|
}
|
|
4067
4215
|
process.exit(1);
|
|
@@ -4069,17 +4217,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4069
4217
|
}
|
|
4070
4218
|
|
|
4071
4219
|
// src/commands/db/list.ts
|
|
4072
|
-
import
|
|
4220
|
+
import chalk26 from "chalk";
|
|
4073
4221
|
async function dbBackupList() {
|
|
4074
|
-
console.log(
|
|
4222
|
+
console.log(chalk26.blue("\u{1F4CB} Database backups:\n"));
|
|
4075
4223
|
const backups = await listBackupFiles();
|
|
4076
4224
|
if (backups.length === 0) {
|
|
4077
|
-
console.log(
|
|
4078
|
-
console.log(
|
|
4225
|
+
console.log(chalk26.yellow("No backups found in ./backups directory"));
|
|
4226
|
+
console.log(chalk26.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
4079
4227
|
return;
|
|
4080
4228
|
}
|
|
4081
|
-
console.log(
|
|
4082
|
-
console.log(
|
|
4229
|
+
console.log(chalk26.bold(" Date Size File"));
|
|
4230
|
+
console.log(chalk26.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4083
4231
|
backups.forEach((backup) => {
|
|
4084
4232
|
const date = backup.date.toLocaleString("en-US", {
|
|
4085
4233
|
year: "numeric",
|
|
@@ -4090,23 +4238,23 @@ async function dbBackupList() {
|
|
|
4090
4238
|
second: "2-digit"
|
|
4091
4239
|
});
|
|
4092
4240
|
const sizeStr = backup.size.padEnd(10);
|
|
4093
|
-
console.log(
|
|
4241
|
+
console.log(chalk26.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
4094
4242
|
});
|
|
4095
|
-
console.log(
|
|
4243
|
+
console.log(chalk26.gray(`
|
|
4096
4244
|
Total: ${backups.length} backup(s)
|
|
4097
4245
|
`));
|
|
4098
4246
|
}
|
|
4099
4247
|
|
|
4100
4248
|
// src/commands/db/clean.ts
|
|
4101
4249
|
import { promises as fs4 } from "fs";
|
|
4102
|
-
import
|
|
4250
|
+
import chalk27 from "chalk";
|
|
4103
4251
|
import ora10 from "ora";
|
|
4104
4252
|
import prompts7 from "prompts";
|
|
4105
4253
|
async function dbBackupClean(options) {
|
|
4106
|
-
console.log(
|
|
4254
|
+
console.log(chalk27.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
4107
4255
|
const backups = await listBackupFiles();
|
|
4108
4256
|
if (backups.length === 0) {
|
|
4109
|
-
console.log(
|
|
4257
|
+
console.log(chalk27.yellow("No backups found"));
|
|
4110
4258
|
return;
|
|
4111
4259
|
}
|
|
4112
4260
|
let toDelete = [];
|
|
@@ -4123,13 +4271,13 @@ async function dbBackupClean(options) {
|
|
|
4123
4271
|
toDelete = backups.slice(defaultKeep);
|
|
4124
4272
|
}
|
|
4125
4273
|
if (toDelete.length === 0) {
|
|
4126
|
-
console.log(
|
|
4274
|
+
console.log(chalk27.green("\u2705 No backups to clean"));
|
|
4127
4275
|
return;
|
|
4128
4276
|
}
|
|
4129
|
-
console.log(
|
|
4277
|
+
console.log(chalk27.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
4130
4278
|
`));
|
|
4131
4279
|
toDelete.forEach((backup) => {
|
|
4132
|
-
console.log(
|
|
4280
|
+
console.log(chalk27.gray(` - ${backup.name} (${backup.size})`));
|
|
4133
4281
|
});
|
|
4134
4282
|
const { confirm } = await prompts7({
|
|
4135
4283
|
type: "confirm",
|
|
@@ -4138,7 +4286,7 @@ async function dbBackupClean(options) {
|
|
|
4138
4286
|
initial: false
|
|
4139
4287
|
});
|
|
4140
4288
|
if (!confirm) {
|
|
4141
|
-
console.log(
|
|
4289
|
+
console.log(chalk27.gray("Cancelled"));
|
|
4142
4290
|
return;
|
|
4143
4291
|
}
|
|
4144
4292
|
const spinner = ora10("Deleting backups...").start();
|
|
@@ -4149,20 +4297,20 @@ async function dbBackupClean(options) {
|
|
|
4149
4297
|
fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
|
|
4150
4298
|
]));
|
|
4151
4299
|
spinner.succeed("Backups deleted");
|
|
4152
|
-
console.log(
|
|
4300
|
+
console.log(chalk27.green(`
|
|
4153
4301
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
4154
4302
|
} catch (error) {
|
|
4155
4303
|
spinner.fail("Failed to delete backups");
|
|
4156
|
-
console.error(
|
|
4304
|
+
console.error(chalk27.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4157
4305
|
process.exit(1);
|
|
4158
4306
|
}
|
|
4159
4307
|
}
|
|
4160
4308
|
|
|
4161
4309
|
// src/commands/db/reindex.ts
|
|
4162
|
-
import { existsSync as
|
|
4163
|
-
import { join as
|
|
4164
|
-
import
|
|
4165
|
-
import { loadEnv as
|
|
4310
|
+
import { existsSync as existsSync24, readFileSync as readFileSync10, writeFileSync as writeFileSync15, renameSync, copyFileSync } from "fs";
|
|
4311
|
+
import { join as join22 } from "path";
|
|
4312
|
+
import chalk28 from "chalk";
|
|
4313
|
+
import { loadEnv as loadEnv9 } from "@spfn/core/server";
|
|
4166
4314
|
function isTimestampPrefix(tag) {
|
|
4167
4315
|
const prefix = tag.split("_")[0];
|
|
4168
4316
|
return /^\d{5,}$/.test(prefix);
|
|
@@ -4178,19 +4326,19 @@ function parseTag(tag) {
|
|
|
4178
4326
|
};
|
|
4179
4327
|
}
|
|
4180
4328
|
async function dbReindex(options = {}) {
|
|
4181
|
-
|
|
4329
|
+
loadEnv9();
|
|
4182
4330
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
4183
4331
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
4184
4332
|
const outDir = config.out;
|
|
4185
|
-
const journalPath =
|
|
4186
|
-
if (!
|
|
4187
|
-
console.error(
|
|
4188
|
-
console.log(
|
|
4333
|
+
const journalPath = join22(outDir, "meta", "_journal.json");
|
|
4334
|
+
if (!existsSync24(journalPath)) {
|
|
4335
|
+
console.error(chalk28.red("\u274C No _journal.json found at:"), journalPath);
|
|
4336
|
+
console.log(chalk28.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
4189
4337
|
process.exit(1);
|
|
4190
4338
|
}
|
|
4191
4339
|
const journal = JSON.parse(readFileSync10(journalPath, "utf-8"));
|
|
4192
4340
|
if (journal.entries.length === 0) {
|
|
4193
|
-
console.log(
|
|
4341
|
+
console.log(chalk28.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
4194
4342
|
return;
|
|
4195
4343
|
}
|
|
4196
4344
|
const renames = [];
|
|
@@ -4204,46 +4352,46 @@ async function dbReindex(options = {}) {
|
|
|
4204
4352
|
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
4205
4353
|
const newPrefix = String(entry.when);
|
|
4206
4354
|
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
4207
|
-
const oldSql =
|
|
4208
|
-
const newSql =
|
|
4209
|
-
if (
|
|
4355
|
+
const oldSql = join22(outDir, `${entry.tag}.sql`);
|
|
4356
|
+
const newSql = join22(outDir, `${newTag}.sql`);
|
|
4357
|
+
if (existsSync24(oldSql)) {
|
|
4210
4358
|
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
4211
4359
|
}
|
|
4212
|
-
const oldSnapshot =
|
|
4213
|
-
const newSnapshot =
|
|
4214
|
-
if (
|
|
4360
|
+
const oldSnapshot = join22(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
4361
|
+
const newSnapshot = join22(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
4362
|
+
if (existsSync24(oldSnapshot)) {
|
|
4215
4363
|
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
4216
4364
|
}
|
|
4217
4365
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
4218
4366
|
}
|
|
4219
4367
|
if (tagUpdates.length === 0) {
|
|
4220
|
-
console.log(
|
|
4368
|
+
console.log(chalk28.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
4221
4369
|
if (skipped > 0) {
|
|
4222
|
-
console.log(
|
|
4370
|
+
console.log(chalk28.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
4223
4371
|
}
|
|
4224
4372
|
return;
|
|
4225
4373
|
}
|
|
4226
|
-
console.log(
|
|
4374
|
+
console.log(chalk28.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
4227
4375
|
for (const update of tagUpdates) {
|
|
4228
4376
|
console.log(
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4377
|
+
chalk28.dim(` [${update.idx}]`),
|
|
4378
|
+
chalk28.red(update.oldTag),
|
|
4379
|
+
chalk28.dim("\u2192"),
|
|
4380
|
+
chalk28.green(update.newTag)
|
|
4233
4381
|
);
|
|
4234
4382
|
}
|
|
4235
|
-
console.log(
|
|
4383
|
+
console.log(chalk28.dim(`
|
|
4236
4384
|
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
4237
4385
|
if (skipped > 0) {
|
|
4238
|
-
console.log(
|
|
4386
|
+
console.log(chalk28.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
4239
4387
|
}
|
|
4240
4388
|
if (options.dryRun) {
|
|
4241
|
-
console.log(
|
|
4389
|
+
console.log(chalk28.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
4242
4390
|
return;
|
|
4243
4391
|
}
|
|
4244
4392
|
const backupPath = journalPath + ".bak";
|
|
4245
4393
|
copyFileSync(journalPath, backupPath);
|
|
4246
|
-
console.log(
|
|
4394
|
+
console.log(chalk28.dim(`
|
|
4247
4395
|
Backed up journal \u2192 ${backupPath}`));
|
|
4248
4396
|
for (const rename of renames) {
|
|
4249
4397
|
renameSync(rename.from, rename.to);
|
|
@@ -4255,7 +4403,7 @@ async function dbReindex(options = {}) {
|
|
|
4255
4403
|
}
|
|
4256
4404
|
}
|
|
4257
4405
|
writeFileSync15(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
4258
|
-
console.log(
|
|
4406
|
+
console.log(chalk28.green(`
|
|
4259
4407
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
4260
4408
|
}
|
|
4261
4409
|
|
|
@@ -4264,9 +4412,10 @@ var dbCommand = new Command9("db").description("Database management commands (wr
|
|
|
4264
4412
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
4265
4413
|
dbCommand.command("push").description("Push schema changes to database (safe mode by default)").option("--force", "Apply all changes including destructive ones").option("--dry-run", "Show changes without applying").action((options) => dbPush(options));
|
|
4266
4414
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
4415
|
+
dbCommand.command("status").description("Show applied/pending migration status (function packages + project)").action(dbStatus);
|
|
4267
4416
|
dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").option("-p, --port <port>", "Studio port (auto-finds if in use)").action((options) => dbStudio(options.port ? Number(options.port) : void 0));
|
|
4268
4417
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
4269
|
-
dbCommand.command("check").description("Check
|
|
4418
|
+
dbCommand.command("check").description("Check migration file consistency (drizzle-kit check)").action(dbCheck);
|
|
4270
4419
|
dbCommand.command("backup").description("Create a database backup").option("-f, --format <format>", "Backup format (sql or custom)", "sql").option("-o, --output <path>", "Custom output path").option("-s, --schema <name>", "Backup specific schema only").option("--data-only", "Backup data only (no schema)").option("--schema-only", "Backup schema only (no data)").option("--tag <tags>", "Comma-separated tags for this backup").option("--env <environment>", "Environment label (e.g., production, staging)").action((options) => dbBackup(options));
|
|
4271
4420
|
dbCommand.command("restore [file]").description("Restore database from backup").option("--drop", "Drop existing tables before restore").option("-s, --schema <name>", "Restore specific schema only").option("--data-only", "Restore data only (requires custom format .dump file)").option("--schema-only", "Restore schema only (requires custom format .dump file)").option("-v, --verbose", "Show detailed restore progress").action((file, options) => dbRestore(file, options));
|
|
4272
4421
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
@@ -4276,25 +4425,25 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
|
|
|
4276
4425
|
// src/commands/add.ts
|
|
4277
4426
|
init_package_manager();
|
|
4278
4427
|
import { Command as Command10 } from "commander";
|
|
4279
|
-
import { existsSync as
|
|
4280
|
-
import { join as
|
|
4428
|
+
import { existsSync as existsSync25, readFileSync as readFileSync11 } from "fs";
|
|
4429
|
+
import { join as join23 } from "path";
|
|
4281
4430
|
import { execa as execa9 } from "execa";
|
|
4282
|
-
import
|
|
4431
|
+
import chalk29 from "chalk";
|
|
4283
4432
|
import ora11 from "ora";
|
|
4284
4433
|
async function addPackage(packageName) {
|
|
4285
4434
|
if (!packageName.includes("/")) {
|
|
4286
|
-
console.error(
|
|
4287
|
-
console.log(
|
|
4288
|
-
console.log(
|
|
4289
|
-
console.log(
|
|
4435
|
+
console.error(chalk29.red("\u274C Please specify full package name"));
|
|
4436
|
+
console.log(chalk29.yellow("\n\u{1F4A1} Examples:"));
|
|
4437
|
+
console.log(chalk29.gray(" pnpm spfn add @spfn/cms"));
|
|
4438
|
+
console.log(chalk29.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
4290
4439
|
process.exit(1);
|
|
4291
4440
|
}
|
|
4292
|
-
console.log(
|
|
4441
|
+
console.log(chalk29.blue(`
|
|
4293
4442
|
\u{1F4E6} Setting up ${packageName}...
|
|
4294
4443
|
`));
|
|
4295
|
-
const pkgPath =
|
|
4296
|
-
const pkgJsonPath =
|
|
4297
|
-
if (!
|
|
4444
|
+
const pkgPath = join23(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
4445
|
+
const pkgJsonPath = join23(pkgPath, "package.json");
|
|
4446
|
+
if (!existsSync25(pkgJsonPath)) {
|
|
4298
4447
|
const pm = detectPackageManager(process.cwd());
|
|
4299
4448
|
const installSpinner = ora11("Installing package...").start();
|
|
4300
4449
|
try {
|
|
@@ -4302,26 +4451,27 @@ async function addPackage(packageName) {
|
|
|
4302
4451
|
installSpinner.succeed("Package installed");
|
|
4303
4452
|
} catch (error) {
|
|
4304
4453
|
installSpinner.fail("Failed to install package");
|
|
4305
|
-
console.error(
|
|
4454
|
+
console.error(chalk29.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4306
4455
|
process.exit(1);
|
|
4307
4456
|
}
|
|
4308
4457
|
} else {
|
|
4309
|
-
console.log(
|
|
4458
|
+
console.log(chalk29.gray("\u2713 Package already installed (using local version)\n"));
|
|
4310
4459
|
}
|
|
4311
|
-
if (!
|
|
4312
|
-
console.error(
|
|
4460
|
+
if (!existsSync25(pkgJsonPath)) {
|
|
4461
|
+
console.error(chalk29.red(`\u274C Package ${packageName} not found after installation`));
|
|
4313
4462
|
process.exit(1);
|
|
4314
4463
|
}
|
|
4315
4464
|
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
4316
4465
|
if (pkgJson.spfn?.migrations) {
|
|
4317
|
-
console.log(
|
|
4466
|
+
console.log(chalk29.blue(`
|
|
4318
4467
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
4319
4468
|
`));
|
|
4320
|
-
const { env:
|
|
4321
|
-
if (!
|
|
4322
|
-
console.log(
|
|
4323
|
-
console.log(
|
|
4324
|
-
console.log(
|
|
4469
|
+
const { env: env9 } = await import("@spfn/core/config");
|
|
4470
|
+
if (!env9.DATABASE_URL) {
|
|
4471
|
+
console.log(chalk29.yellow("\u26A0\uFE0F DATABASE_URL not found \u2014 skipping database setup."));
|
|
4472
|
+
console.log(chalk29.yellow(` ${packageName} tables are created by its bundled migrations, not by schema push.`));
|
|
4473
|
+
console.log(chalk29.cyan(" Once DATABASE_URL is set, run: pnpm spfn db migrate"));
|
|
4474
|
+
console.log(chalk29.gray(" (check state anytime with: pnpm spfn db status)\n"));
|
|
4325
4475
|
} else {
|
|
4326
4476
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4327
4477
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
@@ -4333,21 +4483,21 @@ async function addPackage(packageName) {
|
|
|
4333
4483
|
spinner.succeed("Migrations applied");
|
|
4334
4484
|
} catch (error) {
|
|
4335
4485
|
spinner.fail("Failed to apply migrations");
|
|
4336
|
-
console.error(
|
|
4486
|
+
console.error(chalk29.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4337
4487
|
process.exit(1);
|
|
4338
4488
|
}
|
|
4339
4489
|
} else {
|
|
4340
|
-
console.log(
|
|
4490
|
+
console.log(chalk29.gray("\u2139\uFE0F No migrations found for this package"));
|
|
4341
4491
|
}
|
|
4342
4492
|
}
|
|
4343
4493
|
} else {
|
|
4344
|
-
console.log(
|
|
4494
|
+
console.log(chalk29.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
4345
4495
|
}
|
|
4346
|
-
console.log(
|
|
4496
|
+
console.log(chalk29.green(`
|
|
4347
4497
|
\u2705 ${packageName} installed successfully!
|
|
4348
4498
|
`));
|
|
4349
4499
|
if (pkgJson.spfn?.setupMessage) {
|
|
4350
|
-
console.log(
|
|
4500
|
+
console.log(chalk29.cyan("\u{1F4DA} Setup Guide:"));
|
|
4351
4501
|
console.log(pkgJson.spfn.setupMessage);
|
|
4352
4502
|
console.log();
|
|
4353
4503
|
}
|
|
@@ -4356,8 +4506,8 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
|
|
|
4356
4506
|
|
|
4357
4507
|
// src/commands/env.ts
|
|
4358
4508
|
import { Command as Command11 } from "commander";
|
|
4359
|
-
import
|
|
4360
|
-
import { existsSync as
|
|
4509
|
+
import chalk30 from "chalk";
|
|
4510
|
+
import { existsSync as existsSync26, readFileSync as readFileSync12, writeFileSync as writeFileSync16 } from "fs";
|
|
4361
4511
|
import { resolve as resolve2 } from "path";
|
|
4362
4512
|
import { parse as parse2 } from "dotenv";
|
|
4363
4513
|
|
|
@@ -4413,26 +4563,26 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4413
4563
|
}
|
|
4414
4564
|
function formatType(type) {
|
|
4415
4565
|
const typeColors = {
|
|
4416
|
-
string:
|
|
4417
|
-
number:
|
|
4418
|
-
boolean:
|
|
4419
|
-
url:
|
|
4420
|
-
enum:
|
|
4421
|
-
json:
|
|
4566
|
+
string: chalk30.green,
|
|
4567
|
+
number: chalk30.blue,
|
|
4568
|
+
boolean: chalk30.yellow,
|
|
4569
|
+
url: chalk30.cyan,
|
|
4570
|
+
enum: chalk30.magenta,
|
|
4571
|
+
json: chalk30.red
|
|
4422
4572
|
};
|
|
4423
|
-
return (typeColors[type] ||
|
|
4573
|
+
return (typeColors[type] || chalk30.white)(type);
|
|
4424
4574
|
}
|
|
4425
4575
|
function formatDefault(value, type) {
|
|
4426
4576
|
if (value === void 0) {
|
|
4427
|
-
return
|
|
4577
|
+
return chalk30.dim("(none)");
|
|
4428
4578
|
}
|
|
4429
4579
|
if (type === "string" || type === "url") {
|
|
4430
|
-
return
|
|
4580
|
+
return chalk30.green(`"${value}"`);
|
|
4431
4581
|
}
|
|
4432
4582
|
if (type === "boolean") {
|
|
4433
|
-
return value ?
|
|
4583
|
+
return value ? chalk30.green("true") : chalk30.red("false");
|
|
4434
4584
|
}
|
|
4435
|
-
return
|
|
4585
|
+
return chalk30.cyan(String(value));
|
|
4436
4586
|
}
|
|
4437
4587
|
async function listEnvVars(options) {
|
|
4438
4588
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4446,28 +4596,28 @@ async function listEnvVars(options) {
|
|
|
4446
4596
|
acc[target].push([key, schema]);
|
|
4447
4597
|
return acc;
|
|
4448
4598
|
}, {});
|
|
4449
|
-
console.log(
|
|
4599
|
+
console.log(chalk30.blue.bold(`
|
|
4450
4600
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4451
4601
|
`));
|
|
4452
4602
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4453
|
-
console.log(
|
|
4603
|
+
console.log(chalk30.bold.magenta(`
|
|
4454
4604
|
${file}`));
|
|
4455
|
-
console.log(
|
|
4605
|
+
console.log(chalk30.dim("\u2500".repeat(50)));
|
|
4456
4606
|
for (const [key, schema] of vars) {
|
|
4457
4607
|
printEnvVar(key, schema);
|
|
4458
4608
|
}
|
|
4459
4609
|
}
|
|
4460
4610
|
} else {
|
|
4461
|
-
console.log(
|
|
4611
|
+
console.log(chalk30.blue.bold(`
|
|
4462
4612
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4463
4613
|
`));
|
|
4464
4614
|
for (const [key, schema] of allVars) {
|
|
4465
4615
|
printEnvVar(key, schema, true);
|
|
4466
4616
|
}
|
|
4467
4617
|
}
|
|
4468
|
-
console.log(
|
|
4618
|
+
console.log(chalk30.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4469
4619
|
} catch (error) {
|
|
4470
|
-
console.error(
|
|
4620
|
+
console.error(chalk30.red(`
|
|
4471
4621
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4472
4622
|
`));
|
|
4473
4623
|
process.exit(1);
|
|
@@ -4475,17 +4625,17 @@ ${file}`));
|
|
|
4475
4625
|
}
|
|
4476
4626
|
function printEnvVar(key, schema, showFile = false) {
|
|
4477
4627
|
const typeStr = formatType(schema.type);
|
|
4478
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4479
|
-
const sensitiveStr = schema.sensitive ?
|
|
4480
|
-
const fileStr = showFile ?
|
|
4481
|
-
console.log(`${
|
|
4482
|
-
console.log(` ${
|
|
4628
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk30.red("[required]") : chalk30.dim("[optional]");
|
|
4629
|
+
const sensitiveStr = schema.sensitive ? chalk30.yellow(" [sensitive]") : "";
|
|
4630
|
+
const fileStr = showFile ? chalk30.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
4631
|
+
console.log(`${chalk30.bold.cyan(key)} ${chalk30.dim("(")}${typeStr}${chalk30.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
4632
|
+
console.log(` ${chalk30.dim(schema.description)}`);
|
|
4483
4633
|
if (schema.default !== void 0) {
|
|
4484
|
-
console.log(` ${
|
|
4634
|
+
console.log(` ${chalk30.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4485
4635
|
}
|
|
4486
4636
|
if (schema.examples && schema.examples.length > 0) {
|
|
4487
4637
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4488
|
-
console.log(` ${
|
|
4638
|
+
console.log(` ${chalk30.dim("Examples:")} ${exampleStr}`);
|
|
4489
4639
|
}
|
|
4490
4640
|
console.log();
|
|
4491
4641
|
}
|
|
@@ -4493,7 +4643,7 @@ async function showEnvStats(options) {
|
|
|
4493
4643
|
const packageName = options.package || "@spfn/core";
|
|
4494
4644
|
try {
|
|
4495
4645
|
const envSchema = await loadEnvSchema(packageName);
|
|
4496
|
-
console.log(
|
|
4646
|
+
console.log(chalk30.blue.bold(`
|
|
4497
4647
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4498
4648
|
`));
|
|
4499
4649
|
const allVars = Object.entries(envSchema);
|
|
@@ -4515,24 +4665,24 @@ async function showEnvStats(options) {
|
|
|
4515
4665
|
acc[file] = (acc[file] || 0) + 1;
|
|
4516
4666
|
return acc;
|
|
4517
4667
|
}, {});
|
|
4518
|
-
console.log(`${
|
|
4519
|
-
console.log(`${
|
|
4520
|
-
console.log(`${
|
|
4521
|
-
console.log(`${
|
|
4522
|
-
console.log(
|
|
4523
|
-
console.log(` ${
|
|
4524
|
-
console.log(` ${
|
|
4525
|
-
console.log(
|
|
4668
|
+
console.log(`${chalk30.bold("Total variables:")} ${chalk30.cyan(allVars.length)}`);
|
|
4669
|
+
console.log(`${chalk30.bold("Required:")} ${chalk30.red(required.length)}`);
|
|
4670
|
+
console.log(`${chalk30.bold("Optional:")} ${chalk30.dim(optional.length)}`);
|
|
4671
|
+
console.log(`${chalk30.bold("Sensitive:")} ${chalk30.yellow(sensitive.length)}`);
|
|
4672
|
+
console.log(chalk30.bold("\nBy Target:"));
|
|
4673
|
+
console.log(` ${chalk30.blue("Next.js accessible:")} ${chalk30.cyan(nextjsVars.length)}`);
|
|
4674
|
+
console.log(` ${chalk30.magenta("SPFN server only:")} ${chalk30.cyan(serverOnlyVars.length)}`);
|
|
4675
|
+
console.log(chalk30.bold("\nBy File:"));
|
|
4526
4676
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4527
|
-
console.log(` ${
|
|
4677
|
+
console.log(` ${chalk30.dim(file)}: ${chalk30.cyan(count)}`);
|
|
4528
4678
|
}
|
|
4529
|
-
console.log(
|
|
4679
|
+
console.log(chalk30.bold("\nBy Type:"));
|
|
4530
4680
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4531
|
-
console.log(` ${formatType(type)}: ${
|
|
4681
|
+
console.log(` ${formatType(type)}: ${chalk30.cyan(count)}`);
|
|
4532
4682
|
}
|
|
4533
4683
|
console.log();
|
|
4534
4684
|
} catch (error) {
|
|
4535
|
-
console.error(
|
|
4685
|
+
console.error(chalk30.red(`
|
|
4536
4686
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4537
4687
|
`));
|
|
4538
4688
|
process.exit(1);
|
|
@@ -4552,26 +4702,26 @@ async function searchEnvVars(query, options) {
|
|
|
4552
4702
|
}
|
|
4553
4703
|
}
|
|
4554
4704
|
if (results.length === 0) {
|
|
4555
|
-
console.log(
|
|
4705
|
+
console.log(chalk30.yellow(`
|
|
4556
4706
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4557
4707
|
`));
|
|
4558
4708
|
return;
|
|
4559
4709
|
}
|
|
4560
|
-
console.log(
|
|
4710
|
+
console.log(chalk30.blue.bold(`
|
|
4561
4711
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4562
4712
|
`));
|
|
4563
4713
|
for (const [key, schema] of results) {
|
|
4564
4714
|
const typeStr = formatType(schema.type);
|
|
4565
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4566
|
-
console.log(`${
|
|
4567
|
-
console.log(` ${
|
|
4715
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk30.red("[required]") : chalk30.dim("[optional]");
|
|
4716
|
+
console.log(`${chalk30.bold.cyan(key)} ${chalk30.dim("(")}${typeStr}${chalk30.dim(")")} ${requiredStr}`);
|
|
4717
|
+
console.log(` ${chalk30.dim(schema.description)}`);
|
|
4568
4718
|
if (schema.default !== void 0) {
|
|
4569
|
-
console.log(` ${
|
|
4719
|
+
console.log(` ${chalk30.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4570
4720
|
}
|
|
4571
4721
|
console.log();
|
|
4572
4722
|
}
|
|
4573
4723
|
} catch (error) {
|
|
4574
|
-
console.error(
|
|
4724
|
+
console.error(chalk30.red(`
|
|
4575
4725
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4576
4726
|
`));
|
|
4577
4727
|
process.exit(1);
|
|
@@ -4583,9 +4733,9 @@ envCommand.command("stats").description("Show environment variable statistics").
|
|
|
4583
4733
|
envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(searchEnvVars);
|
|
4584
4734
|
function validateEnvOption(envValue) {
|
|
4585
4735
|
if (!VALID_ENVS.includes(envValue)) {
|
|
4586
|
-
console.error(
|
|
4736
|
+
console.error(chalk30.red(`
|
|
4587
4737
|
\u274C Invalid environment: "${envValue}"`));
|
|
4588
|
-
console.log(
|
|
4738
|
+
console.log(chalk30.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
4589
4739
|
`));
|
|
4590
4740
|
process.exit(1);
|
|
4591
4741
|
}
|
|
@@ -4606,8 +4756,8 @@ async function initEnvFiles(options) {
|
|
|
4606
4756
|
return acc;
|
|
4607
4757
|
}, {});
|
|
4608
4758
|
if (targetEnv) {
|
|
4609
|
-
console.log(
|
|
4610
|
-
\u{1F680} Generating .env template files for ${
|
|
4759
|
+
console.log(chalk30.blue.bold(`
|
|
4760
|
+
\u{1F680} Generating .env template files for ${chalk30.cyan(targetEnv)} environment
|
|
4611
4761
|
`));
|
|
4612
4762
|
const envSpecificFiles = {};
|
|
4613
4763
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -4623,24 +4773,24 @@ async function initEnvFiles(options) {
|
|
|
4623
4773
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4624
4774
|
}
|
|
4625
4775
|
} else {
|
|
4626
|
-
console.log(
|
|
4776
|
+
console.log(chalk30.blue.bold(`
|
|
4627
4777
|
\u{1F680} Generating .env template files
|
|
4628
4778
|
`));
|
|
4629
4779
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4630
4780
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4631
4781
|
}
|
|
4632
4782
|
}
|
|
4633
|
-
console.log(
|
|
4634
|
-
console.log(
|
|
4635
|
-
console.log(
|
|
4636
|
-
console.log(
|
|
4783
|
+
console.log(chalk30.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
4784
|
+
console.log(chalk30.dim(" cp .env.example .env"));
|
|
4785
|
+
console.log(chalk30.dim(" cp .env.local.example .env.local"));
|
|
4786
|
+
console.log(chalk30.dim(" cp .env.server.example .env.server"));
|
|
4637
4787
|
if (targetEnv) {
|
|
4638
|
-
console.log(
|
|
4639
|
-
console.log(
|
|
4788
|
+
console.log(chalk30.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
4789
|
+
console.log(chalk30.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
4640
4790
|
}
|
|
4641
4791
|
console.log("");
|
|
4642
4792
|
} catch (error) {
|
|
4643
|
-
console.error(
|
|
4793
|
+
console.error(chalk30.red(`
|
|
4644
4794
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4645
4795
|
`));
|
|
4646
4796
|
process.exit(1);
|
|
@@ -4648,12 +4798,12 @@ async function initEnvFiles(options) {
|
|
|
4648
4798
|
}
|
|
4649
4799
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
4650
4800
|
const filePath = resolve2(cwd, file);
|
|
4651
|
-
if (
|
|
4652
|
-
console.log(
|
|
4801
|
+
if (existsSync26(filePath) && !force) {
|
|
4802
|
+
console.log(chalk30.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4653
4803
|
return;
|
|
4654
4804
|
}
|
|
4655
4805
|
writeFileSync16(filePath, generateEnvFileContent(vars), "utf-8");
|
|
4656
|
-
console.log(
|
|
4806
|
+
console.log(chalk30.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4657
4807
|
}
|
|
4658
4808
|
function generateEnvFileContent(vars) {
|
|
4659
4809
|
const lines = [
|
|
@@ -4688,7 +4838,7 @@ async function checkEnvFiles(options) {
|
|
|
4688
4838
|
const envSchema = await loadEnvSchema(packageName);
|
|
4689
4839
|
const allVars = Object.entries(envSchema);
|
|
4690
4840
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
4691
|
-
console.log(
|
|
4841
|
+
console.log(chalk30.blue.bold(`
|
|
4692
4842
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4693
4843
|
`));
|
|
4694
4844
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -4697,7 +4847,7 @@ async function checkEnvFiles(options) {
|
|
|
4697
4847
|
const warnings = [];
|
|
4698
4848
|
for (const file of filesToCheck) {
|
|
4699
4849
|
const filePath = resolve2(cwd, file);
|
|
4700
|
-
if (!
|
|
4850
|
+
if (!existsSync26(filePath)) {
|
|
4701
4851
|
continue;
|
|
4702
4852
|
}
|
|
4703
4853
|
const content = readFileSync12(filePath, "utf-8");
|
|
@@ -4705,7 +4855,7 @@ async function checkEnvFiles(options) {
|
|
|
4705
4855
|
for (const [key, value] of Object.entries(parsed)) {
|
|
4706
4856
|
loadedEnv[key] = { value: value || "", file };
|
|
4707
4857
|
}
|
|
4708
|
-
console.log(
|
|
4858
|
+
console.log(chalk30.dim(` \u{1F4C4} ${file} loaded`));
|
|
4709
4859
|
}
|
|
4710
4860
|
console.log("");
|
|
4711
4861
|
for (const [key, schema] of allVars) {
|
|
@@ -4713,7 +4863,7 @@ async function checkEnvFiles(options) {
|
|
|
4713
4863
|
const found = loadedEnv[key];
|
|
4714
4864
|
if (!found) {
|
|
4715
4865
|
if (schema.required && schema.default === void 0) {
|
|
4716
|
-
issues.push(`${
|
|
4866
|
+
issues.push(`${chalk30.red("\u2717")} ${chalk30.cyan(key)} is required but not found in any .env file`);
|
|
4717
4867
|
}
|
|
4718
4868
|
continue;
|
|
4719
4869
|
}
|
|
@@ -4723,11 +4873,11 @@ async function checkEnvFiles(options) {
|
|
|
4723
4873
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4724
4874
|
if (schema.sensitive) {
|
|
4725
4875
|
issues.push(
|
|
4726
|
-
`${
|
|
4876
|
+
`${chalk30.red("\u2717")} ${chalk30.cyan(key)} is sensitive and should be in ${chalk30.magenta(expectedFile)}, but found in ${chalk30.yellow(found.file)} (security risk!)`
|
|
4727
4877
|
);
|
|
4728
4878
|
} else {
|
|
4729
4879
|
warnings.push(
|
|
4730
|
-
`${
|
|
4880
|
+
`${chalk30.yellow("\u26A0")} ${chalk30.cyan(key)} should be in ${chalk30.magenta(expectedFile)}, but found in ${chalk30.dim(found.file)}`
|
|
4731
4881
|
);
|
|
4732
4882
|
}
|
|
4733
4883
|
}
|
|
@@ -4735,34 +4885,34 @@ async function checkEnvFiles(options) {
|
|
|
4735
4885
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4736
4886
|
const inSchema = allVars.some(([k]) => k === key);
|
|
4737
4887
|
if (!inSchema) {
|
|
4738
|
-
warnings.push(`${
|
|
4888
|
+
warnings.push(`${chalk30.yellow("\u26A0")} ${chalk30.cyan(key)} in ${chalk30.dim(file)} is not in schema`);
|
|
4739
4889
|
}
|
|
4740
4890
|
}
|
|
4741
4891
|
if (issues.length > 0) {
|
|
4742
|
-
console.log(
|
|
4892
|
+
console.log(chalk30.red.bold("Issues:"));
|
|
4743
4893
|
for (const issue of issues) {
|
|
4744
4894
|
console.log(` ${issue}`);
|
|
4745
4895
|
}
|
|
4746
4896
|
console.log("");
|
|
4747
4897
|
}
|
|
4748
4898
|
if (warnings.length > 0) {
|
|
4749
|
-
console.log(
|
|
4899
|
+
console.log(chalk30.yellow.bold("Warnings:"));
|
|
4750
4900
|
for (const warning of warnings) {
|
|
4751
4901
|
console.log(` ${warning}`);
|
|
4752
4902
|
}
|
|
4753
4903
|
console.log("");
|
|
4754
4904
|
}
|
|
4755
4905
|
if (issues.length === 0 && warnings.length === 0) {
|
|
4756
|
-
console.log(
|
|
4906
|
+
console.log(chalk30.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4757
4907
|
} else {
|
|
4758
|
-
console.log(
|
|
4908
|
+
console.log(chalk30.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4759
4909
|
`));
|
|
4760
4910
|
if (issues.length > 0) {
|
|
4761
4911
|
process.exit(1);
|
|
4762
4912
|
}
|
|
4763
4913
|
}
|
|
4764
4914
|
} catch (error) {
|
|
4765
|
-
console.error(
|
|
4915
|
+
console.error(chalk30.red(`
|
|
4766
4916
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4767
4917
|
`));
|
|
4768
4918
|
process.exit(1);
|
|
@@ -4774,17 +4924,17 @@ async function validateEnvVars(options) {
|
|
|
4774
4924
|
const packages = options.packages || ["@spfn/core"];
|
|
4775
4925
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4776
4926
|
if (targetEnv) {
|
|
4777
|
-
const { loadEnv:
|
|
4778
|
-
const result =
|
|
4779
|
-
console.log(
|
|
4780
|
-
\u{1F50D} Validating environment variables for ${
|
|
4927
|
+
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
4928
|
+
const result = loadEnv10({ nodeEnv: targetEnv });
|
|
4929
|
+
console.log(chalk30.blue.bold(`
|
|
4930
|
+
\u{1F50D} Validating environment variables for ${chalk30.cyan(targetEnv)}
|
|
4781
4931
|
`));
|
|
4782
4932
|
if (result.loadedFiles.length > 0) {
|
|
4783
|
-
console.log(
|
|
4933
|
+
console.log(chalk30.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
4784
4934
|
}
|
|
4785
4935
|
console.log("");
|
|
4786
4936
|
} else {
|
|
4787
|
-
console.log(
|
|
4937
|
+
console.log(chalk30.blue.bold(`
|
|
4788
4938
|
\u{1F50D} Validating environment variables
|
|
4789
4939
|
`));
|
|
4790
4940
|
}
|
|
@@ -4792,7 +4942,7 @@ async function validateEnvVars(options) {
|
|
|
4792
4942
|
const allWarnings = [];
|
|
4793
4943
|
for (const packageName of packages) {
|
|
4794
4944
|
try {
|
|
4795
|
-
console.log(
|
|
4945
|
+
console.log(chalk30.dim(` \u{1F4E6} ${packageName}`));
|
|
4796
4946
|
const envSchema = await loadEnvSchema(packageName);
|
|
4797
4947
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4798
4948
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -4805,10 +4955,10 @@ async function validateEnvVars(options) {
|
|
|
4805
4955
|
}
|
|
4806
4956
|
} catch (error) {
|
|
4807
4957
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4808
|
-
console.log(
|
|
4958
|
+
console.log(chalk30.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4809
4959
|
continue;
|
|
4810
4960
|
}
|
|
4811
|
-
console.error(
|
|
4961
|
+
console.error(chalk30.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4812
4962
|
if (options.strict) {
|
|
4813
4963
|
process.exit(1);
|
|
4814
4964
|
}
|
|
@@ -4816,32 +4966,32 @@ async function validateEnvVars(options) {
|
|
|
4816
4966
|
}
|
|
4817
4967
|
console.log("");
|
|
4818
4968
|
if (allErrors.length > 0) {
|
|
4819
|
-
console.log(
|
|
4969
|
+
console.log(chalk30.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4820
4970
|
`));
|
|
4821
4971
|
for (const error of allErrors) {
|
|
4822
|
-
console.log(` ${
|
|
4823
|
-
console.log(` ${
|
|
4824
|
-
console.log(` ${
|
|
4972
|
+
console.log(` ${chalk30.red("\u2717")} ${chalk30.cyan(error.key)}`);
|
|
4973
|
+
console.log(` ${chalk30.dim(error.message)}`);
|
|
4974
|
+
console.log(` ${chalk30.dim(`from ${error.package}`)}`);
|
|
4825
4975
|
console.log("");
|
|
4826
4976
|
}
|
|
4827
4977
|
}
|
|
4828
4978
|
if (allWarnings.length > 0) {
|
|
4829
|
-
console.log(
|
|
4979
|
+
console.log(chalk30.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4830
4980
|
`));
|
|
4831
4981
|
for (const warning of allWarnings) {
|
|
4832
|
-
console.log(` ${
|
|
4833
|
-
console.log(` ${
|
|
4982
|
+
console.log(` ${chalk30.yellow("\u26A0")} ${chalk30.cyan(warning.key)}`);
|
|
4983
|
+
console.log(` ${chalk30.dim(warning.message)}`);
|
|
4834
4984
|
console.log("");
|
|
4835
4985
|
}
|
|
4836
4986
|
}
|
|
4837
4987
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4838
|
-
console.log(
|
|
4988
|
+
console.log(chalk30.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4839
4989
|
} else if (allErrors.length === 0) {
|
|
4840
|
-
console.log(
|
|
4841
|
-
console.log(
|
|
4990
|
+
console.log(chalk30.green("\u2705 No errors found."));
|
|
4991
|
+
console.log(chalk30.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4842
4992
|
`));
|
|
4843
4993
|
} else {
|
|
4844
|
-
console.log(
|
|
4994
|
+
console.log(chalk30.red(`
|
|
4845
4995
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4846
4996
|
`));
|
|
4847
4997
|
process.exit(1);
|
|
@@ -4855,12 +5005,12 @@ import { Command as Command12 } from "commander";
|
|
|
4855
5005
|
// src/commands/secret/set.ts
|
|
4856
5006
|
init_logger();
|
|
4857
5007
|
import prompts8 from "prompts";
|
|
4858
|
-
import
|
|
5008
|
+
import chalk31 from "chalk";
|
|
4859
5009
|
|
|
4860
5010
|
// src/commands/secret/options.ts
|
|
4861
5011
|
init_logger();
|
|
4862
|
-
function resolveEnv(
|
|
4863
|
-
const value =
|
|
5012
|
+
function resolveEnv(env9) {
|
|
5013
|
+
const value = env9 ?? "local";
|
|
4864
5014
|
if (!VALID_ENVS.includes(value)) {
|
|
4865
5015
|
logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
|
|
4866
5016
|
process.exit(1);
|
|
@@ -4870,12 +5020,12 @@ function resolveEnv(env8) {
|
|
|
4870
5020
|
|
|
4871
5021
|
// src/commands/secret/store-value.ts
|
|
4872
5022
|
init_logger();
|
|
4873
|
-
import { join as
|
|
5023
|
+
import { join as join25 } from "path";
|
|
4874
5024
|
init_env_file();
|
|
4875
5025
|
|
|
4876
5026
|
// src/utils/sops.ts
|
|
4877
5027
|
import { execa as execa10 } from "execa";
|
|
4878
|
-
import { existsSync as
|
|
5028
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync3, writeFileSync as writeFileSync17 } from "fs";
|
|
4879
5029
|
import { dirname as dirname3 } from "path";
|
|
4880
5030
|
async function ensureSopsInstalled() {
|
|
4881
5031
|
try {
|
|
@@ -4885,14 +5035,14 @@ async function ensureSopsInstalled() {
|
|
|
4885
5035
|
}
|
|
4886
5036
|
}
|
|
4887
5037
|
async function sopsDecrypt(absFile) {
|
|
4888
|
-
if (!
|
|
5038
|
+
if (!existsSync27(absFile)) {
|
|
4889
5039
|
return {};
|
|
4890
5040
|
}
|
|
4891
5041
|
const { stdout } = await execa10("sops", ["--decrypt", "--output-type", "json", absFile]);
|
|
4892
5042
|
return JSON.parse(stdout);
|
|
4893
5043
|
}
|
|
4894
5044
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
4895
|
-
if (
|
|
5045
|
+
if (existsSync27(absFile)) {
|
|
4896
5046
|
await execa10("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
|
|
4897
5047
|
return;
|
|
4898
5048
|
}
|
|
@@ -4918,20 +5068,20 @@ async function sopsUpdateKeys(absFile) {
|
|
|
4918
5068
|
}
|
|
4919
5069
|
|
|
4920
5070
|
// src/utils/secret-config.ts
|
|
4921
|
-
import { existsSync as
|
|
4922
|
-
import { join as
|
|
5071
|
+
import { existsSync as existsSync28, readdirSync as readdirSync3 } from "fs";
|
|
5072
|
+
import { join as join24 } from "path";
|
|
4923
5073
|
var SECRETS_DIR = "secrets";
|
|
4924
|
-
function getSopsFile(cwd,
|
|
4925
|
-
const relFile = `${SECRETS_DIR}/${
|
|
4926
|
-
return { absFile:
|
|
5074
|
+
function getSopsFile(cwd, env9) {
|
|
5075
|
+
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
5076
|
+
return { absFile: join24(cwd, relFile), relFile };
|
|
4927
5077
|
}
|
|
4928
5078
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
4929
5079
|
let dir = cwd;
|
|
4930
5080
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
4931
|
-
if (
|
|
5081
|
+
if (existsSync28(join24(dir, filename))) {
|
|
4932
5082
|
return dir;
|
|
4933
5083
|
}
|
|
4934
|
-
const parent =
|
|
5084
|
+
const parent = join24(dir, "..");
|
|
4935
5085
|
if (parent === dir) {
|
|
4936
5086
|
break;
|
|
4937
5087
|
}
|
|
@@ -4941,22 +5091,22 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
4941
5091
|
}
|
|
4942
5092
|
function findSopsConfig(cwd) {
|
|
4943
5093
|
const dir = findUp(cwd, ".sops.yaml");
|
|
4944
|
-
return dir ?
|
|
5094
|
+
return dir ? join24(dir, ".sops.yaml") : null;
|
|
4945
5095
|
}
|
|
4946
5096
|
function hasSopsConfig(cwd) {
|
|
4947
5097
|
return findSopsConfig(cwd) !== null;
|
|
4948
5098
|
}
|
|
4949
5099
|
function listSopsFiles(cwd) {
|
|
4950
|
-
const dir =
|
|
4951
|
-
if (!
|
|
5100
|
+
const dir = join24(cwd, SECRETS_DIR);
|
|
5101
|
+
if (!existsSync28(dir)) {
|
|
4952
5102
|
return [];
|
|
4953
5103
|
}
|
|
4954
|
-
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) =>
|
|
5104
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join24(dir, name));
|
|
4955
5105
|
}
|
|
4956
5106
|
|
|
4957
5107
|
// src/commands/secret/store-value.ts
|
|
4958
|
-
function isLocalEnv(
|
|
4959
|
-
return
|
|
5108
|
+
function isLocalEnv(env9) {
|
|
5109
|
+
return env9 === "local";
|
|
4960
5110
|
}
|
|
4961
5111
|
var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4962
5112
|
function assertValidKey(key) {
|
|
@@ -4964,15 +5114,15 @@ function assertValidKey(key) {
|
|
|
4964
5114
|
throw new Error(`Invalid secret key "${key}". Use an env-var name: letters, digits, and underscores, not starting with a digit.`);
|
|
4965
5115
|
}
|
|
4966
5116
|
}
|
|
4967
|
-
async function describeTarget(
|
|
4968
|
-
if (isLocalEnv(
|
|
5117
|
+
async function describeTarget(env9) {
|
|
5118
|
+
if (isLocalEnv(env9)) {
|
|
4969
5119
|
return detectStore().label;
|
|
4970
5120
|
}
|
|
4971
|
-
return `SOPS (${getSopsFile(process.cwd(),
|
|
5121
|
+
return `SOPS (${getSopsFile(process.cwd(), env9).relFile})`;
|
|
4972
5122
|
}
|
|
4973
|
-
async function storeSecret(cwd,
|
|
5123
|
+
async function storeSecret(cwd, env9, key, value) {
|
|
4974
5124
|
assertValidKey(key);
|
|
4975
|
-
if (isLocalEnv(
|
|
5125
|
+
if (isLocalEnv(env9)) {
|
|
4976
5126
|
const store = detectStore();
|
|
4977
5127
|
if (!await store.isAvailable()) {
|
|
4978
5128
|
throw new Error(
|
|
@@ -4980,7 +5130,7 @@ async function storeSecret(cwd, env8, key, value) {
|
|
|
4980
5130
|
);
|
|
4981
5131
|
}
|
|
4982
5132
|
await store.set(keychainName(key), value);
|
|
4983
|
-
const serverEnvPath =
|
|
5133
|
+
const serverEnvPath = join25(cwd, ".env.server");
|
|
4984
5134
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
4985
5135
|
restrictEnvFilePerms(serverEnvPath);
|
|
4986
5136
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -4993,14 +5143,14 @@ async function storeSecret(cwd, env8, key, value) {
|
|
|
4993
5143
|
".sops.yaml not found \u2014 add a backend (run `spfn secret keygen` for age, or a KMS rule) so the file can be decrypted on deploy."
|
|
4994
5144
|
);
|
|
4995
5145
|
}
|
|
4996
|
-
const { absFile, relFile } = getSopsFile(cwd,
|
|
5146
|
+
const { absFile, relFile } = getSopsFile(cwd, env9);
|
|
4997
5147
|
await sopsSetValue(absFile, relFile, key, value);
|
|
4998
5148
|
logger.success(`Set ${key} in ${relFile} (encrypted). Commit and deploy to apply.`);
|
|
4999
5149
|
}
|
|
5000
5150
|
|
|
5001
5151
|
// src/commands/secret/set.ts
|
|
5002
5152
|
async function secretSet(key, options) {
|
|
5003
|
-
const
|
|
5153
|
+
const env9 = resolveEnv(options.env);
|
|
5004
5154
|
const pkg = options.package ?? "@spfn/core";
|
|
5005
5155
|
const resolvedKey = key ?? await pickSecretKey(pkg);
|
|
5006
5156
|
if (!resolvedKey) {
|
|
@@ -5011,15 +5161,15 @@ async function secretSet(key, options) {
|
|
|
5011
5161
|
const { value } = await prompts8({
|
|
5012
5162
|
type: "password",
|
|
5013
5163
|
name: "value",
|
|
5014
|
-
message: `Value for ${
|
|
5164
|
+
message: `Value for ${chalk31.cyan(resolvedKey)} (${env9})`
|
|
5015
5165
|
});
|
|
5016
5166
|
if (!value) {
|
|
5017
5167
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
5018
5168
|
process.exit(0);
|
|
5019
5169
|
}
|
|
5020
|
-
logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(
|
|
5170
|
+
logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(env9)}`);
|
|
5021
5171
|
try {
|
|
5022
|
-
await storeSecret(process.cwd(),
|
|
5172
|
+
await storeSecret(process.cwd(), env9, resolvedKey, value);
|
|
5023
5173
|
} catch (error) {
|
|
5024
5174
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
5025
5175
|
process.exit(1);
|
|
@@ -5060,9 +5210,9 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
5060
5210
|
|
|
5061
5211
|
// src/commands/secret/list.ts
|
|
5062
5212
|
init_logger();
|
|
5063
|
-
import
|
|
5213
|
+
import chalk32 from "chalk";
|
|
5064
5214
|
async function secretList(options) {
|
|
5065
|
-
const
|
|
5215
|
+
const env9 = resolveEnv(options.env);
|
|
5066
5216
|
const pkg = options.package ?? "@spfn/core";
|
|
5067
5217
|
let entries;
|
|
5068
5218
|
try {
|
|
@@ -5075,18 +5225,18 @@ async function secretList(options) {
|
|
|
5075
5225
|
logger.info(`No secrets declared in ${pkg}.`);
|
|
5076
5226
|
return;
|
|
5077
5227
|
}
|
|
5078
|
-
const present = await loadPresence(
|
|
5079
|
-
console.log(
|
|
5080
|
-
\u{1F511} Secrets (${pkg}) \u2014 ${
|
|
5228
|
+
const present = await loadPresence(env9, entries);
|
|
5229
|
+
console.log(chalk32.blue.bold(`
|
|
5230
|
+
\u{1F511} Secrets (${pkg}) \u2014 ${env9}
|
|
5081
5231
|
`));
|
|
5082
5232
|
for (const entry of entries) {
|
|
5083
5233
|
const status = statusOf(entry, present.has(entry.key));
|
|
5084
|
-
console.log(` ${badge(status)} ${
|
|
5234
|
+
console.log(` ${badge(status)} ${chalk32.cyan(entry.key)}${entry.generate ? chalk32.dim(" (generatable)") : ""}`);
|
|
5085
5235
|
}
|
|
5086
5236
|
console.log();
|
|
5087
5237
|
}
|
|
5088
|
-
async function loadPresence(
|
|
5089
|
-
if (isLocalEnv(
|
|
5238
|
+
async function loadPresence(env9, entries) {
|
|
5239
|
+
if (isLocalEnv(env9)) {
|
|
5090
5240
|
const store = detectStore();
|
|
5091
5241
|
if (!await store.isAvailable()) {
|
|
5092
5242
|
logger.warn(`${store.label} unavailable \u2014 status shown as missing.`);
|
|
@@ -5101,7 +5251,7 @@ async function loadPresence(env8, entries) {
|
|
|
5101
5251
|
return present;
|
|
5102
5252
|
}
|
|
5103
5253
|
await ensureSopsInstalled();
|
|
5104
|
-
const { absFile } = getSopsFile(process.cwd(),
|
|
5254
|
+
const { absFile } = getSopsFile(process.cwd(), env9);
|
|
5105
5255
|
const decrypted = await sopsDecrypt(absFile);
|
|
5106
5256
|
return new Set(Object.keys(decrypted));
|
|
5107
5257
|
}
|
|
@@ -5114,19 +5264,19 @@ function statusOf(entry, present) {
|
|
|
5114
5264
|
function badge(status) {
|
|
5115
5265
|
switch (status) {
|
|
5116
5266
|
case "set":
|
|
5117
|
-
return
|
|
5267
|
+
return chalk32.green("\u25CF");
|
|
5118
5268
|
case "missing":
|
|
5119
|
-
return
|
|
5269
|
+
return chalk32.yellow("\u25CB");
|
|
5120
5270
|
case "awaiting-input":
|
|
5121
|
-
return
|
|
5271
|
+
return chalk32.red("\u25CB");
|
|
5122
5272
|
}
|
|
5123
5273
|
}
|
|
5124
5274
|
|
|
5125
5275
|
// src/commands/secret/generate.ts
|
|
5126
5276
|
init_logger();
|
|
5127
|
-
import
|
|
5277
|
+
import chalk33 from "chalk";
|
|
5128
5278
|
async function secretGenerate(key, options) {
|
|
5129
|
-
const
|
|
5279
|
+
const env9 = resolveEnv(options.env);
|
|
5130
5280
|
const pkg = options.package ?? "@spfn/core";
|
|
5131
5281
|
let schema;
|
|
5132
5282
|
try {
|
|
@@ -5140,11 +5290,11 @@ async function secretGenerate(key, options) {
|
|
|
5140
5290
|
logger.warn("No generatable secrets found (none declare a `generate` strategy).");
|
|
5141
5291
|
return;
|
|
5142
5292
|
}
|
|
5143
|
-
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(
|
|
5293
|
+
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(env9)}`);
|
|
5144
5294
|
for (const entry of targets) {
|
|
5145
5295
|
const value = generateSecretValue(entry.generate);
|
|
5146
5296
|
try {
|
|
5147
|
-
await storeSecret(process.cwd(),
|
|
5297
|
+
await storeSecret(process.cwd(), env9, entry.key, value);
|
|
5148
5298
|
} catch (error) {
|
|
5149
5299
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5150
5300
|
process.exit(1);
|
|
@@ -5162,7 +5312,7 @@ function requireGeneratable(schema, key) {
|
|
|
5162
5312
|
process.exit(1);
|
|
5163
5313
|
}
|
|
5164
5314
|
if (!entry.generate) {
|
|
5165
|
-
logger.error(`${
|
|
5315
|
+
logger.error(`${chalk33.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
|
|
5166
5316
|
process.exit(1);
|
|
5167
5317
|
}
|
|
5168
5318
|
return entry;
|
|
@@ -5170,9 +5320,9 @@ function requireGeneratable(schema, key) {
|
|
|
5170
5320
|
|
|
5171
5321
|
// src/commands/secret/rotate.ts
|
|
5172
5322
|
init_logger();
|
|
5173
|
-
import
|
|
5323
|
+
import chalk34 from "chalk";
|
|
5174
5324
|
async function secretRotate(key, options) {
|
|
5175
|
-
const
|
|
5325
|
+
const env9 = resolveEnv(options.env);
|
|
5176
5326
|
const pkg = options.package ?? "@spfn/core";
|
|
5177
5327
|
let schema;
|
|
5178
5328
|
try {
|
|
@@ -5185,10 +5335,10 @@ async function secretRotate(key, options) {
|
|
|
5185
5335
|
const generatable = targets.filter((entry) => entry.generate);
|
|
5186
5336
|
const external = targets.filter((entry) => !entry.generate);
|
|
5187
5337
|
if (generatable.length > 0) {
|
|
5188
|
-
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(
|
|
5338
|
+
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(env9)}`);
|
|
5189
5339
|
for (const entry of generatable) {
|
|
5190
5340
|
try {
|
|
5191
|
-
await storeSecret(process.cwd(),
|
|
5341
|
+
await storeSecret(process.cwd(), env9, entry.key, generateSecretValue(entry.generate));
|
|
5192
5342
|
} catch (error) {
|
|
5193
5343
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5194
5344
|
process.exit(1);
|
|
@@ -5196,7 +5346,7 @@ async function secretRotate(key, options) {
|
|
|
5196
5346
|
}
|
|
5197
5347
|
}
|
|
5198
5348
|
for (const entry of external) {
|
|
5199
|
-
logger.warn(`${
|
|
5349
|
+
logger.warn(`${chalk34.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${env9}\`.`);
|
|
5200
5350
|
}
|
|
5201
5351
|
if (generatable.length > 0) {
|
|
5202
5352
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -5218,12 +5368,12 @@ function requireEntry(schema, key) {
|
|
|
5218
5368
|
// src/commands/secret/keygen.ts
|
|
5219
5369
|
init_logger();
|
|
5220
5370
|
import { execa as execa11 } from "execa";
|
|
5221
|
-
import { existsSync as
|
|
5371
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync4 } from "fs";
|
|
5222
5372
|
import { homedir } from "os";
|
|
5223
|
-
import { dirname as dirname4, join as
|
|
5224
|
-
import
|
|
5373
|
+
import { dirname as dirname4, join as join26 } from "path";
|
|
5374
|
+
import chalk35 from "chalk";
|
|
5225
5375
|
function ageKeyFile() {
|
|
5226
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
5376
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join26(homedir(), ".config", "sops", "age", "keys.txt");
|
|
5227
5377
|
}
|
|
5228
5378
|
async function ensureAgeInstalled() {
|
|
5229
5379
|
try {
|
|
@@ -5243,7 +5393,7 @@ async function secretKeygen() {
|
|
|
5243
5393
|
process.exit(1);
|
|
5244
5394
|
}
|
|
5245
5395
|
const keyFile = ageKeyFile();
|
|
5246
|
-
if (
|
|
5396
|
+
if (existsSync29(keyFile)) {
|
|
5247
5397
|
const { stdout } = await execa11("age-keygen", ["-y", keyFile]);
|
|
5248
5398
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
5249
5399
|
printPublicKeys(publicKeys(stdout));
|
|
@@ -5259,27 +5409,27 @@ function printPublicKeys(keys) {
|
|
|
5259
5409
|
logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
|
|
5260
5410
|
return;
|
|
5261
5411
|
}
|
|
5262
|
-
console.log(
|
|
5412
|
+
console.log(chalk35.bold("\nPublic key(s):"));
|
|
5263
5413
|
for (const key of keys) {
|
|
5264
|
-
console.log(` ${
|
|
5414
|
+
console.log(` ${chalk35.cyan(key)}`);
|
|
5265
5415
|
}
|
|
5266
|
-
console.log(
|
|
5267
|
-
console.log(
|
|
5416
|
+
console.log(chalk35.dim("\nRegister it as a recipient:"));
|
|
5417
|
+
console.log(chalk35.dim(` spfn secret recipients add ${keys[0]}
|
|
5268
5418
|
`));
|
|
5269
5419
|
}
|
|
5270
5420
|
|
|
5271
5421
|
// src/commands/secret/recipients.ts
|
|
5272
5422
|
init_logger();
|
|
5273
|
-
import { existsSync as
|
|
5274
|
-
import { join as
|
|
5423
|
+
import { existsSync as existsSync30, readFileSync as readFileSync13, writeFileSync as writeFileSync18 } from "fs";
|
|
5424
|
+
import { join as join27 } from "path";
|
|
5275
5425
|
import { parse as parse3, stringify } from "yaml";
|
|
5276
|
-
import
|
|
5426
|
+
import chalk36 from "chalk";
|
|
5277
5427
|
var SOPS_CONFIG = ".sops.yaml";
|
|
5278
5428
|
var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
5279
5429
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
5280
5430
|
async function secretRecipients(action, key, _options) {
|
|
5281
5431
|
const cwd = process.cwd();
|
|
5282
|
-
const configPath =
|
|
5432
|
+
const configPath = join27(cwd, SOPS_CONFIG);
|
|
5283
5433
|
if (action === "list") {
|
|
5284
5434
|
listRecipients(configPath);
|
|
5285
5435
|
return;
|
|
@@ -5311,25 +5461,25 @@ async function secretRecipients(action, key, _options) {
|
|
|
5311
5461
|
await reencrypt(cwd);
|
|
5312
5462
|
}
|
|
5313
5463
|
function listRecipients(configPath) {
|
|
5314
|
-
if (!
|
|
5464
|
+
if (!existsSync30(configPath)) {
|
|
5315
5465
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
5316
5466
|
return;
|
|
5317
5467
|
}
|
|
5318
5468
|
const config = loadConfig(configPath);
|
|
5319
5469
|
const rules = config.creation_rules ?? [];
|
|
5320
|
-
console.log(
|
|
5470
|
+
console.log(chalk36.blue.bold(`
|
|
5321
5471
|
\u{1F465} Recipients (${SOPS_CONFIG})
|
|
5322
5472
|
`));
|
|
5323
5473
|
for (const rule of rules) {
|
|
5324
|
-
console.log(
|
|
5474
|
+
console.log(chalk36.dim(` ${rule.path_regex ?? "(any path)"}`));
|
|
5325
5475
|
for (const recipient of parseRecipients(rule.age)) {
|
|
5326
|
-
console.log(` ${
|
|
5476
|
+
console.log(` ${chalk36.cyan(recipient)}`);
|
|
5327
5477
|
}
|
|
5328
5478
|
}
|
|
5329
5479
|
console.log();
|
|
5330
5480
|
}
|
|
5331
5481
|
function loadConfig(configPath) {
|
|
5332
|
-
if (!
|
|
5482
|
+
if (!existsSync30(configPath)) {
|
|
5333
5483
|
return { creation_rules: [] };
|
|
5334
5484
|
}
|
|
5335
5485
|
return parse3(readFileSync13(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
@@ -5364,8 +5514,8 @@ async function reencrypt(cwd) {
|
|
|
5364
5514
|
|
|
5365
5515
|
// src/commands/secret/check.ts
|
|
5366
5516
|
init_logger();
|
|
5367
|
-
import { join as
|
|
5368
|
-
import
|
|
5517
|
+
import { join as join28 } from "path";
|
|
5518
|
+
import chalk37 from "chalk";
|
|
5369
5519
|
init_env_file();
|
|
5370
5520
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
5371
5521
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
@@ -5383,35 +5533,35 @@ async function secretCheck(options) {
|
|
|
5383
5533
|
const issues = [];
|
|
5384
5534
|
const warnings = [];
|
|
5385
5535
|
for (const file of COMMITTED_FILES) {
|
|
5386
|
-
const parsed = parseEnvFile(
|
|
5536
|
+
const parsed = parseEnvFile(join28(cwd, file));
|
|
5387
5537
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5388
5538
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
5389
|
-
issues.push(`${
|
|
5539
|
+
issues.push(`${chalk37.cyan(key)} has a real value in committed ${chalk37.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
5390
5540
|
}
|
|
5391
5541
|
}
|
|
5392
5542
|
}
|
|
5393
|
-
const serverEnv = parseEnvFile(
|
|
5543
|
+
const serverEnv = parseEnvFile(join28(cwd, ".env.server"));
|
|
5394
5544
|
for (const key of secretKeys) {
|
|
5395
5545
|
const value = serverEnv[key];
|
|
5396
5546
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
5397
|
-
warnings.push(`${
|
|
5547
|
+
warnings.push(`${chalk37.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
5398
5548
|
}
|
|
5399
5549
|
}
|
|
5400
5550
|
report(issues, warnings, cwd);
|
|
5401
5551
|
}
|
|
5402
5552
|
function report(issues, warnings, cwd) {
|
|
5403
|
-
console.log(
|
|
5553
|
+
console.log(chalk37.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
5404
5554
|
for (const issue of issues) {
|
|
5405
|
-
console.log(` ${
|
|
5555
|
+
console.log(` ${chalk37.red("\u2717")} ${issue}`);
|
|
5406
5556
|
}
|
|
5407
5557
|
for (const warning of warnings) {
|
|
5408
|
-
console.log(` ${
|
|
5558
|
+
console.log(` ${chalk37.yellow("\u26A0")} ${warning}`);
|
|
5409
5559
|
}
|
|
5410
5560
|
if (!hasSopsConfig(cwd)) {
|
|
5411
|
-
console.log(` ${
|
|
5561
|
+
console.log(` ${chalk37.dim("\u2139 no .sops.yaml \u2014 prod secrets need a backend (spfn secret keygen, or add a KMS rule)")}`);
|
|
5412
5562
|
}
|
|
5413
5563
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5414
|
-
console.log(
|
|
5564
|
+
console.log(chalk37.green(" \u2713 No plaintext secret leaks found."));
|
|
5415
5565
|
}
|
|
5416
5566
|
console.log();
|
|
5417
5567
|
if (issues.length > 0) {
|