spfn 0.2.0-beta.4 → 0.2.0-beta.41
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/bin/spfn.js +46 -4
- package/dist/index.js +1160 -463
- package/dist/templates/lib/api-client.ts +65 -34
- package/dist/templates/server/router.ts +0 -14
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -521,35 +521,47 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
521
521
|
import { join as join6 } from "path";
|
|
522
522
|
import fse3 from "fs-extra";
|
|
523
523
|
async function setupApiProxy(cwd, includeAuth) {
|
|
524
|
-
const
|
|
524
|
+
const srcAppDir = join6(cwd, "src", "app");
|
|
525
|
+
const rootAppDir = join6(cwd, "app");
|
|
526
|
+
let appDir;
|
|
527
|
+
if (existsSync6(srcAppDir)) {
|
|
528
|
+
appDir = srcAppDir;
|
|
529
|
+
} else if (existsSync6(rootAppDir)) {
|
|
530
|
+
appDir = rootAppDir;
|
|
531
|
+
} else {
|
|
532
|
+
logger.error("Next.js app directory not found. Expected src/app or app directory.");
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
525
535
|
const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
|
|
526
536
|
const rpcRoutePath = join6(rpcDir, "route.ts");
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
537
|
+
if (existsSync6(rpcRoutePath)) {
|
|
538
|
+
logger.error(`RPC proxy route already exists: ${rpcRoutePath.replace(cwd + "/", "")}`);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
ensureDirSync3(rpcDir);
|
|
542
|
+
const authImport = includeAuth ? `import '@spfn/auth/nextjs/api';
|
|
530
543
|
` : "";
|
|
531
|
-
|
|
544
|
+
const routeContent = `/**
|
|
532
545
|
* SPFN RPC Proxy
|
|
533
546
|
*
|
|
534
|
-
* Resolves routeName to actual HTTP method and path from
|
|
547
|
+
* Resolves routeName to actual HTTP method and path from routeMap,
|
|
535
548
|
* then forwards requests to SPFN API server with automatic:
|
|
536
549
|
* - Cookie forwarding
|
|
537
550
|
* - Interceptor execution
|
|
538
551
|
* - Header manipulation
|
|
539
552
|
*
|
|
540
|
-
* Note:
|
|
541
|
-
*
|
|
553
|
+
* Note: Uses generated route-map to avoid loading server code in Next.js process.
|
|
554
|
+
* Run \`spfn codegen run\` if route-map.ts is missing.
|
|
542
555
|
*/
|
|
543
556
|
|
|
544
|
-
${authImport}import {
|
|
557
|
+
${authImport}import { routeMap } from '@/generated/route-map';
|
|
545
558
|
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
546
559
|
|
|
547
|
-
export const { GET, POST } = createRpcProxy({
|
|
560
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
548
561
|
`;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
562
|
+
writeFileSync2(rpcRoutePath, routeContent);
|
|
563
|
+
const relativePath = rpcRoutePath.replace(cwd + "/", "");
|
|
564
|
+
logger.success(`Created ${relativePath} (RPC proxy)`);
|
|
553
565
|
}
|
|
554
566
|
var ensureDirSync3, writeFileSync2;
|
|
555
567
|
var init_api_proxy = __esm({
|
|
@@ -741,6 +753,23 @@ var init_deployment_config = __esm({
|
|
|
741
753
|
}
|
|
742
754
|
});
|
|
743
755
|
|
|
756
|
+
// src/utils/version.ts
|
|
757
|
+
function getCliVersion() {
|
|
758
|
+
return "0.2.0-beta.41";
|
|
759
|
+
}
|
|
760
|
+
function getTagFromVersion(version) {
|
|
761
|
+
const match = version.match(/-([a-z]+)\./i);
|
|
762
|
+
return match ? match[1] : "latest";
|
|
763
|
+
}
|
|
764
|
+
function getSpfnTag() {
|
|
765
|
+
return getTagFromVersion(getCliVersion());
|
|
766
|
+
}
|
|
767
|
+
var init_version = __esm({
|
|
768
|
+
"src/utils/version.ts"() {
|
|
769
|
+
"use strict";
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
744
773
|
// src/commands/init/steps/package.ts
|
|
745
774
|
import ora3 from "ora";
|
|
746
775
|
import { execa as execa2 } from "execa";
|
|
@@ -750,13 +779,15 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
750
779
|
packageJson.dependencies = packageJson.dependencies || {};
|
|
751
780
|
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
752
781
|
packageJson.scripts = packageJson.scripts || {};
|
|
753
|
-
|
|
782
|
+
const spfnTag = getSpfnTag();
|
|
783
|
+
packageJson.dependencies["@spfn/core"] = spfnTag;
|
|
754
784
|
packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
|
|
785
|
+
packageJson.dependencies["drizzle-orm"] = "^0.45.0";
|
|
755
786
|
packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
|
|
756
|
-
packageJson.dependencies["spfn"] =
|
|
787
|
+
packageJson.dependencies["spfn"] = spfnTag;
|
|
757
788
|
packageJson.dependencies["concurrently"] = "^9.2.1";
|
|
758
789
|
if (includeAuth) {
|
|
759
|
-
packageJson.dependencies["@spfn/auth"] =
|
|
790
|
+
packageJson.dependencies["@spfn/auth"] = spfnTag;
|
|
760
791
|
}
|
|
761
792
|
packageJson.devDependencies["@types/node"] = "^20.11.0";
|
|
762
793
|
packageJson.devDependencies["tsx"] = "^4.20.6";
|
|
@@ -793,6 +824,7 @@ var init_package = __esm({
|
|
|
793
824
|
"src/commands/init/steps/package.ts"() {
|
|
794
825
|
"use strict";
|
|
795
826
|
init_logger();
|
|
827
|
+
init_version();
|
|
796
828
|
({ writeFileSync: writeFileSync4 } = fse6);
|
|
797
829
|
}
|
|
798
830
|
});
|
|
@@ -802,26 +834,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
|
|
|
802
834
|
import { join as join9 } from "path";
|
|
803
835
|
import fse7 from "fs-extra";
|
|
804
836
|
async function setupConfigFiles(cwd) {
|
|
805
|
-
|
|
806
|
-
if (!existsSync9(envExamplePath)) {
|
|
807
|
-
const envExampleContent = `# Environment
|
|
808
|
-
NODE_ENV=local
|
|
809
|
-
|
|
810
|
-
# Logging
|
|
811
|
-
SPFN_LOG_LEVEL=info
|
|
812
|
-
|
|
813
|
-
# Database (matches docker-compose.yml)
|
|
814
|
-
DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
|
|
815
|
-
|
|
816
|
-
# Cache - Redis/Valkey (optional)
|
|
817
|
-
CACHE_URL=redis://localhost:6379
|
|
818
|
-
|
|
819
|
-
# SPFN API Server URL (for API Route Proxy and SSR)
|
|
820
|
-
SPFN_API_URL=http://localhost:8790
|
|
821
|
-
`;
|
|
822
|
-
writeFileSync5(envExamplePath, envExampleContent);
|
|
823
|
-
logger.success("Created .env.local.example");
|
|
824
|
-
}
|
|
837
|
+
generateEnvExamples(cwd);
|
|
825
838
|
const spfnrcPath = join9(cwd, ".spfnrc.ts");
|
|
826
839
|
if (!existsSync9(spfnrcPath)) {
|
|
827
840
|
const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
@@ -830,64 +843,135 @@ SPFN_API_URL=http://localhost:8790
|
|
|
830
843
|
* SPFN Codegen Configuration
|
|
831
844
|
*
|
|
832
845
|
* Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
|
|
833
|
-
*
|
|
834
|
-
* Example: Custom generator
|
|
835
|
-
* @example
|
|
836
|
-
* const myGenerator = defineGenerator({
|
|
837
|
-
* name: 'my-package:generator',
|
|
838
|
-
* enabled: true,
|
|
839
|
-
* // ... generator-specific options
|
|
840
|
-
* });
|
|
841
|
-
*
|
|
842
|
-
* Example: File-based generator
|
|
843
|
-
* @example
|
|
844
|
-
* const customGen = defineGenerator({
|
|
845
|
-
* path: './src/generators/my-generator.ts',
|
|
846
|
-
* });
|
|
847
846
|
*/
|
|
848
847
|
|
|
849
848
|
export default defineConfig({
|
|
850
849
|
generators: [
|
|
851
|
-
//
|
|
852
|
-
//
|
|
850
|
+
// Route map generator - generates routeName \u2192 {method, path} mappings
|
|
851
|
+
// Used by RPC proxy to resolve routes without importing server code
|
|
852
|
+
defineGenerator({
|
|
853
|
+
name: '@spfn/core:route-map',
|
|
854
|
+
routerPath: './src/server/router.ts',
|
|
855
|
+
outputPath: './src/generated/route-map.ts',
|
|
856
|
+
}),
|
|
853
857
|
]
|
|
854
858
|
});
|
|
855
859
|
`;
|
|
856
860
|
writeFileSync5(spfnrcPath, spfnrcContent);
|
|
857
861
|
logger.success("Created .spfnrc.ts (codegen configuration)");
|
|
858
862
|
}
|
|
863
|
+
updateGitignore(cwd);
|
|
864
|
+
updateTsconfig(cwd);
|
|
865
|
+
}
|
|
866
|
+
function generateEnvExamples(cwd) {
|
|
867
|
+
writeEnvExample(cwd, ".env.example", `# Shared defaults (committed)
|
|
868
|
+
# These values are shared across all environments.
|
|
869
|
+
|
|
870
|
+
# Environment
|
|
871
|
+
NODE_ENV=local
|
|
872
|
+
|
|
873
|
+
# Logging
|
|
874
|
+
SPFN_LOG_LEVEL=info
|
|
875
|
+
|
|
876
|
+
# Server
|
|
877
|
+
PORT=4000
|
|
878
|
+
|
|
879
|
+
# SPFN API Server URL (for API Route Proxy and SSR)
|
|
880
|
+
SPFN_API_URL=http://localhost:8790
|
|
881
|
+
NEXT_PUBLIC_SPFN_API_URL=http://localhost:8790
|
|
882
|
+
`);
|
|
883
|
+
writeEnvExample(cwd, ".env.local.example", `# Local overrides (gitignored)
|
|
884
|
+
# Developer-specific values that should NOT be committed.
|
|
885
|
+
|
|
886
|
+
# Database (matches docker-compose.yml)
|
|
887
|
+
DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
|
|
888
|
+
|
|
889
|
+
# Cache - Redis/Valkey (optional)
|
|
890
|
+
CACHE_URL=redis://localhost:6379
|
|
891
|
+
|
|
892
|
+
# SPFN App URL (optional, for CORS and redirects)
|
|
893
|
+
# SPFN_APP_URL=http://localhost:3790
|
|
894
|
+
`);
|
|
895
|
+
writeEnvExample(cwd, ".env.server.example", `# Server-only defaults (committed)
|
|
896
|
+
# These values are only loaded by the SPFN server, not by Next.js.
|
|
897
|
+
|
|
898
|
+
# Database pool
|
|
899
|
+
DB_POOL_MAX=10
|
|
900
|
+
DB_POOL_IDLE_TIMEOUT=30
|
|
901
|
+
|
|
902
|
+
# Server timeouts
|
|
903
|
+
SERVER_TIMEOUT=120000
|
|
904
|
+
SHUTDOWN_TIMEOUT=30000
|
|
905
|
+
`);
|
|
906
|
+
writeEnvExample(cwd, ".env.server.local.example", `# Server secrets (gitignored)
|
|
907
|
+
# Server-only sensitive values. Never commit this file.
|
|
908
|
+
|
|
909
|
+
# Database write/read URLs (master-replica pattern, optional)
|
|
910
|
+
# DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
|
|
911
|
+
# DATABASE_READ_URL=postgresql://user:password@replica:5432/dbname
|
|
912
|
+
|
|
913
|
+
# Cache password (optional)
|
|
914
|
+
# CACHE_PASSWORD=your-redis-password
|
|
915
|
+
`);
|
|
916
|
+
}
|
|
917
|
+
function writeEnvExample(cwd, filename, content) {
|
|
918
|
+
const filePath = join9(cwd, filename);
|
|
919
|
+
if (existsSync9(filePath)) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
writeFileSync5(filePath, content);
|
|
923
|
+
logger.success(`Created ${filename}`);
|
|
924
|
+
}
|
|
925
|
+
function updateGitignore(cwd) {
|
|
859
926
|
const gitignorePath = join9(cwd, ".gitignore");
|
|
860
|
-
if (existsSync9(gitignorePath)) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
927
|
+
if (!existsSync9(gitignorePath)) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
932
|
+
let updated = content;
|
|
933
|
+
let changed = false;
|
|
934
|
+
if (!content.includes(".spfn")) {
|
|
935
|
+
updated = updated.replace(
|
|
936
|
+
/# production\n\/build/,
|
|
937
|
+
"# production\n/build\n\n# spfn\n/.spfn/"
|
|
938
|
+
);
|
|
939
|
+
changed = true;
|
|
940
|
+
}
|
|
941
|
+
if (!content.includes(".env.local") && !content.includes(".env.*.local")) {
|
|
942
|
+
updated += `
|
|
943
|
+
# environment secrets (local overrides)
|
|
944
|
+
.env.local
|
|
945
|
+
.env.*.local
|
|
946
|
+
`;
|
|
947
|
+
changed = true;
|
|
948
|
+
}
|
|
949
|
+
if (changed) {
|
|
950
|
+
writeFileSync5(gitignorePath, updated);
|
|
951
|
+
logger.success("Updated .gitignore with .spfn directory and env patterns");
|
|
873
952
|
}
|
|
953
|
+
} catch (error) {
|
|
954
|
+
logger.warn("Could not update .gitignore (you can add patterns manually)");
|
|
874
955
|
}
|
|
956
|
+
}
|
|
957
|
+
function updateTsconfig(cwd) {
|
|
875
958
|
const tsconfigPath = join9(cwd, "tsconfig.json");
|
|
876
|
-
if (existsSync9(tsconfigPath)) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
|
|
959
|
+
if (!existsSync9(tsconfigPath)) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
|
|
964
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
965
|
+
if (!tsconfig.exclude) {
|
|
966
|
+
tsconfig.exclude = [];
|
|
967
|
+
}
|
|
968
|
+
if (!tsconfig.exclude.includes("src/server")) {
|
|
969
|
+
tsconfig.exclude.push("src/server");
|
|
970
|
+
writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
971
|
+
logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
|
|
890
972
|
}
|
|
973
|
+
} catch (error) {
|
|
974
|
+
logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
|
|
891
975
|
}
|
|
892
976
|
}
|
|
893
977
|
var writeFileSync5;
|
|
@@ -958,10 +1042,10 @@ __export(function_migrations_exports, {
|
|
|
958
1042
|
discoverFunctionMigrations: () => discoverFunctionMigrations,
|
|
959
1043
|
executeFunctionMigrations: () => executeFunctionMigrations
|
|
960
1044
|
});
|
|
961
|
-
import
|
|
1045
|
+
import chalk12 from "chalk";
|
|
962
1046
|
import { join as join15 } from "path";
|
|
963
1047
|
import { env as env2 } from "@spfn/core/config";
|
|
964
|
-
import {
|
|
1048
|
+
import { loadEnv as loadEnv2 } from "@spfn/core/server";
|
|
965
1049
|
import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
966
1050
|
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
967
1051
|
const nodeModulesPath = join15(cwd, "node_modules");
|
|
@@ -989,7 +1073,7 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
|
989
1073
|
const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
|
|
990
1074
|
if (!existsSync16(migrationsDir)) {
|
|
991
1075
|
console.warn(
|
|
992
|
-
|
|
1076
|
+
chalk12.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
|
|
993
1077
|
);
|
|
994
1078
|
continue;
|
|
995
1079
|
}
|
|
@@ -999,28 +1083,99 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
|
999
1083
|
packagePath
|
|
1000
1084
|
});
|
|
1001
1085
|
} catch (error) {
|
|
1002
|
-
console.warn(
|
|
1086
|
+
console.warn(chalk12.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
|
|
1003
1087
|
}
|
|
1004
1088
|
}
|
|
1005
1089
|
return functions;
|
|
1006
1090
|
}
|
|
1091
|
+
async function migrateLegacyTable(db, functionMigrations) {
|
|
1092
|
+
const legacyCheck = await db.execute(
|
|
1093
|
+
`SELECT EXISTS (
|
|
1094
|
+
SELECT 1 FROM information_schema.tables
|
|
1095
|
+
WHERE table_schema = 'drizzle' AND table_name = '__spfn_fn_migrations'
|
|
1096
|
+
) AS "exists"`
|
|
1097
|
+
);
|
|
1098
|
+
if (!legacyCheck[0]?.exists) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
console.log(chalk12.dim("\n Migrating legacy shared migration table to per-package tables..."));
|
|
1102
|
+
const legacyRows = await db.execute(
|
|
1103
|
+
`SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
|
|
1104
|
+
);
|
|
1105
|
+
if (legacyRows.length === 0) {
|
|
1106
|
+
await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
for (const func of functionMigrations) {
|
|
1110
|
+
const journalPath = join15(func.migrationsDir, "meta", "_journal.json");
|
|
1111
|
+
if (!existsSync16(journalPath)) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const journal = JSON.parse(readFileSync5(journalPath, "utf-8"));
|
|
1115
|
+
const entries = journal.entries || [];
|
|
1116
|
+
const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
|
|
1117
|
+
await db.execute(
|
|
1118
|
+
`CREATE SCHEMA IF NOT EXISTS drizzle`
|
|
1119
|
+
);
|
|
1120
|
+
await db.execute(
|
|
1121
|
+
`CREATE TABLE IF NOT EXISTS drizzle."${tableName}" (
|
|
1122
|
+
id serial PRIMARY KEY,
|
|
1123
|
+
hash text NOT NULL,
|
|
1124
|
+
created_at bigint
|
|
1125
|
+
)`
|
|
1126
|
+
);
|
|
1127
|
+
const existing = await db.execute(
|
|
1128
|
+
`SELECT COUNT(*) AS "count" FROM drizzle."${tableName}"`
|
|
1129
|
+
);
|
|
1130
|
+
if (Number(existing[0]?.count) > 0) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
const { createHash } = await import("crypto");
|
|
1134
|
+
let copied = 0;
|
|
1135
|
+
for (const entry of entries) {
|
|
1136
|
+
const sqlPath = join15(func.migrationsDir, `${entry.tag}.sql`);
|
|
1137
|
+
if (!existsSync16(sqlPath)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const sqlContent = readFileSync5(sqlPath, "utf-8");
|
|
1141
|
+
const hash = createHash("sha256").update(sqlContent).digest("hex");
|
|
1142
|
+
const found = legacyRows.find((r) => r.hash === hash);
|
|
1143
|
+
if (found) {
|
|
1144
|
+
await db.execute(
|
|
1145
|
+
`INSERT INTO drizzle."${tableName}" (hash, created_at) VALUES ('${hash}', ${entry.when})`
|
|
1146
|
+
);
|
|
1147
|
+
copied++;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (copied > 0) {
|
|
1151
|
+
console.log(chalk12.dim(` \u2713 ${func.packageName}: copied ${copied} migration record(s)`));
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
|
|
1155
|
+
console.log(chalk12.dim(" \u2713 Legacy migration table removed\n"));
|
|
1156
|
+
}
|
|
1007
1157
|
async function executeFunctionMigrations(functionMigrations) {
|
|
1008
1158
|
let executedCount = 0;
|
|
1009
1159
|
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
1010
1160
|
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
1011
1161
|
const postgres = await import("postgres");
|
|
1012
|
-
|
|
1162
|
+
loadEnv2();
|
|
1013
1163
|
if (!env2.DATABASE_URL) {
|
|
1014
1164
|
throw new Error("DATABASE_URL not found in environment");
|
|
1015
1165
|
}
|
|
1016
1166
|
const connection = postgres.default(env2.DATABASE_URL, { max: 1 });
|
|
1017
1167
|
const db = drizzle(connection);
|
|
1018
1168
|
try {
|
|
1169
|
+
await migrateLegacyTable(db, functionMigrations);
|
|
1019
1170
|
for (const func of functionMigrations) {
|
|
1020
|
-
console.log(
|
|
1171
|
+
console.log(chalk12.blue(`
|
|
1021
1172
|
\u{1F4E6} Running ${func.packageName} migrations...`));
|
|
1022
|
-
|
|
1023
|
-
|
|
1173
|
+
const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
|
|
1174
|
+
await migrate(db, {
|
|
1175
|
+
migrationsFolder: func.migrationsDir,
|
|
1176
|
+
migrationsTable: tableName
|
|
1177
|
+
});
|
|
1178
|
+
console.log(chalk12.green(` \u2713 ${func.packageName} migrations applied`));
|
|
1024
1179
|
executedCount++;
|
|
1025
1180
|
}
|
|
1026
1181
|
} finally {
|
|
@@ -1179,10 +1334,30 @@ init_init();
|
|
|
1179
1334
|
init_logger();
|
|
1180
1335
|
init_package_manager();
|
|
1181
1336
|
import { Command as Command4 } from "commander";
|
|
1182
|
-
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
|
|
1337
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync, unlinkSync, rmSync, watch } from "fs";
|
|
1183
1338
|
import { join as join11 } from "path";
|
|
1184
1339
|
import { execa as execa4 } from "execa";
|
|
1185
1340
|
import chokidar from "chokidar";
|
|
1341
|
+
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
1342
|
+
return new Promise((resolve2, reject) => {
|
|
1343
|
+
if (existsSync11(filePath)) {
|
|
1344
|
+
unlinkSync(filePath);
|
|
1345
|
+
}
|
|
1346
|
+
const timer = setTimeout(() => {
|
|
1347
|
+
watcher.close();
|
|
1348
|
+
reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
|
|
1349
|
+
}, timeoutMs);
|
|
1350
|
+
const dir = join11(filePath, "..");
|
|
1351
|
+
const fileName = filePath.split("/").pop();
|
|
1352
|
+
const watcher = watch(dir, (event, name) => {
|
|
1353
|
+
if (name === fileName && existsSync11(filePath)) {
|
|
1354
|
+
watcher.close();
|
|
1355
|
+
clearTimeout(timer);
|
|
1356
|
+
resolve2(readFileSync3(filePath, "utf-8").trim());
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1186
1361
|
var devCommand = new Command4("dev").description("Start SPFN development server (detects and runs Next.js + Hono)").option("-p, --port <port>", "Server port").option("-H, --host <host>", "Server host").option("--routes <path>", "Routes directory path").option("--server-only", "Run only Hono server (skip Next.js)").option("--watch", "Enable hot reload (watch mode)").action(async (options) => {
|
|
1187
1362
|
process.setMaxListeners(20);
|
|
1188
1363
|
if (!process.env.NODE_ENV) {
|
|
@@ -1205,22 +1380,36 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1205
1380
|
const serverEntry = join11(tempDir, "server.mjs");
|
|
1206
1381
|
const watcherEntry = join11(tempDir, "watcher.mjs");
|
|
1207
1382
|
mkdirSync(tempDir, { recursive: true });
|
|
1383
|
+
const serverBuildDir = join11(tempDir, "server");
|
|
1384
|
+
if (existsSync11(serverBuildDir)) {
|
|
1385
|
+
rmSync(serverBuildDir, { recursive: true, force: true });
|
|
1386
|
+
logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
|
|
1387
|
+
}
|
|
1388
|
+
const readySignal = join11(tempDir, "server-ready");
|
|
1389
|
+
if (existsSync11(readySignal)) {
|
|
1390
|
+
unlinkSync(readySignal);
|
|
1391
|
+
}
|
|
1208
1392
|
const configParts = [];
|
|
1209
1393
|
if (options.port) configParts.push(`port: ${options.port}`);
|
|
1210
1394
|
if (options.host) configParts.push(`host: '${options.host}'`);
|
|
1211
1395
|
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
1212
1396
|
configParts.push("debug: true");
|
|
1397
|
+
const readyFile = join11(tempDir, "server-ready");
|
|
1213
1398
|
writeFileSync6(serverEntry, `
|
|
1399
|
+
import { writeFileSync } from 'fs';
|
|
1400
|
+
|
|
1214
1401
|
// Load environment variables FIRST (before any imports that depend on them)
|
|
1215
|
-
// Use centralized environment loader for standard dotenv priority
|
|
1216
1402
|
await import('@spfn/core/config');
|
|
1217
1403
|
|
|
1218
1404
|
// Import and start server
|
|
1219
1405
|
const { startServer } = await import('@spfn/core/server');
|
|
1220
1406
|
|
|
1221
|
-
await startServer({
|
|
1407
|
+
const instance = await startServer({
|
|
1222
1408
|
${configParts.join(",\n ")}
|
|
1223
1409
|
});
|
|
1410
|
+
|
|
1411
|
+
// Signal ready with actual port
|
|
1412
|
+
writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
|
|
1224
1413
|
`);
|
|
1225
1414
|
writeFileSync6(watcherEntry, `
|
|
1226
1415
|
// Load environment variables
|
|
@@ -1277,7 +1466,9 @@ catch (error)
|
|
|
1277
1466
|
const pm = detectPackageManager(cwd);
|
|
1278
1467
|
if (options.serverOnly || !hasNext) {
|
|
1279
1468
|
const watchMode2 = options.watch === true;
|
|
1280
|
-
|
|
1469
|
+
const host = options.host ?? process.env.HOST ?? "localhost";
|
|
1470
|
+
const port = options.port ?? process.env.PORT ?? "4000";
|
|
1471
|
+
logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
|
|
1281
1472
|
`);
|
|
1282
1473
|
let serverProcess2 = null;
|
|
1283
1474
|
let watcherProcess2 = null;
|
|
@@ -1471,7 +1662,13 @@ catch (error)
|
|
|
1471
1662
|
process.on("SIGTERM", cleanup);
|
|
1472
1663
|
startWatcher();
|
|
1473
1664
|
startServer();
|
|
1474
|
-
|
|
1665
|
+
try {
|
|
1666
|
+
const port = await waitForReadyFile(readyFile);
|
|
1667
|
+
logger.info(`[SPFN] Server ready on port ${port}, starting Next.js...
|
|
1668
|
+
`);
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
|
|
1671
|
+
}
|
|
1475
1672
|
startNext();
|
|
1476
1673
|
await new Promise((resolve2) => {
|
|
1477
1674
|
const keepAlive = setInterval(() => {
|
|
@@ -1522,7 +1719,7 @@ async function buildProject(options) {
|
|
|
1522
1719
|
const orchestrator = new CodegenOrchestrator({
|
|
1523
1720
|
generators,
|
|
1524
1721
|
cwd,
|
|
1525
|
-
debug:
|
|
1722
|
+
debug: true
|
|
1526
1723
|
});
|
|
1527
1724
|
await orchestrator.generateAll();
|
|
1528
1725
|
spinner.succeed("API client generated");
|
|
@@ -1832,7 +2029,7 @@ async function runGenerators() {
|
|
|
1832
2029
|
const orchestrator = new CodegenOrchestrator({
|
|
1833
2030
|
generators,
|
|
1834
2031
|
cwd,
|
|
1835
|
-
debug:
|
|
2032
|
+
debug: true
|
|
1836
2033
|
});
|
|
1837
2034
|
await orchestrator.generateAll();
|
|
1838
2035
|
console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
|
|
@@ -1991,14 +2188,15 @@ import { Command as Command9 } from "commander";
|
|
|
1991
2188
|
import chalk10 from "chalk";
|
|
1992
2189
|
|
|
1993
2190
|
// src/commands/db/utils/drizzle.ts
|
|
1994
|
-
import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
|
|
2191
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
|
|
1995
2192
|
import { spawn } from "child_process";
|
|
2193
|
+
import { pathToFileURL } from "url";
|
|
1996
2194
|
import chalk9 from "chalk";
|
|
1997
2195
|
import ora6 from "ora";
|
|
1998
2196
|
import { env } from "@spfn/core/config";
|
|
1999
|
-
import {
|
|
2197
|
+
import { loadEnv } from "@spfn/core/server";
|
|
2000
2198
|
function validateDatabasePrerequisites() {
|
|
2001
|
-
|
|
2199
|
+
loadEnv();
|
|
2002
2200
|
if (!env.DATABASE_URL) {
|
|
2003
2201
|
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2004
2202
|
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
@@ -2010,7 +2208,7 @@ async function runDrizzleCommand(command) {
|
|
|
2010
2208
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2011
2209
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2012
2210
|
if (!hasUserConfig) {
|
|
2013
|
-
|
|
2211
|
+
loadEnv();
|
|
2014
2212
|
if (!env.DATABASE_URL) {
|
|
2015
2213
|
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2016
2214
|
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
@@ -2020,7 +2218,10 @@ async function runDrizzleCommand(command) {
|
|
|
2020
2218
|
const configContent = generateDrizzleConfigFile({
|
|
2021
2219
|
cwd: process.cwd(),
|
|
2022
2220
|
// Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
|
|
2023
|
-
disablePackageDiscovery: true
|
|
2221
|
+
disablePackageDiscovery: true,
|
|
2222
|
+
// Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
|
|
2223
|
+
expandGlobs: true,
|
|
2224
|
+
autoDetectSchemas: true
|
|
2024
2225
|
});
|
|
2025
2226
|
writeFileSync9(tempConfigPath, configContent);
|
|
2026
2227
|
console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
|
|
@@ -2031,11 +2232,12 @@ async function runDrizzleCommand(command) {
|
|
|
2031
2232
|
const drizzleProcess = spawn("drizzle-kit", args, {
|
|
2032
2233
|
stdio: "inherit",
|
|
2033
2234
|
// Allow interactive input
|
|
2034
|
-
shell: true
|
|
2235
|
+
shell: true,
|
|
2236
|
+
env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
|
|
2035
2237
|
});
|
|
2036
2238
|
const cleanup = () => {
|
|
2037
2239
|
if (!hasUserConfig && existsSync15(tempConfigPath)) {
|
|
2038
|
-
|
|
2240
|
+
unlinkSync2(tempConfigPath);
|
|
2039
2241
|
}
|
|
2040
2242
|
};
|
|
2041
2243
|
drizzleProcess.on("close", (code) => {
|
|
@@ -2064,6 +2266,43 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
|
|
|
2064
2266
|
process.exit(1);
|
|
2065
2267
|
}
|
|
2066
2268
|
}
|
|
2269
|
+
async function ensureTsxLoader() {
|
|
2270
|
+
}
|
|
2271
|
+
async function loadSchemaImports(schemaFiles) {
|
|
2272
|
+
const hasTsFiles = schemaFiles.some((f) => f.endsWith(".ts"));
|
|
2273
|
+
if (hasTsFiles) {
|
|
2274
|
+
await ensureTsxLoader();
|
|
2275
|
+
}
|
|
2276
|
+
const imports = {};
|
|
2277
|
+
for (const file of schemaFiles) {
|
|
2278
|
+
const moduleUrl = pathToFileURL(file).href;
|
|
2279
|
+
const mod = await import(moduleUrl);
|
|
2280
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
2281
|
+
if (key !== "default") {
|
|
2282
|
+
imports[key] = value;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return imports;
|
|
2287
|
+
}
|
|
2288
|
+
async function createPushConnection() {
|
|
2289
|
+
loadEnv();
|
|
2290
|
+
if (!env.DATABASE_URL) {
|
|
2291
|
+
throw new Error("DATABASE_URL is required");
|
|
2292
|
+
}
|
|
2293
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
2294
|
+
const pg = await import("pg");
|
|
2295
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
2296
|
+
const pool = new pg.default.Pool({
|
|
2297
|
+
connectionString: env.DATABASE_URL,
|
|
2298
|
+
max: 1
|
|
2299
|
+
});
|
|
2300
|
+
const db = drizzle(pool);
|
|
2301
|
+
return {
|
|
2302
|
+
db,
|
|
2303
|
+
close: () => pool.end()
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2067
2306
|
|
|
2068
2307
|
// src/commands/db/generate.ts
|
|
2069
2308
|
async function dbGenerate() {
|
|
@@ -2078,41 +2317,241 @@ async function dbGenerate() {
|
|
|
2078
2317
|
}
|
|
2079
2318
|
|
|
2080
2319
|
// src/commands/db/push.ts
|
|
2081
|
-
import
|
|
2320
|
+
import chalk13 from "chalk";
|
|
2321
|
+
import prompts3 from "prompts";
|
|
2082
2322
|
import "@spfn/core/config";
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2323
|
+
import { loadEnv as loadEnv3 } from "@spfn/core/server";
|
|
2324
|
+
import { sql } from "drizzle-orm";
|
|
2325
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
2326
|
+
|
|
2327
|
+
// src/commands/db/utils/sql-classifier.ts
|
|
2328
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
2329
|
+
{ pattern: /^\s*DROP\s+TABLE/i, reason: "Drops entire table" },
|
|
2330
|
+
{ pattern: /^\s*DROP\s+INDEX/i, reason: "Drops index" },
|
|
2331
|
+
{ pattern: /^\s*DROP\s+SCHEMA/i, reason: "Drops schema" },
|
|
2332
|
+
{ pattern: /^\s*DROP\s+TYPE/i, reason: "Drops custom type" },
|
|
2333
|
+
{ pattern: /ALTER\s+TABLE\s+.*\bDROP\s+COLUMN\b/i, reason: "Drops column" },
|
|
2334
|
+
{ pattern: /ALTER\s+TABLE\s+.*\bDROP\s+CONSTRAINT\b/i, reason: "Drops constraint" },
|
|
2335
|
+
{ pattern: /ALTER\s+TABLE\s+.*ALTER\s+COLUMN\s+.*\bTYPE\b/i, reason: "Changes column type (potential data loss)" },
|
|
2336
|
+
{ pattern: /ALTER\s+TYPE\s+.*\bRENAME\s+VALUE\b/i, reason: "Renames enum value" },
|
|
2337
|
+
{ pattern: /^\s*TRUNCATE\b/i, reason: "Truncates table data" },
|
|
2338
|
+
{ pattern: /^\s*DELETE\s+FROM\b/i, reason: "Deletes table data" }
|
|
2339
|
+
];
|
|
2340
|
+
var WARNING_PATTERNS = [
|
|
2341
|
+
{ pattern: /ALTER\s+TABLE\s+.*ALTER\s+COLUMN\s+.*\bSET\s+NOT\s+NULL\b/i, reason: "Adds NOT NULL (fails if NULLs exist)" },
|
|
2342
|
+
{ pattern: /ALTER\s+TABLE\s+.*\bRENAME\s+COLUMN\b/i, reason: "Renames column" }
|
|
2343
|
+
];
|
|
2344
|
+
function classifyStatement(sql2) {
|
|
2345
|
+
for (const { pattern, reason } of DESTRUCTIVE_PATTERNS) {
|
|
2346
|
+
if (pattern.test(sql2)) {
|
|
2347
|
+
return { sql: sql2, category: "destructive", reason };
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
for (const { pattern, reason } of WARNING_PATTERNS) {
|
|
2351
|
+
if (pattern.test(sql2)) {
|
|
2352
|
+
return { sql: sql2, category: "warning", reason };
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return { sql: sql2, category: "safe", reason: "" };
|
|
2356
|
+
}
|
|
2357
|
+
function classifyStatements(statements) {
|
|
2358
|
+
const result = { safe: [], destructive: [], warning: [] };
|
|
2359
|
+
for (const sql2 of statements) {
|
|
2360
|
+
const classified = classifyStatement(sql2);
|
|
2361
|
+
result[classified.category].push(classified);
|
|
2362
|
+
}
|
|
2363
|
+
return result;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// src/commands/db/utils/push-display.ts
|
|
2367
|
+
import chalk11 from "chalk";
|
|
2368
|
+
function formatSql(sql2) {
|
|
2369
|
+
return sql2.replace(/\s+/g, " ").trim();
|
|
2370
|
+
}
|
|
2371
|
+
function printStatements(statements, color) {
|
|
2372
|
+
for (const stmt of statements) {
|
|
2373
|
+
console.log(color(` ${formatSql(stmt.sql)}`));
|
|
2374
|
+
if (stmt.reason) {
|
|
2375
|
+
console.log(chalk11.dim(` \u2192 ${stmt.reason}`));
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
function displayClassifiedStatements(result) {
|
|
2380
|
+
if (result.safe.length > 0) {
|
|
2381
|
+
console.log(chalk11.green(`
|
|
2382
|
+
\u2705 Safe changes (${result.safe.length}):`));
|
|
2383
|
+
printStatements(result.safe, chalk11.green);
|
|
2384
|
+
}
|
|
2385
|
+
if (result.warning.length > 0) {
|
|
2386
|
+
console.log(chalk11.yellow(`
|
|
2387
|
+
\u26A0\uFE0F Warnings (${result.warning.length}):`));
|
|
2388
|
+
printStatements(result.warning, chalk11.yellow);
|
|
2389
|
+
}
|
|
2390
|
+
if (result.destructive.length > 0) {
|
|
2391
|
+
console.log(chalk11.red(`
|
|
2392
|
+
\u274C Destructive changes (${result.destructive.length}):`));
|
|
2393
|
+
printStatements(result.destructive, chalk11.red);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function displayDryRunSummary(result) {
|
|
2397
|
+
const total = result.safe.length + result.warning.length + result.destructive.length;
|
|
2398
|
+
console.log(chalk11.cyan(`
|
|
2399
|
+
\u{1F50D} Dry-run: ${total} statement(s) detected
|
|
2400
|
+
`));
|
|
2401
|
+
displayClassifiedStatements(result);
|
|
2402
|
+
console.log("");
|
|
2403
|
+
}
|
|
2404
|
+
function displayApplySummary(applied, skipped) {
|
|
2405
|
+
if (applied > 0) {
|
|
2406
|
+
console.log(chalk11.green(`
|
|
2407
|
+
\u2705 Applied ${applied} statement(s)`));
|
|
2408
|
+
}
|
|
2409
|
+
if (skipped > 0) {
|
|
2410
|
+
console.log(chalk11.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
|
|
2411
|
+
}
|
|
2412
|
+
console.log("");
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// src/commands/db/push.ts
|
|
2416
|
+
async function dbPush(options = {}) {
|
|
2417
|
+
validateDatabasePrerequisites();
|
|
2418
|
+
loadEnv3();
|
|
2419
|
+
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
2420
|
+
const config = getDrizzleConfig({
|
|
2421
|
+
cwd: process.cwd(),
|
|
2422
|
+
expandGlobs: true,
|
|
2423
|
+
autoDetectSchemas: true,
|
|
2424
|
+
disablePackageDiscovery: true
|
|
2425
|
+
});
|
|
2426
|
+
const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
|
|
2427
|
+
if (schemaFiles.length === 0) {
|
|
2428
|
+
console.log(chalk13.yellow("No schema files found."));
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
console.log(chalk13.dim(`Found ${schemaFiles.length} schema file(s)
|
|
2432
|
+
`));
|
|
2433
|
+
const imports = await loadSchemaImports(schemaFiles);
|
|
2434
|
+
const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
|
|
2435
|
+
for (const value of Object.values(imports)) {
|
|
2097
2436
|
try {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2437
|
+
const cfg = getTableConfig(value);
|
|
2438
|
+
if (cfg.schema) {
|
|
2439
|
+
detectedSchemas.add(cfg.schema);
|
|
2440
|
+
}
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
const schemaFilter = Array.from(detectedSchemas);
|
|
2445
|
+
const { db, close } = await createPushConnection();
|
|
2446
|
+
try {
|
|
2447
|
+
const { pushSchema } = await import("drizzle-kit/api");
|
|
2448
|
+
const { statementsToExecute } = await pushSchema(
|
|
2449
|
+
imports,
|
|
2450
|
+
db,
|
|
2451
|
+
schemaFilter
|
|
2452
|
+
);
|
|
2453
|
+
const managedSchemaSet = new Set(schemaFilter);
|
|
2454
|
+
const patched = statementsToExecute.filter((s) => {
|
|
2455
|
+
const dropMatch = s.match(/^\s*DROP\s+SCHEMA\s+"?([^"\s;]+)"?/i);
|
|
2456
|
+
if (dropMatch && managedSchemaSet.has(dropMatch[1])) {
|
|
2457
|
+
console.log(chalk13.dim(` [skip] DROP SCHEMA "${dropMatch[1]}" \u2014 managed schema, ignoring`));
|
|
2458
|
+
return false;
|
|
2459
|
+
}
|
|
2460
|
+
return true;
|
|
2461
|
+
}).map(
|
|
2462
|
+
(s) => s.replace(/^CREATE SCHEMA(?!\s+IF\s+NOT\s+EXISTS)/i, "CREATE SCHEMA IF NOT EXISTS")
|
|
2463
|
+
);
|
|
2464
|
+
const ensureSchemas = schemaFilter.filter((s) => s !== "public").map((s) => `CREATE SCHEMA IF NOT EXISTS "${s}";
|
|
2465
|
+
`);
|
|
2466
|
+
const statements = [...ensureSchemas, ...patched];
|
|
2467
|
+
if (patched.length === 0) {
|
|
2468
|
+
console.log(chalk13.green("\u2705 No changes detected \u2014 database is up to date\n"));
|
|
2469
|
+
await applyFunctionMigrations();
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
const result = classifyStatements(statements);
|
|
2473
|
+
if (options.dryRun) {
|
|
2474
|
+
displayDryRunSummary(result);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
displayClassifiedStatements(result);
|
|
2478
|
+
if (options.force) {
|
|
2479
|
+
console.log(chalk13.dim("\n--force: applying all changes..."));
|
|
2480
|
+
for (const stmt of statements) {
|
|
2481
|
+
await db.execute(sql.raw(stmt));
|
|
2482
|
+
}
|
|
2483
|
+
displayApplySummary(statements.length, 0);
|
|
2484
|
+
} else if (result.destructive.length === 0) {
|
|
2485
|
+
for (const stmt of statements) {
|
|
2486
|
+
await db.execute(sql.raw(stmt));
|
|
2487
|
+
}
|
|
2488
|
+
displayApplySummary(statements.length, 0);
|
|
2489
|
+
} else {
|
|
2490
|
+
const safeCount = result.safe.length + result.warning.length;
|
|
2491
|
+
if (safeCount > 0) {
|
|
2492
|
+
for (const stmt of [...result.safe, ...result.warning]) {
|
|
2493
|
+
await db.execute(sql.raw(stmt.sql));
|
|
2494
|
+
}
|
|
2495
|
+
console.log(chalk13.green(`
|
|
2496
|
+
\u2705 Applied ${safeCount} safe statement(s)`));
|
|
2497
|
+
}
|
|
2498
|
+
console.log(chalk13.red(`
|
|
2499
|
+
\u274C ${result.destructive.length} destructive change(s) require confirmation:`));
|
|
2500
|
+
for (const stmt of result.destructive) {
|
|
2501
|
+
console.log(chalk13.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
2502
|
+
console.log(chalk13.dim(` \u2192 ${stmt.reason}`));
|
|
2503
|
+
}
|
|
2504
|
+
const { confirm } = await prompts3({
|
|
2505
|
+
type: "confirm",
|
|
2506
|
+
name: "confirm",
|
|
2507
|
+
message: "Apply destructive changes?",
|
|
2508
|
+
initial: false
|
|
2509
|
+
});
|
|
2510
|
+
if (confirm) {
|
|
2511
|
+
for (const stmt of result.destructive) {
|
|
2512
|
+
await db.execute(sql.raw(stmt.sql));
|
|
2513
|
+
}
|
|
2514
|
+
displayApplySummary(statements.length, 0);
|
|
2515
|
+
} else {
|
|
2516
|
+
displayApplySummary(safeCount, result.destructive.length);
|
|
2517
|
+
console.log(chalk13.dim("Tip: Use --force to apply all changes without prompting.\n"));
|
|
2518
|
+
}
|
|
2104
2519
|
}
|
|
2520
|
+
await applyFunctionMigrations();
|
|
2521
|
+
} finally {
|
|
2522
|
+
await close();
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
async function applyFunctionMigrations() {
|
|
2526
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2527
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2528
|
+
if (functions.length === 0) {
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
console.log(chalk13.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
2532
|
+
functions.forEach((func) => {
|
|
2533
|
+
console.log(chalk13.dim(` - ${func.packageName}`));
|
|
2534
|
+
});
|
|
2535
|
+
try {
|
|
2536
|
+
await executeFunctionMigrations2(functions);
|
|
2537
|
+
console.log(chalk13.green("\n\u2705 All function migrations applied\n"));
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
console.error(chalk13.red("\n\u274C Failed to apply function migrations"));
|
|
2540
|
+
console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2541
|
+
process.exit(1);
|
|
2105
2542
|
}
|
|
2106
2543
|
}
|
|
2107
2544
|
|
|
2108
2545
|
// src/commands/db/migrate.ts
|
|
2109
|
-
import
|
|
2546
|
+
import chalk17 from "chalk";
|
|
2547
|
+
import { join as join16 } from "path";
|
|
2548
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2110
2549
|
|
|
2111
2550
|
// src/commands/db/backup.ts
|
|
2112
2551
|
import { promises as fs3 } from "fs";
|
|
2113
2552
|
import path3 from "path";
|
|
2114
2553
|
import { spawn as spawn2 } from "child_process";
|
|
2115
|
-
import
|
|
2554
|
+
import chalk16 from "chalk";
|
|
2116
2555
|
import ora7 from "ora";
|
|
2117
2556
|
|
|
2118
2557
|
// src/commands/db/utils/database.ts
|
|
@@ -2181,14 +2620,14 @@ function formatTimestamp() {
|
|
|
2181
2620
|
import { promises as fs2 } from "fs";
|
|
2182
2621
|
import { existsSync as existsSync17 } from "fs";
|
|
2183
2622
|
import path2 from "path";
|
|
2184
|
-
import
|
|
2623
|
+
import chalk15 from "chalk";
|
|
2185
2624
|
|
|
2186
2625
|
// src/commands/db/utils/metadata.ts
|
|
2187
2626
|
import { promises as fs } from "fs";
|
|
2188
2627
|
import path from "path";
|
|
2189
2628
|
import { promisify } from "util";
|
|
2190
2629
|
import { exec } from "child_process";
|
|
2191
|
-
import
|
|
2630
|
+
import chalk14 from "chalk";
|
|
2192
2631
|
var execAsync = promisify(exec);
|
|
2193
2632
|
async function collectGitInfo() {
|
|
2194
2633
|
try {
|
|
@@ -2251,7 +2690,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
2251
2690
|
await pool.end();
|
|
2252
2691
|
}
|
|
2253
2692
|
} catch (error) {
|
|
2254
|
-
console.log(
|
|
2693
|
+
console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
2255
2694
|
return void 0;
|
|
2256
2695
|
}
|
|
2257
2696
|
}
|
|
@@ -2259,9 +2698,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
2259
2698
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
2260
2699
|
try {
|
|
2261
2700
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
2262
|
-
console.log(
|
|
2701
|
+
console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
2263
2702
|
} catch (error) {
|
|
2264
|
-
console.log(
|
|
2703
|
+
console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
|
|
2265
2704
|
}
|
|
2266
2705
|
}
|
|
2267
2706
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -2290,10 +2729,10 @@ async function ensureBackupInGitignore() {
|
|
|
2290
2729
|
if (!hasBackupsIgnore) {
|
|
2291
2730
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
2292
2731
|
await fs2.appendFile(gitignorePath, entry);
|
|
2293
|
-
console.log(
|
|
2732
|
+
console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
|
|
2294
2733
|
}
|
|
2295
2734
|
} catch (error) {
|
|
2296
|
-
console.log(
|
|
2735
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
2297
2736
|
}
|
|
2298
2737
|
}
|
|
2299
2738
|
async function ensureBackupDir() {
|
|
@@ -2341,12 +2780,14 @@ async function listBackupFiles() {
|
|
|
2341
2780
|
|
|
2342
2781
|
// src/commands/db/backup.ts
|
|
2343
2782
|
import { env as env3 } from "@spfn/core/config";
|
|
2783
|
+
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
2344
2784
|
async function dbBackup(options) {
|
|
2345
|
-
console.log(
|
|
2785
|
+
console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2786
|
+
loadEnv4();
|
|
2346
2787
|
const dbUrl = env3.DATABASE_URL;
|
|
2347
2788
|
if (!dbUrl) {
|
|
2348
|
-
console.error(
|
|
2349
|
-
console.log(
|
|
2789
|
+
console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
|
|
2790
|
+
console.log(chalk16.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2350
2791
|
process.exit(1);
|
|
2351
2792
|
}
|
|
2352
2793
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2356,7 +2797,7 @@ async function dbBackup(options) {
|
|
|
2356
2797
|
const ext = format === "sql" ? "sql" : "dump";
|
|
2357
2798
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
2358
2799
|
if (options.dataOnly && options.schemaOnly) {
|
|
2359
|
-
console.error(
|
|
2800
|
+
console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2360
2801
|
process.exit(1);
|
|
2361
2802
|
}
|
|
2362
2803
|
const args = [
|
|
@@ -2402,11 +2843,11 @@ async function dbBackup(options) {
|
|
|
2402
2843
|
const stats = await fs3.stat(filename);
|
|
2403
2844
|
const size = formatBytes(stats.size);
|
|
2404
2845
|
spinner.succeed("Backup created");
|
|
2405
|
-
console.log(
|
|
2846
|
+
console.log(chalk16.green(`
|
|
2406
2847
|
\u2705 Backup created successfully`));
|
|
2407
|
-
console.log(
|
|
2408
|
-
console.log(
|
|
2409
|
-
console.log(
|
|
2848
|
+
console.log(chalk16.gray(` File: ${filename}`));
|
|
2849
|
+
console.log(chalk16.gray(` Size: ${size}`));
|
|
2850
|
+
console.log(chalk16.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
2410
2851
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
2411
2852
|
collectGitInfo(),
|
|
2412
2853
|
collectMigrationInfo(dbUrl)
|
|
@@ -2446,18 +2887,20 @@ async function dbBackup(options) {
|
|
|
2446
2887
|
reject(error);
|
|
2447
2888
|
});
|
|
2448
2889
|
}).catch((error) => {
|
|
2449
|
-
console.error(
|
|
2890
|
+
console.error(chalk16.red("\n\u274C Failed to create backup"));
|
|
2450
2891
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
2451
|
-
console.error(
|
|
2892
|
+
console.error(chalk16.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
2452
2893
|
} else {
|
|
2453
|
-
console.error(
|
|
2894
|
+
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2454
2895
|
}
|
|
2455
2896
|
process.exit(1);
|
|
2456
2897
|
});
|
|
2457
2898
|
}
|
|
2458
2899
|
|
|
2459
2900
|
// src/commands/db/migrate.ts
|
|
2460
|
-
import "@spfn/core/config";
|
|
2901
|
+
import { env as env4 } from "@spfn/core/config";
|
|
2902
|
+
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
2903
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
2461
2904
|
async function dbMigrate(options = {}) {
|
|
2462
2905
|
try {
|
|
2463
2906
|
validateDatabasePrerequisites();
|
|
@@ -2465,7 +2908,7 @@ async function dbMigrate(options = {}) {
|
|
|
2465
2908
|
process.exit(1);
|
|
2466
2909
|
}
|
|
2467
2910
|
if (options.withBackup) {
|
|
2468
|
-
console.log(
|
|
2911
|
+
console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
2469
2912
|
await dbBackup({
|
|
2470
2913
|
format: "custom",
|
|
2471
2914
|
tag: "pre-migration",
|
|
@@ -2473,59 +2916,72 @@ async function dbMigrate(options = {}) {
|
|
|
2473
2916
|
});
|
|
2474
2917
|
console.log("");
|
|
2475
2918
|
}
|
|
2919
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
2920
|
+
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
2921
|
+
const postgres = await import("postgres");
|
|
2922
|
+
loadEnv5();
|
|
2923
|
+
if (!env4.DATABASE_URL) {
|
|
2924
|
+
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2925
|
+
process.exit(1);
|
|
2926
|
+
}
|
|
2476
2927
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2477
2928
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2478
2929
|
if (functions.length > 0) {
|
|
2479
|
-
console.log(
|
|
2930
|
+
console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
|
|
2480
2931
|
functions.forEach((func) => {
|
|
2481
|
-
console.log(
|
|
2932
|
+
console.log(chalk17.dim(` - ${func.packageName}`));
|
|
2482
2933
|
});
|
|
2934
|
+
await executeFunctionMigrations2(functions);
|
|
2935
|
+
console.log(chalk17.green("\u2705 Function migrations applied\n"));
|
|
2936
|
+
}
|
|
2937
|
+
const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
|
|
2938
|
+
if (existsSync18(projectMigrationsDir)) {
|
|
2939
|
+
const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2940
|
+
const projDb = drizzle(projConn);
|
|
2483
2941
|
try {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2942
|
+
console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
|
|
2943
|
+
await migrate(projDb, {
|
|
2944
|
+
migrationsFolder: projectMigrationsDir,
|
|
2945
|
+
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
2946
|
+
});
|
|
2947
|
+
console.log(chalk17.green("\u2705 Project migrations applied successfully"));
|
|
2948
|
+
} finally {
|
|
2949
|
+
await projConn.end();
|
|
2490
2950
|
}
|
|
2951
|
+
} else {
|
|
2952
|
+
console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
|
|
2491
2953
|
}
|
|
2492
|
-
await runWithSpinner(
|
|
2493
|
-
"Running project migrations...",
|
|
2494
|
-
"migrate",
|
|
2495
|
-
"Project migrations applied successfully",
|
|
2496
|
-
"Failed to run project migrations"
|
|
2497
|
-
);
|
|
2498
2954
|
}
|
|
2499
2955
|
|
|
2500
2956
|
// src/commands/db/studio.ts
|
|
2501
|
-
import
|
|
2502
|
-
import { existsSync as
|
|
2957
|
+
import chalk18 from "chalk";
|
|
2958
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
2503
2959
|
import { spawn as spawn3 } from "child_process";
|
|
2504
|
-
import { env as
|
|
2960
|
+
import { env as env5 } from "@spfn/core/config";
|
|
2505
2961
|
import "@spfn/core/config";
|
|
2506
2962
|
async function dbStudio(requestedPort) {
|
|
2507
|
-
console.log(
|
|
2963
|
+
console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
2508
2964
|
const defaultPort = 4983;
|
|
2509
2965
|
const startPort = requestedPort || defaultPort;
|
|
2510
2966
|
let port;
|
|
2511
2967
|
try {
|
|
2512
2968
|
port = await findAvailablePort(startPort);
|
|
2513
2969
|
if (port !== startPort) {
|
|
2514
|
-
console.log(
|
|
2970
|
+
console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
2515
2971
|
`));
|
|
2516
2972
|
}
|
|
2517
2973
|
} catch (error) {
|
|
2518
|
-
console.error(
|
|
2974
|
+
console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
2519
2975
|
process.exit(1);
|
|
2520
2976
|
}
|
|
2521
|
-
const hasUserConfig =
|
|
2977
|
+
const hasUserConfig = existsSync19("./drizzle.config.ts");
|
|
2522
2978
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2523
2979
|
try {
|
|
2524
2980
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2525
2981
|
if (!hasUserConfig) {
|
|
2526
|
-
if (!
|
|
2527
|
-
console.error(
|
|
2528
|
-
console.log(
|
|
2982
|
+
if (!env5.DATABASE_URL) {
|
|
2983
|
+
console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
|
|
2984
|
+
console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2529
2985
|
process.exit(1);
|
|
2530
2986
|
}
|
|
2531
2987
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -2536,33 +2992,34 @@ async function dbStudio(requestedPort) {
|
|
|
2536
2992
|
// Expand glob patterns for Studio compatibility
|
|
2537
2993
|
});
|
|
2538
2994
|
writeFileSync10(tempConfigPath, configContent);
|
|
2539
|
-
console.log(
|
|
2995
|
+
console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
|
|
2540
2996
|
}
|
|
2541
2997
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
2542
2998
|
stdio: "inherit",
|
|
2543
|
-
shell: true
|
|
2999
|
+
shell: true,
|
|
3000
|
+
env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
|
|
2544
3001
|
});
|
|
2545
3002
|
const cleanup = () => {
|
|
2546
|
-
if (!hasUserConfig &&
|
|
2547
|
-
|
|
3003
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3004
|
+
unlinkSync3(tempConfigPath);
|
|
2548
3005
|
}
|
|
2549
3006
|
};
|
|
2550
3007
|
studioProcess.on("exit", (code) => {
|
|
2551
3008
|
cleanup();
|
|
2552
3009
|
if (code !== 0 && code !== null) {
|
|
2553
|
-
console.error(
|
|
3010
|
+
console.error(chalk18.red(`
|
|
2554
3011
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
2555
3012
|
process.exit(code);
|
|
2556
3013
|
}
|
|
2557
3014
|
});
|
|
2558
3015
|
studioProcess.on("error", (error) => {
|
|
2559
3016
|
cleanup();
|
|
2560
|
-
console.error(
|
|
2561
|
-
console.error(
|
|
3017
|
+
console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
|
|
3018
|
+
console.error(chalk18.red(error.message));
|
|
2562
3019
|
process.exit(1);
|
|
2563
3020
|
});
|
|
2564
3021
|
process.on("SIGINT", () => {
|
|
2565
|
-
console.log(
|
|
3022
|
+
console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
2566
3023
|
studioProcess.kill("SIGTERM");
|
|
2567
3024
|
cleanup();
|
|
2568
3025
|
process.exit(0);
|
|
@@ -2573,28 +3030,28 @@ async function dbStudio(requestedPort) {
|
|
|
2573
3030
|
process.exit(0);
|
|
2574
3031
|
});
|
|
2575
3032
|
} catch (error) {
|
|
2576
|
-
if (!hasUserConfig &&
|
|
2577
|
-
|
|
3033
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3034
|
+
unlinkSync3(tempConfigPath);
|
|
2578
3035
|
}
|
|
2579
|
-
console.error(
|
|
2580
|
-
console.error(
|
|
3036
|
+
console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
|
|
3037
|
+
console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2581
3038
|
process.exit(1);
|
|
2582
3039
|
}
|
|
2583
3040
|
}
|
|
2584
3041
|
|
|
2585
3042
|
// src/commands/db/drop.ts
|
|
2586
|
-
import
|
|
2587
|
-
import
|
|
3043
|
+
import chalk19 from "chalk";
|
|
3044
|
+
import prompts4 from "prompts";
|
|
2588
3045
|
async function dbDrop() {
|
|
2589
|
-
console.log(
|
|
2590
|
-
const { confirm } = await
|
|
3046
|
+
console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3047
|
+
const { confirm } = await prompts4({
|
|
2591
3048
|
type: "confirm",
|
|
2592
3049
|
name: "confirm",
|
|
2593
3050
|
message: "Are you sure you want to drop all tables?",
|
|
2594
3051
|
initial: false
|
|
2595
3052
|
});
|
|
2596
3053
|
if (!confirm) {
|
|
2597
|
-
console.log(
|
|
3054
|
+
console.log(chalk19.gray("Cancelled."));
|
|
2598
3055
|
process.exit(0);
|
|
2599
3056
|
}
|
|
2600
3057
|
await runWithSpinner(
|
|
@@ -2606,7 +3063,7 @@ async function dbDrop() {
|
|
|
2606
3063
|
}
|
|
2607
3064
|
|
|
2608
3065
|
// src/commands/db/check.ts
|
|
2609
|
-
import
|
|
3066
|
+
import chalk20 from "chalk";
|
|
2610
3067
|
import ora8 from "ora";
|
|
2611
3068
|
async function dbCheck() {
|
|
2612
3069
|
const spinner = ora8("Checking database connection...").start();
|
|
@@ -2615,7 +3072,7 @@ async function dbCheck() {
|
|
|
2615
3072
|
spinner.succeed("Database connection OK");
|
|
2616
3073
|
} catch (error) {
|
|
2617
3074
|
spinner.fail("Database connection failed");
|
|
2618
|
-
console.error(
|
|
3075
|
+
console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2619
3076
|
process.exit(1);
|
|
2620
3077
|
}
|
|
2621
3078
|
}
|
|
@@ -2623,26 +3080,28 @@ async function dbCheck() {
|
|
|
2623
3080
|
// src/commands/db/restore.ts
|
|
2624
3081
|
import path4 from "path";
|
|
2625
3082
|
import { spawn as spawn4 } from "child_process";
|
|
2626
|
-
import
|
|
3083
|
+
import chalk21 from "chalk";
|
|
2627
3084
|
import ora9 from "ora";
|
|
2628
|
-
import
|
|
2629
|
-
import { env as
|
|
3085
|
+
import prompts5 from "prompts";
|
|
3086
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3087
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
2630
3088
|
async function dbRestore(backupFile, options = {}) {
|
|
2631
|
-
console.log(
|
|
2632
|
-
|
|
3089
|
+
console.log(chalk21.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
3090
|
+
loadEnv6();
|
|
3091
|
+
const dbUrl = env6.DATABASE_URL;
|
|
2633
3092
|
if (!dbUrl) {
|
|
2634
|
-
console.error(
|
|
2635
|
-
console.log(
|
|
3093
|
+
console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
|
|
3094
|
+
console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2636
3095
|
process.exit(1);
|
|
2637
3096
|
}
|
|
2638
3097
|
let file = backupFile;
|
|
2639
3098
|
if (!file) {
|
|
2640
3099
|
const backups = await listBackupFiles();
|
|
2641
3100
|
if (backups.length === 0) {
|
|
2642
|
-
console.log(
|
|
3101
|
+
console.log(chalk21.yellow("No backups found in ./backups directory"));
|
|
2643
3102
|
process.exit(0);
|
|
2644
3103
|
}
|
|
2645
|
-
const { selected } = await
|
|
3104
|
+
const { selected } = await prompts5({
|
|
2646
3105
|
type: "select",
|
|
2647
3106
|
name: "selected",
|
|
2648
3107
|
message: "Select backup to restore:",
|
|
@@ -2652,70 +3111,70 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2652
3111
|
}))
|
|
2653
3112
|
});
|
|
2654
3113
|
if (!selected) {
|
|
2655
|
-
console.log(
|
|
3114
|
+
console.log(chalk21.gray("Cancelled"));
|
|
2656
3115
|
process.exit(0);
|
|
2657
3116
|
}
|
|
2658
3117
|
file = selected;
|
|
2659
3118
|
}
|
|
2660
3119
|
if (!file) {
|
|
2661
|
-
console.error(
|
|
3120
|
+
console.error(chalk21.red("\u274C No backup file selected"));
|
|
2662
3121
|
process.exit(1);
|
|
2663
3122
|
}
|
|
2664
3123
|
const metadata = await loadBackupMetadata(file);
|
|
2665
3124
|
if (metadata) {
|
|
2666
|
-
console.log(
|
|
2667
|
-
console.log(
|
|
2668
|
-
console.log(
|
|
3125
|
+
console.log(chalk21.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
3126
|
+
console.log(chalk21.dim(` Database: ${metadata.database}`));
|
|
3127
|
+
console.log(chalk21.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
2669
3128
|
if (metadata.environment) {
|
|
2670
|
-
console.log(
|
|
3129
|
+
console.log(chalk21.dim(` Environment: ${metadata.environment}`));
|
|
2671
3130
|
}
|
|
2672
3131
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
2673
|
-
console.log(
|
|
3132
|
+
console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
2674
3133
|
}
|
|
2675
3134
|
if (metadata.backup.dataOnly) {
|
|
2676
|
-
console.log(
|
|
3135
|
+
console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
2677
3136
|
}
|
|
2678
3137
|
if (metadata.backup.schemaOnly) {
|
|
2679
|
-
console.log(
|
|
3138
|
+
console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
2680
3139
|
}
|
|
2681
|
-
const
|
|
3140
|
+
const warnings2 = [];
|
|
2682
3141
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
2683
3142
|
collectGitInfo(),
|
|
2684
3143
|
collectMigrationInfo(dbUrl)
|
|
2685
3144
|
]);
|
|
2686
3145
|
if (metadata.git && currentGitInfo) {
|
|
2687
3146
|
if (metadata.git.commit !== currentGitInfo.commit) {
|
|
2688
|
-
|
|
3147
|
+
warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
|
|
2689
3148
|
}
|
|
2690
3149
|
if (metadata.git.branch !== currentGitInfo.branch) {
|
|
2691
|
-
|
|
3150
|
+
warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
|
|
2692
3151
|
}
|
|
2693
3152
|
}
|
|
2694
3153
|
if (metadata.migrations && currentMigrationInfo) {
|
|
2695
3154
|
if (metadata.migrations.hash !== currentMigrationInfo.hash) {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
3155
|
+
warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
|
|
3156
|
+
warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
|
|
3157
|
+
warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
|
|
2699
3158
|
}
|
|
2700
3159
|
}
|
|
2701
|
-
if (
|
|
2702
|
-
console.log(
|
|
2703
|
-
|
|
3160
|
+
if (warnings2.length > 0) {
|
|
3161
|
+
console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
3162
|
+
warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
|
|
2704
3163
|
console.log("");
|
|
2705
3164
|
}
|
|
2706
3165
|
}
|
|
2707
|
-
const { confirm } = await
|
|
3166
|
+
const { confirm } = await prompts5({
|
|
2708
3167
|
type: "confirm",
|
|
2709
3168
|
name: "confirm",
|
|
2710
|
-
message:
|
|
3169
|
+
message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
|
|
2711
3170
|
initial: false
|
|
2712
3171
|
});
|
|
2713
3172
|
if (!confirm) {
|
|
2714
|
-
console.log(
|
|
3173
|
+
console.log(chalk21.gray("Cancelled"));
|
|
2715
3174
|
process.exit(0);
|
|
2716
3175
|
}
|
|
2717
3176
|
if (options.dataOnly && options.schemaOnly) {
|
|
2718
|
-
console.error(
|
|
3177
|
+
console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2719
3178
|
process.exit(1);
|
|
2720
3179
|
}
|
|
2721
3180
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2728,6 +3187,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2728
3187
|
args.push("-p", dbInfo.port);
|
|
2729
3188
|
args.push("-U", dbInfo.user);
|
|
2730
3189
|
args.push("-d", dbInfo.database);
|
|
3190
|
+
args.push("--verbose");
|
|
2731
3191
|
if (options.drop) {
|
|
2732
3192
|
args.push("--clean");
|
|
2733
3193
|
}
|
|
@@ -2743,15 +3203,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2743
3203
|
args.push(file);
|
|
2744
3204
|
} else {
|
|
2745
3205
|
if (options.dataOnly || options.schemaOnly) {
|
|
2746
|
-
console.log(
|
|
2747
|
-
console.log(
|
|
3206
|
+
console.log(chalk21.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
3207
|
+
console.log(chalk21.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
2748
3208
|
}
|
|
2749
3209
|
args.push("-h", dbInfo.host);
|
|
2750
3210
|
args.push("-p", dbInfo.port);
|
|
2751
3211
|
args.push("-U", dbInfo.user);
|
|
2752
3212
|
args.push("-d", dbInfo.database);
|
|
3213
|
+
args.push("-v", "ON_ERROR_STOP=1");
|
|
2753
3214
|
args.push("-f", file);
|
|
2754
3215
|
}
|
|
3216
|
+
const verbose = options.verbose ?? false;
|
|
2755
3217
|
const spinner = ora9("Restoring backup...").start();
|
|
2756
3218
|
const restoreProcess = spawn4(command, args, {
|
|
2757
3219
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -2760,19 +3222,81 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2760
3222
|
PGPASSWORD: dbInfo.password
|
|
2761
3223
|
}
|
|
2762
3224
|
});
|
|
2763
|
-
|
|
3225
|
+
const warnings = [];
|
|
3226
|
+
const errors = [];
|
|
3227
|
+
let objectCount = 0;
|
|
3228
|
+
let lastObject = "";
|
|
2764
3229
|
restoreProcess.stderr?.on("data", (data) => {
|
|
2765
|
-
|
|
3230
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
3231
|
+
for (const line of lines) {
|
|
3232
|
+
if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
|
|
3233
|
+
warnings.push(line.trim());
|
|
3234
|
+
} else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
|
|
3235
|
+
errors.push(line.trim());
|
|
3236
|
+
}
|
|
3237
|
+
const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
|
|
3238
|
+
if (objectMatch) {
|
|
3239
|
+
objectCount = Number(objectMatch[2]);
|
|
3240
|
+
const current = Number(objectMatch[1]);
|
|
3241
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3242
|
+
lastObject = desc;
|
|
3243
|
+
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
3244
|
+
} else if (isCustomFormat) {
|
|
3245
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3246
|
+
if (desc && !/warning:|error:/i.test(desc)) {
|
|
3247
|
+
lastObject = desc;
|
|
3248
|
+
spinner.text = `Restoring backup... ${desc}`;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
if (verbose) {
|
|
3252
|
+
spinner.stop();
|
|
3253
|
+
console.log(chalk21.dim(` ${line.trim()}`));
|
|
3254
|
+
spinner.start();
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
restoreProcess.stdout?.on("data", (data) => {
|
|
3259
|
+
if (verbose) {
|
|
3260
|
+
spinner.stop();
|
|
3261
|
+
console.log(chalk21.dim(` ${data.toString().trim()}`));
|
|
3262
|
+
spinner.start();
|
|
3263
|
+
}
|
|
2766
3264
|
});
|
|
2767
3265
|
await new Promise((resolve2, reject) => {
|
|
2768
3266
|
restoreProcess.on("close", (code) => {
|
|
2769
3267
|
if (code === 0) {
|
|
2770
|
-
|
|
2771
|
-
|
|
3268
|
+
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
3269
|
+
spinner.succeed(`Restore completed${summary}`);
|
|
3270
|
+
if (warnings.length > 0) {
|
|
3271
|
+
console.log(chalk21.yellow(`
|
|
3272
|
+
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
3273
|
+
`));
|
|
3274
|
+
for (const w of warnings) {
|
|
3275
|
+
console.log(chalk21.yellow(` - ${w}`));
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
console.log(chalk21.green("\n\u2705 Database restored successfully"));
|
|
2772
3279
|
resolve2();
|
|
2773
3280
|
} else {
|
|
2774
3281
|
spinner.fail("Restore failed");
|
|
2775
|
-
|
|
3282
|
+
if (errors.length > 0) {
|
|
3283
|
+
console.error(chalk21.red(`
|
|
3284
|
+
\u274C Errors (${errors.length}):
|
|
3285
|
+
`));
|
|
3286
|
+
for (const e of errors) {
|
|
3287
|
+
console.error(chalk21.red(` - ${e}`));
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
if (warnings.length > 0) {
|
|
3291
|
+
console.log(chalk21.yellow(`
|
|
3292
|
+
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
3293
|
+
`));
|
|
3294
|
+
for (const w of warnings) {
|
|
3295
|
+
console.log(chalk21.yellow(` - ${w}`));
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
3299
|
+
reject(new Error(fallback));
|
|
2776
3300
|
}
|
|
2777
3301
|
});
|
|
2778
3302
|
restoreProcess.on("error", (error) => {
|
|
@@ -2780,24 +3304,27 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2780
3304
|
reject(error);
|
|
2781
3305
|
});
|
|
2782
3306
|
}).catch((error) => {
|
|
2783
|
-
|
|
2784
|
-
|
|
3307
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3308
|
+
if (msg) {
|
|
3309
|
+
console.error(chalk21.red(`
|
|
3310
|
+
\u274C ${msg}`));
|
|
3311
|
+
}
|
|
2785
3312
|
process.exit(1);
|
|
2786
3313
|
});
|
|
2787
3314
|
}
|
|
2788
3315
|
|
|
2789
3316
|
// src/commands/db/list.ts
|
|
2790
|
-
import
|
|
3317
|
+
import chalk22 from "chalk";
|
|
2791
3318
|
async function dbBackupList() {
|
|
2792
|
-
console.log(
|
|
3319
|
+
console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
|
|
2793
3320
|
const backups = await listBackupFiles();
|
|
2794
3321
|
if (backups.length === 0) {
|
|
2795
|
-
console.log(
|
|
2796
|
-
console.log(
|
|
3322
|
+
console.log(chalk22.yellow("No backups found in ./backups directory"));
|
|
3323
|
+
console.log(chalk22.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
2797
3324
|
return;
|
|
2798
3325
|
}
|
|
2799
|
-
console.log(
|
|
2800
|
-
console.log(
|
|
3326
|
+
console.log(chalk22.bold(" Date Size File"));
|
|
3327
|
+
console.log(chalk22.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"));
|
|
2801
3328
|
backups.forEach((backup) => {
|
|
2802
3329
|
const date = backup.date.toLocaleString("en-US", {
|
|
2803
3330
|
year: "numeric",
|
|
@@ -2808,23 +3335,23 @@ async function dbBackupList() {
|
|
|
2808
3335
|
second: "2-digit"
|
|
2809
3336
|
});
|
|
2810
3337
|
const sizeStr = backup.size.padEnd(10);
|
|
2811
|
-
console.log(
|
|
3338
|
+
console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
2812
3339
|
});
|
|
2813
|
-
console.log(
|
|
3340
|
+
console.log(chalk22.gray(`
|
|
2814
3341
|
Total: ${backups.length} backup(s)
|
|
2815
3342
|
`));
|
|
2816
3343
|
}
|
|
2817
3344
|
|
|
2818
3345
|
// src/commands/db/clean.ts
|
|
2819
3346
|
import { promises as fs4 } from "fs";
|
|
2820
|
-
import
|
|
3347
|
+
import chalk23 from "chalk";
|
|
2821
3348
|
import ora10 from "ora";
|
|
2822
|
-
import
|
|
3349
|
+
import prompts6 from "prompts";
|
|
2823
3350
|
async function dbBackupClean(options) {
|
|
2824
|
-
console.log(
|
|
3351
|
+
console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
2825
3352
|
const backups = await listBackupFiles();
|
|
2826
3353
|
if (backups.length === 0) {
|
|
2827
|
-
console.log(
|
|
3354
|
+
console.log(chalk23.yellow("No backups found"));
|
|
2828
3355
|
return;
|
|
2829
3356
|
}
|
|
2830
3357
|
let toDelete = [];
|
|
@@ -2841,74 +3368,176 @@ async function dbBackupClean(options) {
|
|
|
2841
3368
|
toDelete = backups.slice(defaultKeep);
|
|
2842
3369
|
}
|
|
2843
3370
|
if (toDelete.length === 0) {
|
|
2844
|
-
console.log(
|
|
3371
|
+
console.log(chalk23.green("\u2705 No backups to clean"));
|
|
2845
3372
|
return;
|
|
2846
3373
|
}
|
|
2847
|
-
console.log(
|
|
3374
|
+
console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
2848
3375
|
`));
|
|
2849
3376
|
toDelete.forEach((backup) => {
|
|
2850
|
-
console.log(
|
|
3377
|
+
console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
|
|
2851
3378
|
});
|
|
2852
|
-
const { confirm } = await
|
|
3379
|
+
const { confirm } = await prompts6({
|
|
2853
3380
|
type: "confirm",
|
|
2854
3381
|
name: "confirm",
|
|
2855
3382
|
message: "\nProceed with deletion?",
|
|
2856
3383
|
initial: false
|
|
2857
3384
|
});
|
|
2858
3385
|
if (!confirm) {
|
|
2859
|
-
console.log(
|
|
3386
|
+
console.log(chalk23.gray("Cancelled"));
|
|
2860
3387
|
return;
|
|
2861
3388
|
}
|
|
2862
3389
|
const spinner = ora10("Deleting backups...").start();
|
|
2863
3390
|
try {
|
|
2864
3391
|
await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
|
|
2865
3392
|
spinner.succeed("Backups deleted");
|
|
2866
|
-
console.log(
|
|
3393
|
+
console.log(chalk23.green(`
|
|
2867
3394
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
2868
3395
|
} catch (error) {
|
|
2869
3396
|
spinner.fail("Failed to delete backups");
|
|
2870
|
-
console.error(
|
|
3397
|
+
console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2871
3398
|
process.exit(1);
|
|
2872
3399
|
}
|
|
2873
3400
|
}
|
|
2874
3401
|
|
|
3402
|
+
// src/commands/db/reindex.ts
|
|
3403
|
+
import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
|
|
3404
|
+
import { join as join17 } from "path";
|
|
3405
|
+
import chalk24 from "chalk";
|
|
3406
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3407
|
+
function isTimestampPrefix(tag) {
|
|
3408
|
+
const prefix = tag.split("_")[0];
|
|
3409
|
+
return /^\d{5,}$/.test(prefix);
|
|
3410
|
+
}
|
|
3411
|
+
function parseTag(tag) {
|
|
3412
|
+
const underscoreIdx = tag.indexOf("_");
|
|
3413
|
+
if (underscoreIdx === -1) {
|
|
3414
|
+
return { prefix: tag, suffix: "" };
|
|
3415
|
+
}
|
|
3416
|
+
return {
|
|
3417
|
+
prefix: tag.substring(0, underscoreIdx),
|
|
3418
|
+
suffix: tag.substring(underscoreIdx + 1)
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
async function dbReindex(options = {}) {
|
|
3422
|
+
loadEnv7();
|
|
3423
|
+
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3424
|
+
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
3425
|
+
const outDir = config.out;
|
|
3426
|
+
const journalPath = join17(outDir, "meta", "_journal.json");
|
|
3427
|
+
if (!existsSync20(journalPath)) {
|
|
3428
|
+
console.error(chalk24.red("\u274C No _journal.json found at:"), journalPath);
|
|
3429
|
+
console.log(chalk24.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
3430
|
+
process.exit(1);
|
|
3431
|
+
}
|
|
3432
|
+
const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
|
|
3433
|
+
if (journal.entries.length === 0) {
|
|
3434
|
+
console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
const renames = [];
|
|
3438
|
+
const tagUpdates = [];
|
|
3439
|
+
let skipped = 0;
|
|
3440
|
+
for (const entry of journal.entries) {
|
|
3441
|
+
if (isTimestampPrefix(entry.tag)) {
|
|
3442
|
+
skipped++;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
3446
|
+
const newPrefix = String(entry.when);
|
|
3447
|
+
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
3448
|
+
const oldSql = join17(outDir, `${entry.tag}.sql`);
|
|
3449
|
+
const newSql = join17(outDir, `${newTag}.sql`);
|
|
3450
|
+
if (existsSync20(oldSql)) {
|
|
3451
|
+
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
3452
|
+
}
|
|
3453
|
+
const oldSnapshot = join17(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
3454
|
+
const newSnapshot = join17(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
3455
|
+
if (existsSync20(oldSnapshot)) {
|
|
3456
|
+
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
3457
|
+
}
|
|
3458
|
+
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
3459
|
+
}
|
|
3460
|
+
if (tagUpdates.length === 0) {
|
|
3461
|
+
console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
3462
|
+
if (skipped > 0) {
|
|
3463
|
+
console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
3464
|
+
}
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
3468
|
+
for (const update of tagUpdates) {
|
|
3469
|
+
console.log(
|
|
3470
|
+
chalk24.dim(` [${update.idx}]`),
|
|
3471
|
+
chalk24.red(update.oldTag),
|
|
3472
|
+
chalk24.dim("\u2192"),
|
|
3473
|
+
chalk24.green(update.newTag)
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
console.log(chalk24.dim(`
|
|
3477
|
+
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
3478
|
+
if (skipped > 0) {
|
|
3479
|
+
console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
3480
|
+
}
|
|
3481
|
+
if (options.dryRun) {
|
|
3482
|
+
console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
const backupPath = journalPath + ".bak";
|
|
3486
|
+
copyFileSync(journalPath, backupPath);
|
|
3487
|
+
console.log(chalk24.dim(`
|
|
3488
|
+
Backed up journal \u2192 ${backupPath}`));
|
|
3489
|
+
for (const rename of renames) {
|
|
3490
|
+
renameSync(rename.from, rename.to);
|
|
3491
|
+
}
|
|
3492
|
+
for (const update of tagUpdates) {
|
|
3493
|
+
const entry = journal.entries.find((e) => e.idx === update.idx);
|
|
3494
|
+
if (entry) {
|
|
3495
|
+
entry.tag = update.newTag;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
3499
|
+
console.log(chalk24.green(`
|
|
3500
|
+
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
3501
|
+
}
|
|
3502
|
+
|
|
2875
3503
|
// src/commands/db/index.ts
|
|
2876
3504
|
var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
|
|
2877
3505
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
2878
|
-
dbCommand.command("push").description("Push schema changes
|
|
3506
|
+
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));
|
|
2879
3507
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
2880
3508
|
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));
|
|
2881
3509
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
2882
3510
|
dbCommand.command("check").description("Check database connection").action(dbCheck);
|
|
2883
3511
|
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));
|
|
2884
|
-
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)").action((file, options) => dbRestore(file, options));
|
|
3512
|
+
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));
|
|
2885
3513
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
2886
3514
|
dbCommand.command("backup:clean").description("Clean old database backups").option("-k, --keep <number>", "Keep N most recent backups", parseInt).option("-o, --older-than <days>", "Delete backups older than N days", parseInt).action((options) => dbBackupClean(options));
|
|
3515
|
+
dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
|
|
2887
3516
|
|
|
2888
3517
|
// src/commands/add.ts
|
|
2889
3518
|
import { Command as Command10 } from "commander";
|
|
2890
|
-
import { existsSync as
|
|
2891
|
-
import { join as
|
|
3519
|
+
import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
|
|
3520
|
+
import { join as join18 } from "path";
|
|
2892
3521
|
import { exec as exec2 } from "child_process";
|
|
2893
3522
|
import { promisify as promisify2 } from "util";
|
|
2894
|
-
import
|
|
3523
|
+
import chalk25 from "chalk";
|
|
2895
3524
|
import ora11 from "ora";
|
|
2896
3525
|
var execAsync2 = promisify2(exec2);
|
|
2897
3526
|
async function addPackage(packageName) {
|
|
2898
3527
|
if (!packageName.includes("/")) {
|
|
2899
|
-
console.error(
|
|
2900
|
-
console.log(
|
|
2901
|
-
console.log(
|
|
2902
|
-
console.log(
|
|
3528
|
+
console.error(chalk25.red("\u274C Please specify full package name"));
|
|
3529
|
+
console.log(chalk25.yellow("\n\u{1F4A1} Examples:"));
|
|
3530
|
+
console.log(chalk25.gray(" pnpm spfn add @spfn/cms"));
|
|
3531
|
+
console.log(chalk25.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
2903
3532
|
process.exit(1);
|
|
2904
3533
|
}
|
|
2905
|
-
console.log(
|
|
3534
|
+
console.log(chalk25.blue(`
|
|
2906
3535
|
\u{1F4E6} Setting up ${packageName}...
|
|
2907
3536
|
`));
|
|
2908
3537
|
try {
|
|
2909
|
-
const pkgPath =
|
|
2910
|
-
const pkgJsonPath =
|
|
2911
|
-
if (!
|
|
3538
|
+
const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3539
|
+
const pkgJsonPath = join18(pkgPath, "package.json");
|
|
3540
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
2912
3541
|
const installSpinner = ora11("Installing package...").start();
|
|
2913
3542
|
try {
|
|
2914
3543
|
await execAsync2(`pnpm add ${packageName}`);
|
|
@@ -2918,21 +3547,21 @@ async function addPackage(packageName) {
|
|
|
2918
3547
|
throw error;
|
|
2919
3548
|
}
|
|
2920
3549
|
} else {
|
|
2921
|
-
console.log(
|
|
3550
|
+
console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
|
|
2922
3551
|
}
|
|
2923
|
-
if (!
|
|
3552
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
2924
3553
|
throw new Error(`Package ${packageName} not found after installation`);
|
|
2925
3554
|
}
|
|
2926
|
-
const pkgJson = JSON.parse(
|
|
3555
|
+
const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
|
|
2927
3556
|
if (pkgJson.spfn?.migrations) {
|
|
2928
|
-
console.log(
|
|
3557
|
+
console.log(chalk25.blue(`
|
|
2929
3558
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
2930
3559
|
`));
|
|
2931
|
-
const { env:
|
|
2932
|
-
if (!
|
|
2933
|
-
console.log(
|
|
2934
|
-
console.log(
|
|
2935
|
-
console.log(
|
|
3560
|
+
const { env: env7 } = await import("@spfn/core/config");
|
|
3561
|
+
if (!env7.DATABASE_URL) {
|
|
3562
|
+
console.log(chalk25.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
3563
|
+
console.log(chalk25.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
3564
|
+
console.log(chalk25.gray(` pnpm spfn db push
|
|
2936
3565
|
`));
|
|
2937
3566
|
} else {
|
|
2938
3567
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
@@ -2948,25 +3577,25 @@ async function addPackage(packageName) {
|
|
|
2948
3577
|
throw error;
|
|
2949
3578
|
}
|
|
2950
3579
|
} else {
|
|
2951
|
-
console.log(
|
|
3580
|
+
console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
|
|
2952
3581
|
}
|
|
2953
3582
|
}
|
|
2954
3583
|
} else {
|
|
2955
|
-
console.log(
|
|
3584
|
+
console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
2956
3585
|
}
|
|
2957
|
-
console.log(
|
|
3586
|
+
console.log(chalk25.green(`
|
|
2958
3587
|
\u2705 ${packageName} installed successfully!
|
|
2959
3588
|
`));
|
|
2960
3589
|
if (pkgJson.spfn?.setupMessage) {
|
|
2961
|
-
console.log(
|
|
3590
|
+
console.log(chalk25.cyan("\u{1F4DA} Setup Guide:"));
|
|
2962
3591
|
console.log(pkgJson.spfn.setupMessage);
|
|
2963
3592
|
console.log();
|
|
2964
3593
|
}
|
|
2965
3594
|
} catch (error) {
|
|
2966
|
-
console.error(
|
|
3595
|
+
console.error(chalk25.red(`
|
|
2967
3596
|
\u274C Failed to install ${packageName}
|
|
2968
3597
|
`));
|
|
2969
|
-
console.error(
|
|
3598
|
+
console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2970
3599
|
process.exit(1);
|
|
2971
3600
|
}
|
|
2972
3601
|
}
|
|
@@ -2976,16 +3605,16 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
|
|
|
2976
3605
|
init_logger();
|
|
2977
3606
|
import { Command as Command11 } from "commander";
|
|
2978
3607
|
import ora12 from "ora";
|
|
2979
|
-
import { join as
|
|
2980
|
-
import { existsSync as
|
|
2981
|
-
import
|
|
3608
|
+
import { join as join27 } from "path";
|
|
3609
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3610
|
+
import chalk27 from "chalk";
|
|
2982
3611
|
|
|
2983
3612
|
// src/commands/generate/prompts.ts
|
|
2984
3613
|
init_logger();
|
|
2985
|
-
import
|
|
2986
|
-
import
|
|
3614
|
+
import prompts7 from "prompts";
|
|
3615
|
+
import chalk26 from "chalk";
|
|
2987
3616
|
async function promptScope() {
|
|
2988
|
-
const response = await
|
|
3617
|
+
const response = await prompts7({
|
|
2989
3618
|
type: "text",
|
|
2990
3619
|
name: "scope",
|
|
2991
3620
|
message: "NPM scope (e.g., @mycompany, @username):",
|
|
@@ -3007,7 +3636,7 @@ async function promptScope() {
|
|
|
3007
3636
|
return response.scope;
|
|
3008
3637
|
}
|
|
3009
3638
|
async function promptFunctionName() {
|
|
3010
|
-
const response = await
|
|
3639
|
+
const response = await prompts7({
|
|
3011
3640
|
type: "text",
|
|
3012
3641
|
name: "fnName",
|
|
3013
3642
|
message: "Function name:",
|
|
@@ -3028,7 +3657,7 @@ async function promptFunctionName() {
|
|
|
3028
3657
|
return response.fnName;
|
|
3029
3658
|
}
|
|
3030
3659
|
async function promptDescription(fnName) {
|
|
3031
|
-
const response = await
|
|
3660
|
+
const response = await prompts7({
|
|
3032
3661
|
type: "text",
|
|
3033
3662
|
name: "description",
|
|
3034
3663
|
message: "Function description:",
|
|
@@ -3037,7 +3666,7 @@ async function promptDescription(fnName) {
|
|
|
3037
3666
|
return response.description || "A description of what this module does";
|
|
3038
3667
|
}
|
|
3039
3668
|
async function promptEntities() {
|
|
3040
|
-
const response = await
|
|
3669
|
+
const response = await prompts7({
|
|
3041
3670
|
type: "list",
|
|
3042
3671
|
name: "entities",
|
|
3043
3672
|
message: "Entity names (comma-separated, press enter to skip):",
|
|
@@ -3049,14 +3678,14 @@ async function promptEntities() {
|
|
|
3049
3678
|
async function confirmConfiguration(config) {
|
|
3050
3679
|
const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
|
|
3051
3680
|
console.log("");
|
|
3052
|
-
logger.info(
|
|
3053
|
-
console.log(` ${
|
|
3054
|
-
console.log(` ${
|
|
3055
|
-
console.log(` ${
|
|
3056
|
-
console.log(` ${
|
|
3057
|
-
console.log(` ${
|
|
3681
|
+
logger.info(chalk26.bold("\u26A1 Function Configuration:"));
|
|
3682
|
+
console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
|
|
3683
|
+
console.log(` ${chalk26.gray("Description:")} ${description}`);
|
|
3684
|
+
console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
|
|
3685
|
+
console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3686
|
+
console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3058
3687
|
console.log("");
|
|
3059
|
-
const { confirmed } = await
|
|
3688
|
+
const { confirmed } = await prompts7({
|
|
3060
3689
|
type: "confirm",
|
|
3061
3690
|
name: "confirmed",
|
|
3062
3691
|
message: "Create function?",
|
|
@@ -3070,12 +3699,12 @@ async function confirmConfiguration(config) {
|
|
|
3070
3699
|
}
|
|
3071
3700
|
|
|
3072
3701
|
// src/commands/generate/generators/structure.ts
|
|
3073
|
-
import { join as
|
|
3074
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
3702
|
+
import { join as join26 } from "path";
|
|
3703
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
3075
3704
|
|
|
3076
3705
|
// src/commands/generate/generators/config.ts
|
|
3077
|
-
import { join as
|
|
3078
|
-
import { writeFileSync as
|
|
3706
|
+
import { join as join20 } from "path";
|
|
3707
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
|
|
3079
3708
|
|
|
3080
3709
|
// src/commands/generate/string-utils.ts
|
|
3081
3710
|
function toPascalCase(str) {
|
|
@@ -3104,30 +3733,30 @@ function toSafeSchemaName(str) {
|
|
|
3104
3733
|
}
|
|
3105
3734
|
|
|
3106
3735
|
// src/commands/generate/template-loader.ts
|
|
3107
|
-
import { readFileSync as
|
|
3108
|
-
import { join as
|
|
3736
|
+
import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
|
|
3737
|
+
import { join as join19, dirname as dirname2 } from "path";
|
|
3109
3738
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3110
3739
|
function findTemplatesPath2() {
|
|
3111
3740
|
const __filename = fileURLToPath2(import.meta.url);
|
|
3112
3741
|
const __dirname2 = dirname2(__filename);
|
|
3113
|
-
const distPath =
|
|
3114
|
-
if (
|
|
3742
|
+
const distPath = join19(__dirname2, "commands", "generate", "templates");
|
|
3743
|
+
if (existsSync22(distPath)) {
|
|
3115
3744
|
return distPath;
|
|
3116
3745
|
}
|
|
3117
|
-
const sameDirPath =
|
|
3118
|
-
if (
|
|
3746
|
+
const sameDirPath = join19(__dirname2, "templates");
|
|
3747
|
+
if (existsSync22(sameDirPath)) {
|
|
3119
3748
|
return sameDirPath;
|
|
3120
3749
|
}
|
|
3121
|
-
const srcPath =
|
|
3122
|
-
if (
|
|
3750
|
+
const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
|
|
3751
|
+
if (existsSync22(srcPath)) {
|
|
3123
3752
|
return srcPath;
|
|
3124
3753
|
}
|
|
3125
3754
|
throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
|
|
3126
3755
|
}
|
|
3127
3756
|
function loadTemplate(templateName, variables) {
|
|
3128
3757
|
const templatesDir = findTemplatesPath2();
|
|
3129
|
-
const templatePath =
|
|
3130
|
-
let content =
|
|
3758
|
+
const templatePath = join19(templatesDir, `${templateName}.template`);
|
|
3759
|
+
let content = readFileSync8(templatePath, "utf-8");
|
|
3131
3760
|
for (const [key, value] of Object.entries(variables)) {
|
|
3132
3761
|
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
3133
3762
|
content = content.replace(regex, value);
|
|
@@ -3229,8 +3858,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
|
|
|
3229
3858
|
vitest: "^4.0.6"
|
|
3230
3859
|
}
|
|
3231
3860
|
};
|
|
3232
|
-
|
|
3233
|
-
|
|
3861
|
+
writeFileSync12(
|
|
3862
|
+
join20(fnDir, "package.json"),
|
|
3234
3863
|
JSON.stringify(content, null, 4) + "\n"
|
|
3235
3864
|
);
|
|
3236
3865
|
}
|
|
@@ -3263,8 +3892,8 @@ function generateTsConfig(fnDir) {
|
|
|
3263
3892
|
include: ["src/**/*"],
|
|
3264
3893
|
exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
|
|
3265
3894
|
};
|
|
3266
|
-
|
|
3267
|
-
|
|
3895
|
+
writeFileSync12(
|
|
3896
|
+
join20(fnDir, "tsconfig.json"),
|
|
3268
3897
|
JSON.stringify(content, null, 4) + "\n"
|
|
3269
3898
|
);
|
|
3270
3899
|
}
|
|
@@ -3340,7 +3969,7 @@ export default defineConfig({
|
|
|
3340
3969
|
],
|
|
3341
3970
|
});
|
|
3342
3971
|
`;
|
|
3343
|
-
|
|
3972
|
+
writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
|
|
3344
3973
|
}
|
|
3345
3974
|
function generateDrizzleConfig(fnDir, scope, fnName) {
|
|
3346
3975
|
const schemaName = `spfn_${toSnakeCase(fnName)}`;
|
|
@@ -3360,7 +3989,7 @@ export default defineConfig({
|
|
|
3360
3989
|
schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
|
|
3361
3990
|
});
|
|
3362
3991
|
`;
|
|
3363
|
-
|
|
3992
|
+
writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
|
|
3364
3993
|
}
|
|
3365
3994
|
function generateExampleGenerator(fnDir, scope, fnName) {
|
|
3366
3995
|
const pascalName = toPascalCase(fnName);
|
|
@@ -3429,10 +4058,10 @@ export const moduleName = '${fnName}';
|
|
|
3429
4058
|
};
|
|
3430
4059
|
}
|
|
3431
4060
|
`;
|
|
3432
|
-
const generatorsDir =
|
|
4061
|
+
const generatorsDir = join20(fnDir, "src/server/generators");
|
|
3433
4062
|
mkdirSync3(generatorsDir, { recursive: true });
|
|
3434
|
-
|
|
3435
|
-
|
|
4063
|
+
writeFileSync12(
|
|
4064
|
+
join20(generatorsDir, "example-generator.ts"),
|
|
3436
4065
|
content
|
|
3437
4066
|
);
|
|
3438
4067
|
const indexContent = `/**
|
|
@@ -3446,8 +4075,8 @@ export const moduleName = '${fnName}';
|
|
|
3446
4075
|
|
|
3447
4076
|
export { create${pascalName}ExampleGenerator } from './example-generator.js';
|
|
3448
4077
|
`;
|
|
3449
|
-
|
|
3450
|
-
|
|
4078
|
+
writeFileSync12(
|
|
4079
|
+
join20(generatorsDir, "index.ts"),
|
|
3451
4080
|
indexContent
|
|
3452
4081
|
);
|
|
3453
4082
|
}
|
|
@@ -3936,15 +4565,15 @@ Contributions are welcome! Please follow the development workflow above.
|
|
|
3936
4565
|
|
|
3937
4566
|
MIT
|
|
3938
4567
|
`;
|
|
3939
|
-
|
|
4568
|
+
writeFileSync12(join20(fnDir, "README.md"), content);
|
|
3940
4569
|
}
|
|
3941
4570
|
|
|
3942
4571
|
// src/commands/generate/generators/entity.ts
|
|
3943
|
-
import { join as
|
|
3944
|
-
import { writeFileSync as
|
|
4572
|
+
import { join as join21 } from "path";
|
|
4573
|
+
import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
|
|
3945
4574
|
function generateSchema(fnDir, scope, fnName) {
|
|
3946
|
-
const schemaFilePath =
|
|
3947
|
-
if (
|
|
4575
|
+
const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
|
|
4576
|
+
if (existsSync23(schemaFilePath)) {
|
|
3948
4577
|
return;
|
|
3949
4578
|
}
|
|
3950
4579
|
const packageName = `${scope}/${fnName}`;
|
|
@@ -3955,7 +4584,7 @@ function generateSchema(fnDir, scope, fnName) {
|
|
|
3955
4584
|
PACKAGE_NAME: packageName,
|
|
3956
4585
|
SCHEMA_VAR_NAME: schemaVarName
|
|
3957
4586
|
});
|
|
3958
|
-
|
|
4587
|
+
writeFileSync13(schemaFilePath, content);
|
|
3959
4588
|
}
|
|
3960
4589
|
function generateEntity(fnDir, scope, fnName, entityName) {
|
|
3961
4590
|
const safeScope = toSafeSchemaName(scope);
|
|
@@ -3974,8 +4603,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
|
|
|
3974
4603
|
SCHEMA_VAR_NAME: schemaVarName,
|
|
3975
4604
|
SCHEMA_FILE_NAME: schemaFileName
|
|
3976
4605
|
});
|
|
3977
|
-
|
|
3978
|
-
|
|
4606
|
+
writeFileSync13(
|
|
4607
|
+
join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
|
|
3979
4608
|
content
|
|
3980
4609
|
);
|
|
3981
4610
|
}
|
|
@@ -3983,12 +4612,12 @@ function generateEntitiesIndex(fnDir, entities) {
|
|
|
3983
4612
|
const schemaExport = `export * from './schema';`;
|
|
3984
4613
|
const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
|
|
3985
4614
|
const content = [schemaExport, entityExports].filter(Boolean).join("\n");
|
|
3986
|
-
|
|
4615
|
+
writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
|
|
3987
4616
|
}
|
|
3988
4617
|
|
|
3989
4618
|
// src/commands/generate/generators/repository.ts
|
|
3990
|
-
import { join as
|
|
3991
|
-
import { writeFileSync as
|
|
4619
|
+
import { join as join22 } from "path";
|
|
4620
|
+
import { writeFileSync as writeFileSync14 } from "fs";
|
|
3992
4621
|
function generateRepository(fnDir, entityName) {
|
|
3993
4622
|
const pascalName = toPascalCase(entityName);
|
|
3994
4623
|
const repoName = `${entityName}Repository`;
|
|
@@ -3997,19 +4626,19 @@ function generateRepository(fnDir, entityName) {
|
|
|
3997
4626
|
ENTITY_NAME: entityName,
|
|
3998
4627
|
REPO_NAME: repoName
|
|
3999
4628
|
});
|
|
4000
|
-
|
|
4001
|
-
|
|
4629
|
+
writeFileSync14(
|
|
4630
|
+
join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
|
|
4002
4631
|
content
|
|
4003
4632
|
);
|
|
4004
4633
|
}
|
|
4005
4634
|
function generateRepositoriesIndex(fnDir, entities) {
|
|
4006
4635
|
const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
|
|
4007
|
-
|
|
4636
|
+
writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
|
|
4008
4637
|
}
|
|
4009
4638
|
|
|
4010
4639
|
// src/commands/generate/generators/route.ts
|
|
4011
|
-
import { join as
|
|
4012
|
-
import { mkdirSync as mkdirSync4, writeFileSync as
|
|
4640
|
+
import { join as join23 } from "path";
|
|
4641
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
4013
4642
|
function generateRoute(fnDir, entityName) {
|
|
4014
4643
|
const pascalName = toPascalCase(entityName);
|
|
4015
4644
|
const repoName = `${entityName}Repository`;
|
|
@@ -4020,29 +4649,29 @@ function generateRoute(fnDir, entityName) {
|
|
|
4020
4649
|
REPO_NAME: repoName,
|
|
4021
4650
|
KEBAB_NAME: kebabName
|
|
4022
4651
|
});
|
|
4023
|
-
const routeDir =
|
|
4652
|
+
const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
|
|
4024
4653
|
mkdirSync4(routeDir, { recursive: true });
|
|
4025
|
-
|
|
4654
|
+
writeFileSync15(join23(routeDir, "index.ts"), content);
|
|
4026
4655
|
}
|
|
4027
4656
|
|
|
4028
4657
|
// src/commands/generate/generators/contract.ts
|
|
4029
|
-
import { join as
|
|
4030
|
-
import { writeFileSync as
|
|
4658
|
+
import { join as join24 } from "path";
|
|
4659
|
+
import { writeFileSync as writeFileSync16 } from "fs";
|
|
4031
4660
|
function generateContract(fnDir, entityName) {
|
|
4032
4661
|
const pascalName = toPascalCase(entityName);
|
|
4033
4662
|
const content = loadTemplate("contract", {
|
|
4034
4663
|
PASCAL_NAME: pascalName,
|
|
4035
4664
|
ENTITY_NAME: entityName
|
|
4036
4665
|
});
|
|
4037
|
-
|
|
4038
|
-
|
|
4666
|
+
writeFileSync16(
|
|
4667
|
+
join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
|
|
4039
4668
|
content
|
|
4040
4669
|
);
|
|
4041
4670
|
}
|
|
4042
4671
|
|
|
4043
4672
|
// src/commands/generate/generators/index-files.ts
|
|
4044
|
-
import { join as
|
|
4045
|
-
import { writeFileSync as
|
|
4673
|
+
import { join as join25 } from "path";
|
|
4674
|
+
import { writeFileSync as writeFileSync17 } from "fs";
|
|
4046
4675
|
function generateMainIndex(fnDir, fnName) {
|
|
4047
4676
|
const content = `/**
|
|
4048
4677
|
* @spfn/${fnName}
|
|
@@ -4068,7 +4697,7 @@ export * from '@/lib/types/index';
|
|
|
4068
4697
|
|
|
4069
4698
|
export * from '@/server/entities/index';
|
|
4070
4699
|
`;
|
|
4071
|
-
|
|
4700
|
+
writeFileSync17(join25(fnDir, "src/index.ts"), content);
|
|
4072
4701
|
}
|
|
4073
4702
|
function generateServerIndex(fnDir) {
|
|
4074
4703
|
const content = `/**
|
|
@@ -4103,7 +4732,7 @@ export * from '@/server/repositories/index';
|
|
|
4103
4732
|
|
|
4104
4733
|
// TODO: Export helpers here
|
|
4105
4734
|
`;
|
|
4106
|
-
|
|
4735
|
+
writeFileSync17(join25(fnDir, "src/server.ts"), content);
|
|
4107
4736
|
}
|
|
4108
4737
|
function generateClientIndex(fnDir) {
|
|
4109
4738
|
const content = `/**
|
|
@@ -4136,7 +4765,7 @@ export * from './client/store';
|
|
|
4136
4765
|
|
|
4137
4766
|
export * from './client/components';
|
|
4138
4767
|
`;
|
|
4139
|
-
|
|
4768
|
+
writeFileSync17(join25(fnDir, "src/client.ts"), content);
|
|
4140
4769
|
}
|
|
4141
4770
|
function generateTypesFile(fnDir, fnName) {
|
|
4142
4771
|
const content = `/**
|
|
@@ -4148,7 +4777,7 @@ function generateTypesFile(fnDir, fnName) {
|
|
|
4148
4777
|
|
|
4149
4778
|
export * from '@/lib/types/index';
|
|
4150
4779
|
`;
|
|
4151
|
-
|
|
4780
|
+
writeFileSync17(join25(fnDir, "src/types.ts"), content);
|
|
4152
4781
|
}
|
|
4153
4782
|
|
|
4154
4783
|
// src/commands/generate/generators/structure.ts
|
|
@@ -4170,7 +4799,7 @@ async function generateFunctionStructure(options) {
|
|
|
4170
4799
|
"src/client/store",
|
|
4171
4800
|
"src/client/components"
|
|
4172
4801
|
];
|
|
4173
|
-
dirs.forEach((dir) => mkdirSync5(
|
|
4802
|
+
dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
|
|
4174
4803
|
generatePackageJson(fnDir, scope, fnName, description);
|
|
4175
4804
|
generateTsConfig(fnDir);
|
|
4176
4805
|
generateTsupConfig(fnDir);
|
|
@@ -4190,15 +4819,15 @@ async function generateFunctionStructure(options) {
|
|
|
4190
4819
|
generateEntitiesIndex(fnDir, entities);
|
|
4191
4820
|
generateRepositoriesIndex(fnDir, entities);
|
|
4192
4821
|
} else {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4822
|
+
writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
|
|
4823
|
+
writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
|
|
4824
|
+
}
|
|
4825
|
+
writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
|
|
4826
|
+
writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
|
|
4827
|
+
writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
|
|
4828
|
+
writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
|
|
4829
|
+
writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
|
|
4830
|
+
writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
|
|
4202
4831
|
generateMainIndex(fnDir, fnName);
|
|
4203
4832
|
generateServerIndex(fnDir);
|
|
4204
4833
|
generateClientIndex(fnDir);
|
|
@@ -4222,8 +4851,8 @@ async function generateFunction(name, options) {
|
|
|
4222
4851
|
logger.error("Function name is required");
|
|
4223
4852
|
process.exit(1);
|
|
4224
4853
|
}
|
|
4225
|
-
const fnDir =
|
|
4226
|
-
if (
|
|
4854
|
+
const fnDir = join27(cwd, fnName);
|
|
4855
|
+
if (existsSync24(fnDir)) {
|
|
4227
4856
|
logger.error(`Directory ${fnName} already exists at ${fnDir}`);
|
|
4228
4857
|
process.exit(1);
|
|
4229
4858
|
}
|
|
@@ -4268,13 +4897,13 @@ async function generateFunction(name, options) {
|
|
|
4268
4897
|
});
|
|
4269
4898
|
spinner.succeed("Function structure generated");
|
|
4270
4899
|
console.log("");
|
|
4271
|
-
logger.success(`\u2728 Package ${
|
|
4900
|
+
logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
|
|
4272
4901
|
`);
|
|
4273
|
-
logger.info(
|
|
4274
|
-
console.log(` ${
|
|
4275
|
-
console.log(` ${
|
|
4276
|
-
console.log(` ${
|
|
4277
|
-
console.log(` ${
|
|
4902
|
+
logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
|
|
4903
|
+
console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
|
|
4904
|
+
console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
|
|
4905
|
+
console.log(` ${chalk27.gray("3.")} pnpm build`);
|
|
4906
|
+
console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
|
|
4278
4907
|
console.log("");
|
|
4279
4908
|
} catch (error) {
|
|
4280
4909
|
spinner.fail("Failed to generate function");
|
|
@@ -4287,14 +4916,30 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
|
|
|
4287
4916
|
|
|
4288
4917
|
// src/commands/env.ts
|
|
4289
4918
|
import { Command as Command12 } from "commander";
|
|
4290
|
-
import
|
|
4291
|
-
import { existsSync as
|
|
4919
|
+
import chalk28 from "chalk";
|
|
4920
|
+
import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
|
|
4292
4921
|
import { resolve } from "path";
|
|
4293
4922
|
import { parse } from "dotenv";
|
|
4294
|
-
var
|
|
4923
|
+
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
4924
|
+
var BASE_ENV_FILES = {
|
|
4295
4925
|
nextjs: [".env", ".env.local"],
|
|
4296
4926
|
server: [".env.server", ".env.server.local"]
|
|
4297
4927
|
};
|
|
4928
|
+
function getEnvFilesForEnvironment(nodeEnv) {
|
|
4929
|
+
const files = [".env"];
|
|
4930
|
+
if (nodeEnv) {
|
|
4931
|
+
files.push(`.env.${nodeEnv}`);
|
|
4932
|
+
}
|
|
4933
|
+
if (nodeEnv !== "test") {
|
|
4934
|
+
files.push(".env.local");
|
|
4935
|
+
}
|
|
4936
|
+
if (nodeEnv) {
|
|
4937
|
+
files.push(`.env.${nodeEnv}.local`);
|
|
4938
|
+
}
|
|
4939
|
+
files.push(".env.server");
|
|
4940
|
+
files.push(".env.server.local");
|
|
4941
|
+
return files;
|
|
4942
|
+
}
|
|
4298
4943
|
function getTargetFile(schema) {
|
|
4299
4944
|
const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
|
|
4300
4945
|
if (isNextjs) {
|
|
@@ -4320,27 +4965,26 @@ async function loadEnvSchema(packageName) {
|
|
|
4320
4965
|
}
|
|
4321
4966
|
function formatType(type) {
|
|
4322
4967
|
const typeColors = {
|
|
4323
|
-
string:
|
|
4324
|
-
number:
|
|
4325
|
-
boolean:
|
|
4326
|
-
url:
|
|
4327
|
-
enum:
|
|
4328
|
-
json:
|
|
4968
|
+
string: chalk28.green,
|
|
4969
|
+
number: chalk28.blue,
|
|
4970
|
+
boolean: chalk28.yellow,
|
|
4971
|
+
url: chalk28.cyan,
|
|
4972
|
+
enum: chalk28.magenta,
|
|
4973
|
+
json: chalk28.red
|
|
4329
4974
|
};
|
|
4330
|
-
|
|
4331
|
-
return colorFn(type);
|
|
4975
|
+
return (typeColors[type] || chalk28.white)(type);
|
|
4332
4976
|
}
|
|
4333
4977
|
function formatDefault(value, type) {
|
|
4334
4978
|
if (value === void 0) {
|
|
4335
|
-
return
|
|
4979
|
+
return chalk28.dim("(none)");
|
|
4336
4980
|
}
|
|
4337
4981
|
if (type === "string" || type === "url") {
|
|
4338
|
-
return
|
|
4982
|
+
return chalk28.green(`"${value}"`);
|
|
4339
4983
|
}
|
|
4340
4984
|
if (type === "boolean") {
|
|
4341
|
-
return value ?
|
|
4985
|
+
return value ? chalk28.green("true") : chalk28.red("false");
|
|
4342
4986
|
}
|
|
4343
|
-
return
|
|
4987
|
+
return chalk28.cyan(String(value));
|
|
4344
4988
|
}
|
|
4345
4989
|
async function listEnvVars(options) {
|
|
4346
4990
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4354,28 +4998,28 @@ async function listEnvVars(options) {
|
|
|
4354
4998
|
acc[target].push([key, schema]);
|
|
4355
4999
|
return acc;
|
|
4356
5000
|
}, {});
|
|
4357
|
-
console.log(
|
|
5001
|
+
console.log(chalk28.blue.bold(`
|
|
4358
5002
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4359
5003
|
`));
|
|
4360
5004
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4361
|
-
console.log(
|
|
5005
|
+
console.log(chalk28.bold.magenta(`
|
|
4362
5006
|
${file}`));
|
|
4363
|
-
console.log(
|
|
5007
|
+
console.log(chalk28.dim("\u2500".repeat(50)));
|
|
4364
5008
|
for (const [key, schema] of vars) {
|
|
4365
5009
|
printEnvVar(key, schema);
|
|
4366
5010
|
}
|
|
4367
5011
|
}
|
|
4368
5012
|
} else {
|
|
4369
|
-
console.log(
|
|
5013
|
+
console.log(chalk28.blue.bold(`
|
|
4370
5014
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4371
5015
|
`));
|
|
4372
5016
|
for (const [key, schema] of allVars) {
|
|
4373
5017
|
printEnvVar(key, schema, true);
|
|
4374
5018
|
}
|
|
4375
5019
|
}
|
|
4376
|
-
console.log(
|
|
5020
|
+
console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4377
5021
|
} catch (error) {
|
|
4378
|
-
console.error(
|
|
5022
|
+
console.error(chalk28.red(`
|
|
4379
5023
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4380
5024
|
`));
|
|
4381
5025
|
process.exit(1);
|
|
@@ -4383,17 +5027,17 @@ ${file}`));
|
|
|
4383
5027
|
}
|
|
4384
5028
|
function printEnvVar(key, schema, showFile = false) {
|
|
4385
5029
|
const typeStr = formatType(schema.type);
|
|
4386
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4387
|
-
const sensitiveStr = schema.sensitive ?
|
|
4388
|
-
const fileStr = showFile ?
|
|
4389
|
-
console.log(`${
|
|
4390
|
-
console.log(` ${
|
|
5030
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
|
|
5031
|
+
const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
|
|
5032
|
+
const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
5033
|
+
console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
5034
|
+
console.log(` ${chalk28.dim(schema.description)}`);
|
|
4391
5035
|
if (schema.default !== void 0) {
|
|
4392
|
-
console.log(` ${
|
|
5036
|
+
console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4393
5037
|
}
|
|
4394
5038
|
if (schema.examples && schema.examples.length > 0) {
|
|
4395
5039
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4396
|
-
console.log(` ${
|
|
5040
|
+
console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
|
|
4397
5041
|
}
|
|
4398
5042
|
console.log();
|
|
4399
5043
|
}
|
|
@@ -4401,7 +5045,7 @@ async function showEnvStats(options) {
|
|
|
4401
5045
|
const packageName = options.package || "@spfn/core";
|
|
4402
5046
|
try {
|
|
4403
5047
|
const envSchema = await loadEnvSchema(packageName);
|
|
4404
|
-
console.log(
|
|
5048
|
+
console.log(chalk28.blue.bold(`
|
|
4405
5049
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4406
5050
|
`));
|
|
4407
5051
|
const allVars = Object.entries(envSchema);
|
|
@@ -4423,24 +5067,24 @@ async function showEnvStats(options) {
|
|
|
4423
5067
|
acc[file] = (acc[file] || 0) + 1;
|
|
4424
5068
|
return acc;
|
|
4425
5069
|
}, {});
|
|
4426
|
-
console.log(`${
|
|
4427
|
-
console.log(`${
|
|
4428
|
-
console.log(`${
|
|
4429
|
-
console.log(`${
|
|
4430
|
-
console.log(
|
|
4431
|
-
console.log(` ${
|
|
4432
|
-
console.log(` ${
|
|
4433
|
-
console.log(
|
|
5070
|
+
console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
|
|
5071
|
+
console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
|
|
5072
|
+
console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
|
|
5073
|
+
console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
|
|
5074
|
+
console.log(chalk28.bold("\nBy Target:"));
|
|
5075
|
+
console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
|
|
5076
|
+
console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
|
|
5077
|
+
console.log(chalk28.bold("\nBy File:"));
|
|
4434
5078
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4435
|
-
console.log(` ${
|
|
5079
|
+
console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
|
|
4436
5080
|
}
|
|
4437
|
-
console.log(
|
|
5081
|
+
console.log(chalk28.bold("\nBy Type:"));
|
|
4438
5082
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4439
|
-
console.log(` ${formatType(type)}: ${
|
|
5083
|
+
console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
|
|
4440
5084
|
}
|
|
4441
5085
|
console.log();
|
|
4442
5086
|
} catch (error) {
|
|
4443
|
-
console.error(
|
|
5087
|
+
console.error(chalk28.red(`
|
|
4444
5088
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4445
5089
|
`));
|
|
4446
5090
|
process.exit(1);
|
|
@@ -4460,26 +5104,26 @@ async function searchEnvVars(query, options) {
|
|
|
4460
5104
|
}
|
|
4461
5105
|
}
|
|
4462
5106
|
if (results.length === 0) {
|
|
4463
|
-
console.log(
|
|
5107
|
+
console.log(chalk28.yellow(`
|
|
4464
5108
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4465
5109
|
`));
|
|
4466
5110
|
return;
|
|
4467
5111
|
}
|
|
4468
|
-
console.log(
|
|
5112
|
+
console.log(chalk28.blue.bold(`
|
|
4469
5113
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4470
5114
|
`));
|
|
4471
5115
|
for (const [key, schema] of results) {
|
|
4472
5116
|
const typeStr = formatType(schema.type);
|
|
4473
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4474
|
-
console.log(`${
|
|
4475
|
-
console.log(` ${
|
|
5117
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
|
|
5118
|
+
console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
|
|
5119
|
+
console.log(` ${chalk28.dim(schema.description)}`);
|
|
4476
5120
|
if (schema.default !== void 0) {
|
|
4477
|
-
console.log(` ${
|
|
5121
|
+
console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4478
5122
|
}
|
|
4479
5123
|
console.log();
|
|
4480
5124
|
}
|
|
4481
5125
|
} catch (error) {
|
|
4482
|
-
console.error(
|
|
5126
|
+
console.error(chalk28.red(`
|
|
4483
5127
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4484
5128
|
`));
|
|
4485
5129
|
process.exit(1);
|
|
@@ -4489,8 +5133,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
|
|
|
4489
5133
|
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-g, --group", "Group variables by target file").action(listEnvVars);
|
|
4490
5134
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4491
5135
|
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);
|
|
5136
|
+
function validateEnvOption(envValue) {
|
|
5137
|
+
if (!VALID_ENVS.includes(envValue)) {
|
|
5138
|
+
console.error(chalk28.red(`
|
|
5139
|
+
\u274C Invalid environment: "${envValue}"`));
|
|
5140
|
+
console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
5141
|
+
`));
|
|
5142
|
+
process.exit(1);
|
|
5143
|
+
}
|
|
5144
|
+
return envValue;
|
|
5145
|
+
}
|
|
4492
5146
|
async function initEnvFiles(options) {
|
|
4493
5147
|
const packageName = options.package || "@spfn/core";
|
|
5148
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4494
5149
|
const cwd = process.cwd();
|
|
4495
5150
|
try {
|
|
4496
5151
|
const envSchema = await loadEnvSchema(packageName);
|
|
@@ -4502,31 +5157,57 @@ async function initEnvFiles(options) {
|
|
|
4502
5157
|
acc[exampleFile].push([key, schema]);
|
|
4503
5158
|
return acc;
|
|
4504
5159
|
}, {});
|
|
4505
|
-
|
|
5160
|
+
if (targetEnv) {
|
|
5161
|
+
console.log(chalk28.blue.bold(`
|
|
5162
|
+
\u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
|
|
5163
|
+
`));
|
|
5164
|
+
const envSpecificFiles = {};
|
|
5165
|
+
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
5166
|
+
if (committedVars.length > 0) {
|
|
5167
|
+
envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
|
|
5168
|
+
}
|
|
5169
|
+
const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
|
|
5170
|
+
if (sensitiveVars.length > 0) {
|
|
5171
|
+
envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
|
|
5172
|
+
}
|
|
5173
|
+
const allGrouped = { ...grouped, ...envSpecificFiles };
|
|
5174
|
+
for (const [file, vars] of Object.entries(allGrouped)) {
|
|
5175
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5176
|
+
}
|
|
5177
|
+
} else {
|
|
5178
|
+
console.log(chalk28.blue.bold(`
|
|
4506
5179
|
\u{1F680} Generating .env template files
|
|
4507
5180
|
`));
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
if (existsSync23(filePath) && !options.force) {
|
|
4511
|
-
console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4512
|
-
continue;
|
|
5181
|
+
for (const [file, vars] of Object.entries(grouped)) {
|
|
5182
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4513
5183
|
}
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
console.log(
|
|
4519
|
-
console.log(
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
5184
|
+
}
|
|
5185
|
+
console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
5186
|
+
console.log(chalk28.dim(" cp .env.example .env"));
|
|
5187
|
+
console.log(chalk28.dim(" cp .env.local.example .env.local"));
|
|
5188
|
+
console.log(chalk28.dim(" cp .env.server.example .env.server"));
|
|
5189
|
+
console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
|
|
5190
|
+
if (targetEnv) {
|
|
5191
|
+
console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
5192
|
+
console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
5193
|
+
}
|
|
5194
|
+
console.log("");
|
|
4523
5195
|
} catch (error) {
|
|
4524
|
-
console.error(
|
|
5196
|
+
console.error(chalk28.red(`
|
|
4525
5197
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4526
5198
|
`));
|
|
4527
5199
|
process.exit(1);
|
|
4528
5200
|
}
|
|
4529
5201
|
}
|
|
5202
|
+
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5203
|
+
const filePath = resolve(cwd, file);
|
|
5204
|
+
if (existsSync25(filePath) && !force) {
|
|
5205
|
+
console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
|
|
5209
|
+
console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5210
|
+
}
|
|
4530
5211
|
function generateEnvFileContent(vars) {
|
|
4531
5212
|
const lines = [
|
|
4532
5213
|
"# Auto-generated by spfn env init",
|
|
@@ -4554,28 +5235,30 @@ function generateEnvFileContent(vars) {
|
|
|
4554
5235
|
}
|
|
4555
5236
|
async function checkEnvFiles(options) {
|
|
4556
5237
|
const packageName = options.package || "@spfn/core";
|
|
5238
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4557
5239
|
const cwd = process.cwd();
|
|
4558
5240
|
try {
|
|
4559
5241
|
const envSchema = await loadEnvSchema(packageName);
|
|
4560
5242
|
const allVars = Object.entries(envSchema);
|
|
4561
|
-
|
|
4562
|
-
|
|
5243
|
+
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5244
|
+
console.log(chalk28.blue.bold(`
|
|
5245
|
+
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4563
5246
|
`));
|
|
4564
|
-
const
|
|
5247
|
+
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
4565
5248
|
const loadedEnv = {};
|
|
4566
5249
|
const issues = [];
|
|
4567
5250
|
const warnings = [];
|
|
4568
|
-
for (const file of
|
|
5251
|
+
for (const file of filesToCheck) {
|
|
4569
5252
|
const filePath = resolve(cwd, file);
|
|
4570
|
-
if (!
|
|
5253
|
+
if (!existsSync25(filePath)) {
|
|
4571
5254
|
continue;
|
|
4572
5255
|
}
|
|
4573
|
-
const content =
|
|
5256
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4574
5257
|
const parsed = parse(content);
|
|
4575
5258
|
for (const [key, value] of Object.entries(parsed)) {
|
|
4576
5259
|
loadedEnv[key] = { value: value || "", file };
|
|
4577
5260
|
}
|
|
4578
|
-
console.log(
|
|
5261
|
+
console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
|
|
4579
5262
|
}
|
|
4580
5263
|
console.log("");
|
|
4581
5264
|
for (const [key, schema] of allVars) {
|
|
@@ -4583,21 +5266,21 @@ async function checkEnvFiles(options) {
|
|
|
4583
5266
|
const found = loadedEnv[key];
|
|
4584
5267
|
if (!found) {
|
|
4585
5268
|
if (schema.required && schema.default === void 0) {
|
|
4586
|
-
issues.push(`${
|
|
5269
|
+
issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
|
|
4587
5270
|
}
|
|
4588
5271
|
continue;
|
|
4589
5272
|
}
|
|
4590
|
-
const isNextjsFile =
|
|
4591
|
-
const isServerFile =
|
|
5273
|
+
const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
|
|
5274
|
+
const isServerFile = BASE_ENV_FILES.server.includes(found.file);
|
|
4592
5275
|
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
4593
5276
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4594
5277
|
if (schema.sensitive) {
|
|
4595
5278
|
issues.push(
|
|
4596
|
-
`${
|
|
5279
|
+
`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is sensitive and should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.yellow(found.file)} (security risk!)`
|
|
4597
5280
|
);
|
|
4598
5281
|
} else {
|
|
4599
5282
|
warnings.push(
|
|
4600
|
-
`${
|
|
5283
|
+
`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
|
|
4601
5284
|
);
|
|
4602
5285
|
}
|
|
4603
5286
|
}
|
|
@@ -4605,51 +5288,64 @@ async function checkEnvFiles(options) {
|
|
|
4605
5288
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4606
5289
|
const inSchema = allVars.some(([k]) => k === key);
|
|
4607
5290
|
if (!inSchema) {
|
|
4608
|
-
warnings.push(`${
|
|
5291
|
+
warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
|
|
4609
5292
|
}
|
|
4610
5293
|
}
|
|
4611
5294
|
if (issues.length > 0) {
|
|
4612
|
-
console.log(
|
|
5295
|
+
console.log(chalk28.red.bold("Issues:"));
|
|
4613
5296
|
for (const issue of issues) {
|
|
4614
5297
|
console.log(` ${issue}`);
|
|
4615
5298
|
}
|
|
4616
5299
|
console.log("");
|
|
4617
5300
|
}
|
|
4618
5301
|
if (warnings.length > 0) {
|
|
4619
|
-
console.log(
|
|
5302
|
+
console.log(chalk28.yellow.bold("Warnings:"));
|
|
4620
5303
|
for (const warning of warnings) {
|
|
4621
5304
|
console.log(` ${warning}`);
|
|
4622
5305
|
}
|
|
4623
5306
|
console.log("");
|
|
4624
5307
|
}
|
|
4625
5308
|
if (issues.length === 0 && warnings.length === 0) {
|
|
4626
|
-
console.log(
|
|
5309
|
+
console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4627
5310
|
} else {
|
|
4628
|
-
console.log(
|
|
5311
|
+
console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4629
5312
|
`));
|
|
4630
5313
|
if (issues.length > 0) {
|
|
4631
5314
|
process.exit(1);
|
|
4632
5315
|
}
|
|
4633
5316
|
}
|
|
4634
5317
|
} catch (error) {
|
|
4635
|
-
console.error(
|
|
5318
|
+
console.error(chalk28.red(`
|
|
4636
5319
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4637
5320
|
`));
|
|
4638
5321
|
process.exit(1);
|
|
4639
5322
|
}
|
|
4640
5323
|
}
|
|
4641
|
-
envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
|
|
4642
|
-
envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(checkEnvFiles);
|
|
5324
|
+
envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Generate environment-specific templates (e.g. production, staging)").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
|
|
5325
|
+
envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Check files for a specific environment (e.g. production)").action(checkEnvFiles);
|
|
4643
5326
|
async function validateEnvVars(options) {
|
|
4644
5327
|
const packages = options.packages || ["@spfn/core"];
|
|
4645
|
-
|
|
5328
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5329
|
+
if (targetEnv) {
|
|
5330
|
+
const { loadEnv: loadEnv8 } = await import("@spfn/core/env/loader");
|
|
5331
|
+
const result = loadEnv8({ nodeEnv: targetEnv });
|
|
5332
|
+
console.log(chalk28.blue.bold(`
|
|
5333
|
+
\u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
|
|
5334
|
+
`));
|
|
5335
|
+
if (result.loadedFiles.length > 0) {
|
|
5336
|
+
console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5337
|
+
}
|
|
5338
|
+
console.log("");
|
|
5339
|
+
} else {
|
|
5340
|
+
console.log(chalk28.blue.bold(`
|
|
4646
5341
|
\u{1F50D} Validating environment variables
|
|
4647
5342
|
`));
|
|
5343
|
+
}
|
|
4648
5344
|
const allErrors = [];
|
|
4649
5345
|
const allWarnings = [];
|
|
4650
5346
|
for (const packageName of packages) {
|
|
4651
5347
|
try {
|
|
4652
|
-
console.log(
|
|
5348
|
+
console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
|
|
4653
5349
|
const envSchema = await loadEnvSchema(packageName);
|
|
4654
5350
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4655
5351
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -4662,10 +5358,10 @@ async function validateEnvVars(options) {
|
|
|
4662
5358
|
}
|
|
4663
5359
|
} catch (error) {
|
|
4664
5360
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4665
|
-
console.log(
|
|
5361
|
+
console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4666
5362
|
continue;
|
|
4667
5363
|
}
|
|
4668
|
-
console.error(
|
|
5364
|
+
console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4669
5365
|
if (options.strict) {
|
|
4670
5366
|
process.exit(1);
|
|
4671
5367
|
}
|
|
@@ -4673,42 +5369,43 @@ async function validateEnvVars(options) {
|
|
|
4673
5369
|
}
|
|
4674
5370
|
console.log("");
|
|
4675
5371
|
if (allErrors.length > 0) {
|
|
4676
|
-
console.log(
|
|
5372
|
+
console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4677
5373
|
`));
|
|
4678
5374
|
for (const error of allErrors) {
|
|
4679
|
-
console.log(` ${
|
|
4680
|
-
console.log(` ${
|
|
4681
|
-
console.log(` ${
|
|
5375
|
+
console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
|
|
5376
|
+
console.log(` ${chalk28.dim(error.message)}`);
|
|
5377
|
+
console.log(` ${chalk28.dim(`from ${error.package}`)}`);
|
|
4682
5378
|
console.log("");
|
|
4683
5379
|
}
|
|
4684
5380
|
}
|
|
4685
5381
|
if (allWarnings.length > 0) {
|
|
4686
|
-
console.log(
|
|
5382
|
+
console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4687
5383
|
`));
|
|
4688
5384
|
for (const warning of allWarnings) {
|
|
4689
|
-
console.log(` ${
|
|
4690
|
-
console.log(` ${
|
|
5385
|
+
console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
|
|
5386
|
+
console.log(` ${chalk28.dim(warning.message)}`);
|
|
4691
5387
|
console.log("");
|
|
4692
5388
|
}
|
|
4693
5389
|
}
|
|
4694
5390
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4695
|
-
console.log(
|
|
5391
|
+
console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4696
5392
|
} else if (allErrors.length === 0) {
|
|
4697
|
-
console.log(
|
|
4698
|
-
console.log(
|
|
5393
|
+
console.log(chalk28.green("\u2705 No errors found."));
|
|
5394
|
+
console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4699
5395
|
`));
|
|
4700
5396
|
} else {
|
|
4701
|
-
console.log(
|
|
5397
|
+
console.log(chalk28.red(`
|
|
4702
5398
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4703
5399
|
`));
|
|
4704
5400
|
process.exit(1);
|
|
4705
5401
|
}
|
|
4706
5402
|
}
|
|
4707
|
-
envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
|
|
5403
|
+
envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
|
|
4708
5404
|
|
|
4709
5405
|
// src/index.ts
|
|
5406
|
+
init_version();
|
|
4710
5407
|
var program = new Command13();
|
|
4711
|
-
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(
|
|
5408
|
+
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
4712
5409
|
program.addCommand(createCommand);
|
|
4713
5410
|
program.addCommand(initCommand);
|
|
4714
5411
|
program.addCommand(addCommand);
|