spfn 0.2.0-beta.4 → 0.2.0-beta.40
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 +1157 -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.40";
|
|
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,238 @@ 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
|
-
process.exit(1);
|
|
2437
|
+
const cfg = getTableConfig(value);
|
|
2438
|
+
if (cfg.schema) {
|
|
2439
|
+
detectedSchemas.add(cfg.schema);
|
|
2440
|
+
}
|
|
2441
|
+
} catch {
|
|
2104
2442
|
}
|
|
2105
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 statements = 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
|
+
if (statements.length === 0) {
|
|
2465
|
+
console.log(chalk13.green("\u2705 No changes detected \u2014 database is up to date\n"));
|
|
2466
|
+
await applyFunctionMigrations();
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const result = classifyStatements(statements);
|
|
2470
|
+
if (options.dryRun) {
|
|
2471
|
+
displayDryRunSummary(result);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
displayClassifiedStatements(result);
|
|
2475
|
+
if (options.force) {
|
|
2476
|
+
console.log(chalk13.dim("\n--force: applying all changes..."));
|
|
2477
|
+
for (const stmt of statements) {
|
|
2478
|
+
await db.execute(sql.raw(stmt));
|
|
2479
|
+
}
|
|
2480
|
+
displayApplySummary(statements.length, 0);
|
|
2481
|
+
} else if (result.destructive.length === 0) {
|
|
2482
|
+
for (const stmt of statements) {
|
|
2483
|
+
await db.execute(sql.raw(stmt));
|
|
2484
|
+
}
|
|
2485
|
+
displayApplySummary(statements.length, 0);
|
|
2486
|
+
} else {
|
|
2487
|
+
const safeCount = result.safe.length + result.warning.length;
|
|
2488
|
+
if (safeCount > 0) {
|
|
2489
|
+
for (const stmt of [...result.safe, ...result.warning]) {
|
|
2490
|
+
await db.execute(sql.raw(stmt.sql));
|
|
2491
|
+
}
|
|
2492
|
+
console.log(chalk13.green(`
|
|
2493
|
+
\u2705 Applied ${safeCount} safe statement(s)`));
|
|
2494
|
+
}
|
|
2495
|
+
console.log(chalk13.red(`
|
|
2496
|
+
\u274C ${result.destructive.length} destructive change(s) require confirmation:`));
|
|
2497
|
+
for (const stmt of result.destructive) {
|
|
2498
|
+
console.log(chalk13.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
2499
|
+
console.log(chalk13.dim(` \u2192 ${stmt.reason}`));
|
|
2500
|
+
}
|
|
2501
|
+
const { confirm } = await prompts3({
|
|
2502
|
+
type: "confirm",
|
|
2503
|
+
name: "confirm",
|
|
2504
|
+
message: "Apply destructive changes?",
|
|
2505
|
+
initial: false
|
|
2506
|
+
});
|
|
2507
|
+
if (confirm) {
|
|
2508
|
+
for (const stmt of result.destructive) {
|
|
2509
|
+
await db.execute(sql.raw(stmt.sql));
|
|
2510
|
+
}
|
|
2511
|
+
displayApplySummary(statements.length, 0);
|
|
2512
|
+
} else {
|
|
2513
|
+
displayApplySummary(safeCount, result.destructive.length);
|
|
2514
|
+
console.log(chalk13.dim("Tip: Use --force to apply all changes without prompting.\n"));
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
await applyFunctionMigrations();
|
|
2518
|
+
} finally {
|
|
2519
|
+
await close();
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
async function applyFunctionMigrations() {
|
|
2523
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2524
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2525
|
+
if (functions.length === 0) {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
console.log(chalk13.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
2529
|
+
functions.forEach((func) => {
|
|
2530
|
+
console.log(chalk13.dim(` - ${func.packageName}`));
|
|
2531
|
+
});
|
|
2532
|
+
try {
|
|
2533
|
+
await executeFunctionMigrations2(functions);
|
|
2534
|
+
console.log(chalk13.green("\n\u2705 All function migrations applied\n"));
|
|
2535
|
+
} catch (error) {
|
|
2536
|
+
console.error(chalk13.red("\n\u274C Failed to apply function migrations"));
|
|
2537
|
+
console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2538
|
+
process.exit(1);
|
|
2539
|
+
}
|
|
2106
2540
|
}
|
|
2107
2541
|
|
|
2108
2542
|
// src/commands/db/migrate.ts
|
|
2109
|
-
import
|
|
2543
|
+
import chalk17 from "chalk";
|
|
2544
|
+
import { join as join16 } from "path";
|
|
2545
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2110
2546
|
|
|
2111
2547
|
// src/commands/db/backup.ts
|
|
2112
2548
|
import { promises as fs3 } from "fs";
|
|
2113
2549
|
import path3 from "path";
|
|
2114
2550
|
import { spawn as spawn2 } from "child_process";
|
|
2115
|
-
import
|
|
2551
|
+
import chalk16 from "chalk";
|
|
2116
2552
|
import ora7 from "ora";
|
|
2117
2553
|
|
|
2118
2554
|
// src/commands/db/utils/database.ts
|
|
@@ -2181,14 +2617,14 @@ function formatTimestamp() {
|
|
|
2181
2617
|
import { promises as fs2 } from "fs";
|
|
2182
2618
|
import { existsSync as existsSync17 } from "fs";
|
|
2183
2619
|
import path2 from "path";
|
|
2184
|
-
import
|
|
2620
|
+
import chalk15 from "chalk";
|
|
2185
2621
|
|
|
2186
2622
|
// src/commands/db/utils/metadata.ts
|
|
2187
2623
|
import { promises as fs } from "fs";
|
|
2188
2624
|
import path from "path";
|
|
2189
2625
|
import { promisify } from "util";
|
|
2190
2626
|
import { exec } from "child_process";
|
|
2191
|
-
import
|
|
2627
|
+
import chalk14 from "chalk";
|
|
2192
2628
|
var execAsync = promisify(exec);
|
|
2193
2629
|
async function collectGitInfo() {
|
|
2194
2630
|
try {
|
|
@@ -2251,7 +2687,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
2251
2687
|
await pool.end();
|
|
2252
2688
|
}
|
|
2253
2689
|
} catch (error) {
|
|
2254
|
-
console.log(
|
|
2690
|
+
console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
2255
2691
|
return void 0;
|
|
2256
2692
|
}
|
|
2257
2693
|
}
|
|
@@ -2259,9 +2695,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
2259
2695
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
2260
2696
|
try {
|
|
2261
2697
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
2262
|
-
console.log(
|
|
2698
|
+
console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
2263
2699
|
} catch (error) {
|
|
2264
|
-
console.log(
|
|
2700
|
+
console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
|
|
2265
2701
|
}
|
|
2266
2702
|
}
|
|
2267
2703
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -2290,10 +2726,10 @@ async function ensureBackupInGitignore() {
|
|
|
2290
2726
|
if (!hasBackupsIgnore) {
|
|
2291
2727
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
2292
2728
|
await fs2.appendFile(gitignorePath, entry);
|
|
2293
|
-
console.log(
|
|
2729
|
+
console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
|
|
2294
2730
|
}
|
|
2295
2731
|
} catch (error) {
|
|
2296
|
-
console.log(
|
|
2732
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
2297
2733
|
}
|
|
2298
2734
|
}
|
|
2299
2735
|
async function ensureBackupDir() {
|
|
@@ -2341,12 +2777,14 @@ async function listBackupFiles() {
|
|
|
2341
2777
|
|
|
2342
2778
|
// src/commands/db/backup.ts
|
|
2343
2779
|
import { env as env3 } from "@spfn/core/config";
|
|
2780
|
+
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
2344
2781
|
async function dbBackup(options) {
|
|
2345
|
-
console.log(
|
|
2782
|
+
console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2783
|
+
loadEnv4();
|
|
2346
2784
|
const dbUrl = env3.DATABASE_URL;
|
|
2347
2785
|
if (!dbUrl) {
|
|
2348
|
-
console.error(
|
|
2349
|
-
console.log(
|
|
2786
|
+
console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
|
|
2787
|
+
console.log(chalk16.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2350
2788
|
process.exit(1);
|
|
2351
2789
|
}
|
|
2352
2790
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2356,7 +2794,7 @@ async function dbBackup(options) {
|
|
|
2356
2794
|
const ext = format === "sql" ? "sql" : "dump";
|
|
2357
2795
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
2358
2796
|
if (options.dataOnly && options.schemaOnly) {
|
|
2359
|
-
console.error(
|
|
2797
|
+
console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2360
2798
|
process.exit(1);
|
|
2361
2799
|
}
|
|
2362
2800
|
const args = [
|
|
@@ -2402,11 +2840,11 @@ async function dbBackup(options) {
|
|
|
2402
2840
|
const stats = await fs3.stat(filename);
|
|
2403
2841
|
const size = formatBytes(stats.size);
|
|
2404
2842
|
spinner.succeed("Backup created");
|
|
2405
|
-
console.log(
|
|
2843
|
+
console.log(chalk16.green(`
|
|
2406
2844
|
\u2705 Backup created successfully`));
|
|
2407
|
-
console.log(
|
|
2408
|
-
console.log(
|
|
2409
|
-
console.log(
|
|
2845
|
+
console.log(chalk16.gray(` File: ${filename}`));
|
|
2846
|
+
console.log(chalk16.gray(` Size: ${size}`));
|
|
2847
|
+
console.log(chalk16.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
2410
2848
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
2411
2849
|
collectGitInfo(),
|
|
2412
2850
|
collectMigrationInfo(dbUrl)
|
|
@@ -2446,18 +2884,20 @@ async function dbBackup(options) {
|
|
|
2446
2884
|
reject(error);
|
|
2447
2885
|
});
|
|
2448
2886
|
}).catch((error) => {
|
|
2449
|
-
console.error(
|
|
2887
|
+
console.error(chalk16.red("\n\u274C Failed to create backup"));
|
|
2450
2888
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
2451
|
-
console.error(
|
|
2889
|
+
console.error(chalk16.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
2452
2890
|
} else {
|
|
2453
|
-
console.error(
|
|
2891
|
+
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2454
2892
|
}
|
|
2455
2893
|
process.exit(1);
|
|
2456
2894
|
});
|
|
2457
2895
|
}
|
|
2458
2896
|
|
|
2459
2897
|
// src/commands/db/migrate.ts
|
|
2460
|
-
import "@spfn/core/config";
|
|
2898
|
+
import { env as env4 } from "@spfn/core/config";
|
|
2899
|
+
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
2900
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
2461
2901
|
async function dbMigrate(options = {}) {
|
|
2462
2902
|
try {
|
|
2463
2903
|
validateDatabasePrerequisites();
|
|
@@ -2465,7 +2905,7 @@ async function dbMigrate(options = {}) {
|
|
|
2465
2905
|
process.exit(1);
|
|
2466
2906
|
}
|
|
2467
2907
|
if (options.withBackup) {
|
|
2468
|
-
console.log(
|
|
2908
|
+
console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
2469
2909
|
await dbBackup({
|
|
2470
2910
|
format: "custom",
|
|
2471
2911
|
tag: "pre-migration",
|
|
@@ -2473,59 +2913,72 @@ async function dbMigrate(options = {}) {
|
|
|
2473
2913
|
});
|
|
2474
2914
|
console.log("");
|
|
2475
2915
|
}
|
|
2916
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
2917
|
+
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
2918
|
+
const postgres = await import("postgres");
|
|
2919
|
+
loadEnv5();
|
|
2920
|
+
if (!env4.DATABASE_URL) {
|
|
2921
|
+
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
}
|
|
2476
2924
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2477
2925
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2478
2926
|
if (functions.length > 0) {
|
|
2479
|
-
console.log(
|
|
2927
|
+
console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
|
|
2480
2928
|
functions.forEach((func) => {
|
|
2481
|
-
console.log(
|
|
2929
|
+
console.log(chalk17.dim(` - ${func.packageName}`));
|
|
2482
2930
|
});
|
|
2931
|
+
await executeFunctionMigrations2(functions);
|
|
2932
|
+
console.log(chalk17.green("\u2705 Function migrations applied\n"));
|
|
2933
|
+
}
|
|
2934
|
+
const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
|
|
2935
|
+
if (existsSync18(projectMigrationsDir)) {
|
|
2936
|
+
const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2937
|
+
const projDb = drizzle(projConn);
|
|
2483
2938
|
try {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2939
|
+
console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
|
|
2940
|
+
await migrate(projDb, {
|
|
2941
|
+
migrationsFolder: projectMigrationsDir,
|
|
2942
|
+
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
2943
|
+
});
|
|
2944
|
+
console.log(chalk17.green("\u2705 Project migrations applied successfully"));
|
|
2945
|
+
} finally {
|
|
2946
|
+
await projConn.end();
|
|
2490
2947
|
}
|
|
2948
|
+
} else {
|
|
2949
|
+
console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
|
|
2491
2950
|
}
|
|
2492
|
-
await runWithSpinner(
|
|
2493
|
-
"Running project migrations...",
|
|
2494
|
-
"migrate",
|
|
2495
|
-
"Project migrations applied successfully",
|
|
2496
|
-
"Failed to run project migrations"
|
|
2497
|
-
);
|
|
2498
2951
|
}
|
|
2499
2952
|
|
|
2500
2953
|
// src/commands/db/studio.ts
|
|
2501
|
-
import
|
|
2502
|
-
import { existsSync as
|
|
2954
|
+
import chalk18 from "chalk";
|
|
2955
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
2503
2956
|
import { spawn as spawn3 } from "child_process";
|
|
2504
|
-
import { env as
|
|
2957
|
+
import { env as env5 } from "@spfn/core/config";
|
|
2505
2958
|
import "@spfn/core/config";
|
|
2506
2959
|
async function dbStudio(requestedPort) {
|
|
2507
|
-
console.log(
|
|
2960
|
+
console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
2508
2961
|
const defaultPort = 4983;
|
|
2509
2962
|
const startPort = requestedPort || defaultPort;
|
|
2510
2963
|
let port;
|
|
2511
2964
|
try {
|
|
2512
2965
|
port = await findAvailablePort(startPort);
|
|
2513
2966
|
if (port !== startPort) {
|
|
2514
|
-
console.log(
|
|
2967
|
+
console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
2515
2968
|
`));
|
|
2516
2969
|
}
|
|
2517
2970
|
} catch (error) {
|
|
2518
|
-
console.error(
|
|
2971
|
+
console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
2519
2972
|
process.exit(1);
|
|
2520
2973
|
}
|
|
2521
|
-
const hasUserConfig =
|
|
2974
|
+
const hasUserConfig = existsSync19("./drizzle.config.ts");
|
|
2522
2975
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2523
2976
|
try {
|
|
2524
2977
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2525
2978
|
if (!hasUserConfig) {
|
|
2526
|
-
if (!
|
|
2527
|
-
console.error(
|
|
2528
|
-
console.log(
|
|
2979
|
+
if (!env5.DATABASE_URL) {
|
|
2980
|
+
console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
|
|
2981
|
+
console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2529
2982
|
process.exit(1);
|
|
2530
2983
|
}
|
|
2531
2984
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -2536,33 +2989,34 @@ async function dbStudio(requestedPort) {
|
|
|
2536
2989
|
// Expand glob patterns for Studio compatibility
|
|
2537
2990
|
});
|
|
2538
2991
|
writeFileSync10(tempConfigPath, configContent);
|
|
2539
|
-
console.log(
|
|
2992
|
+
console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
|
|
2540
2993
|
}
|
|
2541
2994
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
2542
2995
|
stdio: "inherit",
|
|
2543
|
-
shell: true
|
|
2996
|
+
shell: true,
|
|
2997
|
+
env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
|
|
2544
2998
|
});
|
|
2545
2999
|
const cleanup = () => {
|
|
2546
|
-
if (!hasUserConfig &&
|
|
2547
|
-
|
|
3000
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3001
|
+
unlinkSync3(tempConfigPath);
|
|
2548
3002
|
}
|
|
2549
3003
|
};
|
|
2550
3004
|
studioProcess.on("exit", (code) => {
|
|
2551
3005
|
cleanup();
|
|
2552
3006
|
if (code !== 0 && code !== null) {
|
|
2553
|
-
console.error(
|
|
3007
|
+
console.error(chalk18.red(`
|
|
2554
3008
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
2555
3009
|
process.exit(code);
|
|
2556
3010
|
}
|
|
2557
3011
|
});
|
|
2558
3012
|
studioProcess.on("error", (error) => {
|
|
2559
3013
|
cleanup();
|
|
2560
|
-
console.error(
|
|
2561
|
-
console.error(
|
|
3014
|
+
console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
|
|
3015
|
+
console.error(chalk18.red(error.message));
|
|
2562
3016
|
process.exit(1);
|
|
2563
3017
|
});
|
|
2564
3018
|
process.on("SIGINT", () => {
|
|
2565
|
-
console.log(
|
|
3019
|
+
console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
2566
3020
|
studioProcess.kill("SIGTERM");
|
|
2567
3021
|
cleanup();
|
|
2568
3022
|
process.exit(0);
|
|
@@ -2573,28 +3027,28 @@ async function dbStudio(requestedPort) {
|
|
|
2573
3027
|
process.exit(0);
|
|
2574
3028
|
});
|
|
2575
3029
|
} catch (error) {
|
|
2576
|
-
if (!hasUserConfig &&
|
|
2577
|
-
|
|
3030
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3031
|
+
unlinkSync3(tempConfigPath);
|
|
2578
3032
|
}
|
|
2579
|
-
console.error(
|
|
2580
|
-
console.error(
|
|
3033
|
+
console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
|
|
3034
|
+
console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2581
3035
|
process.exit(1);
|
|
2582
3036
|
}
|
|
2583
3037
|
}
|
|
2584
3038
|
|
|
2585
3039
|
// src/commands/db/drop.ts
|
|
2586
|
-
import
|
|
2587
|
-
import
|
|
3040
|
+
import chalk19 from "chalk";
|
|
3041
|
+
import prompts4 from "prompts";
|
|
2588
3042
|
async function dbDrop() {
|
|
2589
|
-
console.log(
|
|
2590
|
-
const { confirm } = await
|
|
3043
|
+
console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3044
|
+
const { confirm } = await prompts4({
|
|
2591
3045
|
type: "confirm",
|
|
2592
3046
|
name: "confirm",
|
|
2593
3047
|
message: "Are you sure you want to drop all tables?",
|
|
2594
3048
|
initial: false
|
|
2595
3049
|
});
|
|
2596
3050
|
if (!confirm) {
|
|
2597
|
-
console.log(
|
|
3051
|
+
console.log(chalk19.gray("Cancelled."));
|
|
2598
3052
|
process.exit(0);
|
|
2599
3053
|
}
|
|
2600
3054
|
await runWithSpinner(
|
|
@@ -2606,7 +3060,7 @@ async function dbDrop() {
|
|
|
2606
3060
|
}
|
|
2607
3061
|
|
|
2608
3062
|
// src/commands/db/check.ts
|
|
2609
|
-
import
|
|
3063
|
+
import chalk20 from "chalk";
|
|
2610
3064
|
import ora8 from "ora";
|
|
2611
3065
|
async function dbCheck() {
|
|
2612
3066
|
const spinner = ora8("Checking database connection...").start();
|
|
@@ -2615,7 +3069,7 @@ async function dbCheck() {
|
|
|
2615
3069
|
spinner.succeed("Database connection OK");
|
|
2616
3070
|
} catch (error) {
|
|
2617
3071
|
spinner.fail("Database connection failed");
|
|
2618
|
-
console.error(
|
|
3072
|
+
console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2619
3073
|
process.exit(1);
|
|
2620
3074
|
}
|
|
2621
3075
|
}
|
|
@@ -2623,26 +3077,28 @@ async function dbCheck() {
|
|
|
2623
3077
|
// src/commands/db/restore.ts
|
|
2624
3078
|
import path4 from "path";
|
|
2625
3079
|
import { spawn as spawn4 } from "child_process";
|
|
2626
|
-
import
|
|
3080
|
+
import chalk21 from "chalk";
|
|
2627
3081
|
import ora9 from "ora";
|
|
2628
|
-
import
|
|
2629
|
-
import { env as
|
|
3082
|
+
import prompts5 from "prompts";
|
|
3083
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3084
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
2630
3085
|
async function dbRestore(backupFile, options = {}) {
|
|
2631
|
-
console.log(
|
|
2632
|
-
|
|
3086
|
+
console.log(chalk21.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
3087
|
+
loadEnv6();
|
|
3088
|
+
const dbUrl = env6.DATABASE_URL;
|
|
2633
3089
|
if (!dbUrl) {
|
|
2634
|
-
console.error(
|
|
2635
|
-
console.log(
|
|
3090
|
+
console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
|
|
3091
|
+
console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2636
3092
|
process.exit(1);
|
|
2637
3093
|
}
|
|
2638
3094
|
let file = backupFile;
|
|
2639
3095
|
if (!file) {
|
|
2640
3096
|
const backups = await listBackupFiles();
|
|
2641
3097
|
if (backups.length === 0) {
|
|
2642
|
-
console.log(
|
|
3098
|
+
console.log(chalk21.yellow("No backups found in ./backups directory"));
|
|
2643
3099
|
process.exit(0);
|
|
2644
3100
|
}
|
|
2645
|
-
const { selected } = await
|
|
3101
|
+
const { selected } = await prompts5({
|
|
2646
3102
|
type: "select",
|
|
2647
3103
|
name: "selected",
|
|
2648
3104
|
message: "Select backup to restore:",
|
|
@@ -2652,70 +3108,70 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2652
3108
|
}))
|
|
2653
3109
|
});
|
|
2654
3110
|
if (!selected) {
|
|
2655
|
-
console.log(
|
|
3111
|
+
console.log(chalk21.gray("Cancelled"));
|
|
2656
3112
|
process.exit(0);
|
|
2657
3113
|
}
|
|
2658
3114
|
file = selected;
|
|
2659
3115
|
}
|
|
2660
3116
|
if (!file) {
|
|
2661
|
-
console.error(
|
|
3117
|
+
console.error(chalk21.red("\u274C No backup file selected"));
|
|
2662
3118
|
process.exit(1);
|
|
2663
3119
|
}
|
|
2664
3120
|
const metadata = await loadBackupMetadata(file);
|
|
2665
3121
|
if (metadata) {
|
|
2666
|
-
console.log(
|
|
2667
|
-
console.log(
|
|
2668
|
-
console.log(
|
|
3122
|
+
console.log(chalk21.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
3123
|
+
console.log(chalk21.dim(` Database: ${metadata.database}`));
|
|
3124
|
+
console.log(chalk21.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
2669
3125
|
if (metadata.environment) {
|
|
2670
|
-
console.log(
|
|
3126
|
+
console.log(chalk21.dim(` Environment: ${metadata.environment}`));
|
|
2671
3127
|
}
|
|
2672
3128
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
2673
|
-
console.log(
|
|
3129
|
+
console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
2674
3130
|
}
|
|
2675
3131
|
if (metadata.backup.dataOnly) {
|
|
2676
|
-
console.log(
|
|
3132
|
+
console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
2677
3133
|
}
|
|
2678
3134
|
if (metadata.backup.schemaOnly) {
|
|
2679
|
-
console.log(
|
|
3135
|
+
console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
2680
3136
|
}
|
|
2681
|
-
const
|
|
3137
|
+
const warnings2 = [];
|
|
2682
3138
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
2683
3139
|
collectGitInfo(),
|
|
2684
3140
|
collectMigrationInfo(dbUrl)
|
|
2685
3141
|
]);
|
|
2686
3142
|
if (metadata.git && currentGitInfo) {
|
|
2687
3143
|
if (metadata.git.commit !== currentGitInfo.commit) {
|
|
2688
|
-
|
|
3144
|
+
warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
|
|
2689
3145
|
}
|
|
2690
3146
|
if (metadata.git.branch !== currentGitInfo.branch) {
|
|
2691
|
-
|
|
3147
|
+
warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
|
|
2692
3148
|
}
|
|
2693
3149
|
}
|
|
2694
3150
|
if (metadata.migrations && currentMigrationInfo) {
|
|
2695
3151
|
if (metadata.migrations.hash !== currentMigrationInfo.hash) {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
3152
|
+
warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
|
|
3153
|
+
warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
|
|
3154
|
+
warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
|
|
2699
3155
|
}
|
|
2700
3156
|
}
|
|
2701
|
-
if (
|
|
2702
|
-
console.log(
|
|
2703
|
-
|
|
3157
|
+
if (warnings2.length > 0) {
|
|
3158
|
+
console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
3159
|
+
warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
|
|
2704
3160
|
console.log("");
|
|
2705
3161
|
}
|
|
2706
3162
|
}
|
|
2707
|
-
const { confirm } = await
|
|
3163
|
+
const { confirm } = await prompts5({
|
|
2708
3164
|
type: "confirm",
|
|
2709
3165
|
name: "confirm",
|
|
2710
|
-
message:
|
|
3166
|
+
message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
|
|
2711
3167
|
initial: false
|
|
2712
3168
|
});
|
|
2713
3169
|
if (!confirm) {
|
|
2714
|
-
console.log(
|
|
3170
|
+
console.log(chalk21.gray("Cancelled"));
|
|
2715
3171
|
process.exit(0);
|
|
2716
3172
|
}
|
|
2717
3173
|
if (options.dataOnly && options.schemaOnly) {
|
|
2718
|
-
console.error(
|
|
3174
|
+
console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2719
3175
|
process.exit(1);
|
|
2720
3176
|
}
|
|
2721
3177
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2728,6 +3184,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2728
3184
|
args.push("-p", dbInfo.port);
|
|
2729
3185
|
args.push("-U", dbInfo.user);
|
|
2730
3186
|
args.push("-d", dbInfo.database);
|
|
3187
|
+
args.push("--verbose");
|
|
2731
3188
|
if (options.drop) {
|
|
2732
3189
|
args.push("--clean");
|
|
2733
3190
|
}
|
|
@@ -2743,15 +3200,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2743
3200
|
args.push(file);
|
|
2744
3201
|
} else {
|
|
2745
3202
|
if (options.dataOnly || options.schemaOnly) {
|
|
2746
|
-
console.log(
|
|
2747
|
-
console.log(
|
|
3203
|
+
console.log(chalk21.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
3204
|
+
console.log(chalk21.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
2748
3205
|
}
|
|
2749
3206
|
args.push("-h", dbInfo.host);
|
|
2750
3207
|
args.push("-p", dbInfo.port);
|
|
2751
3208
|
args.push("-U", dbInfo.user);
|
|
2752
3209
|
args.push("-d", dbInfo.database);
|
|
3210
|
+
args.push("-v", "ON_ERROR_STOP=1");
|
|
2753
3211
|
args.push("-f", file);
|
|
2754
3212
|
}
|
|
3213
|
+
const verbose = options.verbose ?? false;
|
|
2755
3214
|
const spinner = ora9("Restoring backup...").start();
|
|
2756
3215
|
const restoreProcess = spawn4(command, args, {
|
|
2757
3216
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -2760,19 +3219,81 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2760
3219
|
PGPASSWORD: dbInfo.password
|
|
2761
3220
|
}
|
|
2762
3221
|
});
|
|
2763
|
-
|
|
3222
|
+
const warnings = [];
|
|
3223
|
+
const errors = [];
|
|
3224
|
+
let objectCount = 0;
|
|
3225
|
+
let lastObject = "";
|
|
2764
3226
|
restoreProcess.stderr?.on("data", (data) => {
|
|
2765
|
-
|
|
3227
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
3228
|
+
for (const line of lines) {
|
|
3229
|
+
if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
|
|
3230
|
+
warnings.push(line.trim());
|
|
3231
|
+
} else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
|
|
3232
|
+
errors.push(line.trim());
|
|
3233
|
+
}
|
|
3234
|
+
const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
|
|
3235
|
+
if (objectMatch) {
|
|
3236
|
+
objectCount = Number(objectMatch[2]);
|
|
3237
|
+
const current = Number(objectMatch[1]);
|
|
3238
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3239
|
+
lastObject = desc;
|
|
3240
|
+
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
3241
|
+
} else if (isCustomFormat) {
|
|
3242
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3243
|
+
if (desc && !/warning:|error:/i.test(desc)) {
|
|
3244
|
+
lastObject = desc;
|
|
3245
|
+
spinner.text = `Restoring backup... ${desc}`;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
if (verbose) {
|
|
3249
|
+
spinner.stop();
|
|
3250
|
+
console.log(chalk21.dim(` ${line.trim()}`));
|
|
3251
|
+
spinner.start();
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
restoreProcess.stdout?.on("data", (data) => {
|
|
3256
|
+
if (verbose) {
|
|
3257
|
+
spinner.stop();
|
|
3258
|
+
console.log(chalk21.dim(` ${data.toString().trim()}`));
|
|
3259
|
+
spinner.start();
|
|
3260
|
+
}
|
|
2766
3261
|
});
|
|
2767
3262
|
await new Promise((resolve2, reject) => {
|
|
2768
3263
|
restoreProcess.on("close", (code) => {
|
|
2769
3264
|
if (code === 0) {
|
|
2770
|
-
|
|
2771
|
-
|
|
3265
|
+
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
3266
|
+
spinner.succeed(`Restore completed${summary}`);
|
|
3267
|
+
if (warnings.length > 0) {
|
|
3268
|
+
console.log(chalk21.yellow(`
|
|
3269
|
+
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
3270
|
+
`));
|
|
3271
|
+
for (const w of warnings) {
|
|
3272
|
+
console.log(chalk21.yellow(` - ${w}`));
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
console.log(chalk21.green("\n\u2705 Database restored successfully"));
|
|
2772
3276
|
resolve2();
|
|
2773
3277
|
} else {
|
|
2774
3278
|
spinner.fail("Restore failed");
|
|
2775
|
-
|
|
3279
|
+
if (errors.length > 0) {
|
|
3280
|
+
console.error(chalk21.red(`
|
|
3281
|
+
\u274C Errors (${errors.length}):
|
|
3282
|
+
`));
|
|
3283
|
+
for (const e of errors) {
|
|
3284
|
+
console.error(chalk21.red(` - ${e}`));
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
if (warnings.length > 0) {
|
|
3288
|
+
console.log(chalk21.yellow(`
|
|
3289
|
+
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
3290
|
+
`));
|
|
3291
|
+
for (const w of warnings) {
|
|
3292
|
+
console.log(chalk21.yellow(` - ${w}`));
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
3296
|
+
reject(new Error(fallback));
|
|
2776
3297
|
}
|
|
2777
3298
|
});
|
|
2778
3299
|
restoreProcess.on("error", (error) => {
|
|
@@ -2780,24 +3301,27 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2780
3301
|
reject(error);
|
|
2781
3302
|
});
|
|
2782
3303
|
}).catch((error) => {
|
|
2783
|
-
|
|
2784
|
-
|
|
3304
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3305
|
+
if (msg) {
|
|
3306
|
+
console.error(chalk21.red(`
|
|
3307
|
+
\u274C ${msg}`));
|
|
3308
|
+
}
|
|
2785
3309
|
process.exit(1);
|
|
2786
3310
|
});
|
|
2787
3311
|
}
|
|
2788
3312
|
|
|
2789
3313
|
// src/commands/db/list.ts
|
|
2790
|
-
import
|
|
3314
|
+
import chalk22 from "chalk";
|
|
2791
3315
|
async function dbBackupList() {
|
|
2792
|
-
console.log(
|
|
3316
|
+
console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
|
|
2793
3317
|
const backups = await listBackupFiles();
|
|
2794
3318
|
if (backups.length === 0) {
|
|
2795
|
-
console.log(
|
|
2796
|
-
console.log(
|
|
3319
|
+
console.log(chalk22.yellow("No backups found in ./backups directory"));
|
|
3320
|
+
console.log(chalk22.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
2797
3321
|
return;
|
|
2798
3322
|
}
|
|
2799
|
-
console.log(
|
|
2800
|
-
console.log(
|
|
3323
|
+
console.log(chalk22.bold(" Date Size File"));
|
|
3324
|
+
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
3325
|
backups.forEach((backup) => {
|
|
2802
3326
|
const date = backup.date.toLocaleString("en-US", {
|
|
2803
3327
|
year: "numeric",
|
|
@@ -2808,23 +3332,23 @@ async function dbBackupList() {
|
|
|
2808
3332
|
second: "2-digit"
|
|
2809
3333
|
});
|
|
2810
3334
|
const sizeStr = backup.size.padEnd(10);
|
|
2811
|
-
console.log(
|
|
3335
|
+
console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
2812
3336
|
});
|
|
2813
|
-
console.log(
|
|
3337
|
+
console.log(chalk22.gray(`
|
|
2814
3338
|
Total: ${backups.length} backup(s)
|
|
2815
3339
|
`));
|
|
2816
3340
|
}
|
|
2817
3341
|
|
|
2818
3342
|
// src/commands/db/clean.ts
|
|
2819
3343
|
import { promises as fs4 } from "fs";
|
|
2820
|
-
import
|
|
3344
|
+
import chalk23 from "chalk";
|
|
2821
3345
|
import ora10 from "ora";
|
|
2822
|
-
import
|
|
3346
|
+
import prompts6 from "prompts";
|
|
2823
3347
|
async function dbBackupClean(options) {
|
|
2824
|
-
console.log(
|
|
3348
|
+
console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
2825
3349
|
const backups = await listBackupFiles();
|
|
2826
3350
|
if (backups.length === 0) {
|
|
2827
|
-
console.log(
|
|
3351
|
+
console.log(chalk23.yellow("No backups found"));
|
|
2828
3352
|
return;
|
|
2829
3353
|
}
|
|
2830
3354
|
let toDelete = [];
|
|
@@ -2841,74 +3365,176 @@ async function dbBackupClean(options) {
|
|
|
2841
3365
|
toDelete = backups.slice(defaultKeep);
|
|
2842
3366
|
}
|
|
2843
3367
|
if (toDelete.length === 0) {
|
|
2844
|
-
console.log(
|
|
3368
|
+
console.log(chalk23.green("\u2705 No backups to clean"));
|
|
2845
3369
|
return;
|
|
2846
3370
|
}
|
|
2847
|
-
console.log(
|
|
3371
|
+
console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
2848
3372
|
`));
|
|
2849
3373
|
toDelete.forEach((backup) => {
|
|
2850
|
-
console.log(
|
|
3374
|
+
console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
|
|
2851
3375
|
});
|
|
2852
|
-
const { confirm } = await
|
|
3376
|
+
const { confirm } = await prompts6({
|
|
2853
3377
|
type: "confirm",
|
|
2854
3378
|
name: "confirm",
|
|
2855
3379
|
message: "\nProceed with deletion?",
|
|
2856
3380
|
initial: false
|
|
2857
3381
|
});
|
|
2858
3382
|
if (!confirm) {
|
|
2859
|
-
console.log(
|
|
3383
|
+
console.log(chalk23.gray("Cancelled"));
|
|
2860
3384
|
return;
|
|
2861
3385
|
}
|
|
2862
3386
|
const spinner = ora10("Deleting backups...").start();
|
|
2863
3387
|
try {
|
|
2864
3388
|
await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
|
|
2865
3389
|
spinner.succeed("Backups deleted");
|
|
2866
|
-
console.log(
|
|
3390
|
+
console.log(chalk23.green(`
|
|
2867
3391
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
2868
3392
|
} catch (error) {
|
|
2869
3393
|
spinner.fail("Failed to delete backups");
|
|
2870
|
-
console.error(
|
|
3394
|
+
console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2871
3395
|
process.exit(1);
|
|
2872
3396
|
}
|
|
2873
3397
|
}
|
|
2874
3398
|
|
|
3399
|
+
// src/commands/db/reindex.ts
|
|
3400
|
+
import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
|
|
3401
|
+
import { join as join17 } from "path";
|
|
3402
|
+
import chalk24 from "chalk";
|
|
3403
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3404
|
+
function isTimestampPrefix(tag) {
|
|
3405
|
+
const prefix = tag.split("_")[0];
|
|
3406
|
+
return /^\d{5,}$/.test(prefix);
|
|
3407
|
+
}
|
|
3408
|
+
function parseTag(tag) {
|
|
3409
|
+
const underscoreIdx = tag.indexOf("_");
|
|
3410
|
+
if (underscoreIdx === -1) {
|
|
3411
|
+
return { prefix: tag, suffix: "" };
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
prefix: tag.substring(0, underscoreIdx),
|
|
3415
|
+
suffix: tag.substring(underscoreIdx + 1)
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
async function dbReindex(options = {}) {
|
|
3419
|
+
loadEnv7();
|
|
3420
|
+
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3421
|
+
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
3422
|
+
const outDir = config.out;
|
|
3423
|
+
const journalPath = join17(outDir, "meta", "_journal.json");
|
|
3424
|
+
if (!existsSync20(journalPath)) {
|
|
3425
|
+
console.error(chalk24.red("\u274C No _journal.json found at:"), journalPath);
|
|
3426
|
+
console.log(chalk24.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
3427
|
+
process.exit(1);
|
|
3428
|
+
}
|
|
3429
|
+
const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
|
|
3430
|
+
if (journal.entries.length === 0) {
|
|
3431
|
+
console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
const renames = [];
|
|
3435
|
+
const tagUpdates = [];
|
|
3436
|
+
let skipped = 0;
|
|
3437
|
+
for (const entry of journal.entries) {
|
|
3438
|
+
if (isTimestampPrefix(entry.tag)) {
|
|
3439
|
+
skipped++;
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
3443
|
+
const newPrefix = String(entry.when);
|
|
3444
|
+
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
3445
|
+
const oldSql = join17(outDir, `${entry.tag}.sql`);
|
|
3446
|
+
const newSql = join17(outDir, `${newTag}.sql`);
|
|
3447
|
+
if (existsSync20(oldSql)) {
|
|
3448
|
+
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
3449
|
+
}
|
|
3450
|
+
const oldSnapshot = join17(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
3451
|
+
const newSnapshot = join17(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
3452
|
+
if (existsSync20(oldSnapshot)) {
|
|
3453
|
+
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
3454
|
+
}
|
|
3455
|
+
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
3456
|
+
}
|
|
3457
|
+
if (tagUpdates.length === 0) {
|
|
3458
|
+
console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
3459
|
+
if (skipped > 0) {
|
|
3460
|
+
console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
3461
|
+
}
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3464
|
+
console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
3465
|
+
for (const update of tagUpdates) {
|
|
3466
|
+
console.log(
|
|
3467
|
+
chalk24.dim(` [${update.idx}]`),
|
|
3468
|
+
chalk24.red(update.oldTag),
|
|
3469
|
+
chalk24.dim("\u2192"),
|
|
3470
|
+
chalk24.green(update.newTag)
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
console.log(chalk24.dim(`
|
|
3474
|
+
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
3475
|
+
if (skipped > 0) {
|
|
3476
|
+
console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
3477
|
+
}
|
|
3478
|
+
if (options.dryRun) {
|
|
3479
|
+
console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
const backupPath = journalPath + ".bak";
|
|
3483
|
+
copyFileSync(journalPath, backupPath);
|
|
3484
|
+
console.log(chalk24.dim(`
|
|
3485
|
+
Backed up journal \u2192 ${backupPath}`));
|
|
3486
|
+
for (const rename of renames) {
|
|
3487
|
+
renameSync(rename.from, rename.to);
|
|
3488
|
+
}
|
|
3489
|
+
for (const update of tagUpdates) {
|
|
3490
|
+
const entry = journal.entries.find((e) => e.idx === update.idx);
|
|
3491
|
+
if (entry) {
|
|
3492
|
+
entry.tag = update.newTag;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
3496
|
+
console.log(chalk24.green(`
|
|
3497
|
+
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
3498
|
+
}
|
|
3499
|
+
|
|
2875
3500
|
// src/commands/db/index.ts
|
|
2876
3501
|
var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
|
|
2877
3502
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
2878
|
-
dbCommand.command("push").description("Push schema changes
|
|
3503
|
+
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
3504
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
2880
3505
|
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
3506
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
2882
3507
|
dbCommand.command("check").description("Check database connection").action(dbCheck);
|
|
2883
3508
|
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));
|
|
3509
|
+
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
3510
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
2886
3511
|
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));
|
|
3512
|
+
dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
|
|
2887
3513
|
|
|
2888
3514
|
// src/commands/add.ts
|
|
2889
3515
|
import { Command as Command10 } from "commander";
|
|
2890
|
-
import { existsSync as
|
|
2891
|
-
import { join as
|
|
3516
|
+
import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
|
|
3517
|
+
import { join as join18 } from "path";
|
|
2892
3518
|
import { exec as exec2 } from "child_process";
|
|
2893
3519
|
import { promisify as promisify2 } from "util";
|
|
2894
|
-
import
|
|
3520
|
+
import chalk25 from "chalk";
|
|
2895
3521
|
import ora11 from "ora";
|
|
2896
3522
|
var execAsync2 = promisify2(exec2);
|
|
2897
3523
|
async function addPackage(packageName) {
|
|
2898
3524
|
if (!packageName.includes("/")) {
|
|
2899
|
-
console.error(
|
|
2900
|
-
console.log(
|
|
2901
|
-
console.log(
|
|
2902
|
-
console.log(
|
|
3525
|
+
console.error(chalk25.red("\u274C Please specify full package name"));
|
|
3526
|
+
console.log(chalk25.yellow("\n\u{1F4A1} Examples:"));
|
|
3527
|
+
console.log(chalk25.gray(" pnpm spfn add @spfn/cms"));
|
|
3528
|
+
console.log(chalk25.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
2903
3529
|
process.exit(1);
|
|
2904
3530
|
}
|
|
2905
|
-
console.log(
|
|
3531
|
+
console.log(chalk25.blue(`
|
|
2906
3532
|
\u{1F4E6} Setting up ${packageName}...
|
|
2907
3533
|
`));
|
|
2908
3534
|
try {
|
|
2909
|
-
const pkgPath =
|
|
2910
|
-
const pkgJsonPath =
|
|
2911
|
-
if (!
|
|
3535
|
+
const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3536
|
+
const pkgJsonPath = join18(pkgPath, "package.json");
|
|
3537
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
2912
3538
|
const installSpinner = ora11("Installing package...").start();
|
|
2913
3539
|
try {
|
|
2914
3540
|
await execAsync2(`pnpm add ${packageName}`);
|
|
@@ -2918,21 +3544,21 @@ async function addPackage(packageName) {
|
|
|
2918
3544
|
throw error;
|
|
2919
3545
|
}
|
|
2920
3546
|
} else {
|
|
2921
|
-
console.log(
|
|
3547
|
+
console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
|
|
2922
3548
|
}
|
|
2923
|
-
if (!
|
|
3549
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
2924
3550
|
throw new Error(`Package ${packageName} not found after installation`);
|
|
2925
3551
|
}
|
|
2926
|
-
const pkgJson = JSON.parse(
|
|
3552
|
+
const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
|
|
2927
3553
|
if (pkgJson.spfn?.migrations) {
|
|
2928
|
-
console.log(
|
|
3554
|
+
console.log(chalk25.blue(`
|
|
2929
3555
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
2930
3556
|
`));
|
|
2931
|
-
const { env:
|
|
2932
|
-
if (!
|
|
2933
|
-
console.log(
|
|
2934
|
-
console.log(
|
|
2935
|
-
console.log(
|
|
3557
|
+
const { env: env7 } = await import("@spfn/core/config");
|
|
3558
|
+
if (!env7.DATABASE_URL) {
|
|
3559
|
+
console.log(chalk25.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
3560
|
+
console.log(chalk25.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
3561
|
+
console.log(chalk25.gray(` pnpm spfn db push
|
|
2936
3562
|
`));
|
|
2937
3563
|
} else {
|
|
2938
3564
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
@@ -2948,25 +3574,25 @@ async function addPackage(packageName) {
|
|
|
2948
3574
|
throw error;
|
|
2949
3575
|
}
|
|
2950
3576
|
} else {
|
|
2951
|
-
console.log(
|
|
3577
|
+
console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
|
|
2952
3578
|
}
|
|
2953
3579
|
}
|
|
2954
3580
|
} else {
|
|
2955
|
-
console.log(
|
|
3581
|
+
console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
2956
3582
|
}
|
|
2957
|
-
console.log(
|
|
3583
|
+
console.log(chalk25.green(`
|
|
2958
3584
|
\u2705 ${packageName} installed successfully!
|
|
2959
3585
|
`));
|
|
2960
3586
|
if (pkgJson.spfn?.setupMessage) {
|
|
2961
|
-
console.log(
|
|
3587
|
+
console.log(chalk25.cyan("\u{1F4DA} Setup Guide:"));
|
|
2962
3588
|
console.log(pkgJson.spfn.setupMessage);
|
|
2963
3589
|
console.log();
|
|
2964
3590
|
}
|
|
2965
3591
|
} catch (error) {
|
|
2966
|
-
console.error(
|
|
3592
|
+
console.error(chalk25.red(`
|
|
2967
3593
|
\u274C Failed to install ${packageName}
|
|
2968
3594
|
`));
|
|
2969
|
-
console.error(
|
|
3595
|
+
console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2970
3596
|
process.exit(1);
|
|
2971
3597
|
}
|
|
2972
3598
|
}
|
|
@@ -2976,16 +3602,16 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
|
|
|
2976
3602
|
init_logger();
|
|
2977
3603
|
import { Command as Command11 } from "commander";
|
|
2978
3604
|
import ora12 from "ora";
|
|
2979
|
-
import { join as
|
|
2980
|
-
import { existsSync as
|
|
2981
|
-
import
|
|
3605
|
+
import { join as join27 } from "path";
|
|
3606
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3607
|
+
import chalk27 from "chalk";
|
|
2982
3608
|
|
|
2983
3609
|
// src/commands/generate/prompts.ts
|
|
2984
3610
|
init_logger();
|
|
2985
|
-
import
|
|
2986
|
-
import
|
|
3611
|
+
import prompts7 from "prompts";
|
|
3612
|
+
import chalk26 from "chalk";
|
|
2987
3613
|
async function promptScope() {
|
|
2988
|
-
const response = await
|
|
3614
|
+
const response = await prompts7({
|
|
2989
3615
|
type: "text",
|
|
2990
3616
|
name: "scope",
|
|
2991
3617
|
message: "NPM scope (e.g., @mycompany, @username):",
|
|
@@ -3007,7 +3633,7 @@ async function promptScope() {
|
|
|
3007
3633
|
return response.scope;
|
|
3008
3634
|
}
|
|
3009
3635
|
async function promptFunctionName() {
|
|
3010
|
-
const response = await
|
|
3636
|
+
const response = await prompts7({
|
|
3011
3637
|
type: "text",
|
|
3012
3638
|
name: "fnName",
|
|
3013
3639
|
message: "Function name:",
|
|
@@ -3028,7 +3654,7 @@ async function promptFunctionName() {
|
|
|
3028
3654
|
return response.fnName;
|
|
3029
3655
|
}
|
|
3030
3656
|
async function promptDescription(fnName) {
|
|
3031
|
-
const response = await
|
|
3657
|
+
const response = await prompts7({
|
|
3032
3658
|
type: "text",
|
|
3033
3659
|
name: "description",
|
|
3034
3660
|
message: "Function description:",
|
|
@@ -3037,7 +3663,7 @@ async function promptDescription(fnName) {
|
|
|
3037
3663
|
return response.description || "A description of what this module does";
|
|
3038
3664
|
}
|
|
3039
3665
|
async function promptEntities() {
|
|
3040
|
-
const response = await
|
|
3666
|
+
const response = await prompts7({
|
|
3041
3667
|
type: "list",
|
|
3042
3668
|
name: "entities",
|
|
3043
3669
|
message: "Entity names (comma-separated, press enter to skip):",
|
|
@@ -3049,14 +3675,14 @@ async function promptEntities() {
|
|
|
3049
3675
|
async function confirmConfiguration(config) {
|
|
3050
3676
|
const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
|
|
3051
3677
|
console.log("");
|
|
3052
|
-
logger.info(
|
|
3053
|
-
console.log(` ${
|
|
3054
|
-
console.log(` ${
|
|
3055
|
-
console.log(` ${
|
|
3056
|
-
console.log(` ${
|
|
3057
|
-
console.log(` ${
|
|
3678
|
+
logger.info(chalk26.bold("\u26A1 Function Configuration:"));
|
|
3679
|
+
console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
|
|
3680
|
+
console.log(` ${chalk26.gray("Description:")} ${description}`);
|
|
3681
|
+
console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
|
|
3682
|
+
console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3683
|
+
console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3058
3684
|
console.log("");
|
|
3059
|
-
const { confirmed } = await
|
|
3685
|
+
const { confirmed } = await prompts7({
|
|
3060
3686
|
type: "confirm",
|
|
3061
3687
|
name: "confirmed",
|
|
3062
3688
|
message: "Create function?",
|
|
@@ -3070,12 +3696,12 @@ async function confirmConfiguration(config) {
|
|
|
3070
3696
|
}
|
|
3071
3697
|
|
|
3072
3698
|
// src/commands/generate/generators/structure.ts
|
|
3073
|
-
import { join as
|
|
3074
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
3699
|
+
import { join as join26 } from "path";
|
|
3700
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
3075
3701
|
|
|
3076
3702
|
// src/commands/generate/generators/config.ts
|
|
3077
|
-
import { join as
|
|
3078
|
-
import { writeFileSync as
|
|
3703
|
+
import { join as join20 } from "path";
|
|
3704
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
|
|
3079
3705
|
|
|
3080
3706
|
// src/commands/generate/string-utils.ts
|
|
3081
3707
|
function toPascalCase(str) {
|
|
@@ -3104,30 +3730,30 @@ function toSafeSchemaName(str) {
|
|
|
3104
3730
|
}
|
|
3105
3731
|
|
|
3106
3732
|
// src/commands/generate/template-loader.ts
|
|
3107
|
-
import { readFileSync as
|
|
3108
|
-
import { join as
|
|
3733
|
+
import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
|
|
3734
|
+
import { join as join19, dirname as dirname2 } from "path";
|
|
3109
3735
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3110
3736
|
function findTemplatesPath2() {
|
|
3111
3737
|
const __filename = fileURLToPath2(import.meta.url);
|
|
3112
3738
|
const __dirname2 = dirname2(__filename);
|
|
3113
|
-
const distPath =
|
|
3114
|
-
if (
|
|
3739
|
+
const distPath = join19(__dirname2, "commands", "generate", "templates");
|
|
3740
|
+
if (existsSync22(distPath)) {
|
|
3115
3741
|
return distPath;
|
|
3116
3742
|
}
|
|
3117
|
-
const sameDirPath =
|
|
3118
|
-
if (
|
|
3743
|
+
const sameDirPath = join19(__dirname2, "templates");
|
|
3744
|
+
if (existsSync22(sameDirPath)) {
|
|
3119
3745
|
return sameDirPath;
|
|
3120
3746
|
}
|
|
3121
|
-
const srcPath =
|
|
3122
|
-
if (
|
|
3747
|
+
const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
|
|
3748
|
+
if (existsSync22(srcPath)) {
|
|
3123
3749
|
return srcPath;
|
|
3124
3750
|
}
|
|
3125
3751
|
throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
|
|
3126
3752
|
}
|
|
3127
3753
|
function loadTemplate(templateName, variables) {
|
|
3128
3754
|
const templatesDir = findTemplatesPath2();
|
|
3129
|
-
const templatePath =
|
|
3130
|
-
let content =
|
|
3755
|
+
const templatePath = join19(templatesDir, `${templateName}.template`);
|
|
3756
|
+
let content = readFileSync8(templatePath, "utf-8");
|
|
3131
3757
|
for (const [key, value] of Object.entries(variables)) {
|
|
3132
3758
|
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
3133
3759
|
content = content.replace(regex, value);
|
|
@@ -3229,8 +3855,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
|
|
|
3229
3855
|
vitest: "^4.0.6"
|
|
3230
3856
|
}
|
|
3231
3857
|
};
|
|
3232
|
-
|
|
3233
|
-
|
|
3858
|
+
writeFileSync12(
|
|
3859
|
+
join20(fnDir, "package.json"),
|
|
3234
3860
|
JSON.stringify(content, null, 4) + "\n"
|
|
3235
3861
|
);
|
|
3236
3862
|
}
|
|
@@ -3263,8 +3889,8 @@ function generateTsConfig(fnDir) {
|
|
|
3263
3889
|
include: ["src/**/*"],
|
|
3264
3890
|
exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
|
|
3265
3891
|
};
|
|
3266
|
-
|
|
3267
|
-
|
|
3892
|
+
writeFileSync12(
|
|
3893
|
+
join20(fnDir, "tsconfig.json"),
|
|
3268
3894
|
JSON.stringify(content, null, 4) + "\n"
|
|
3269
3895
|
);
|
|
3270
3896
|
}
|
|
@@ -3340,7 +3966,7 @@ export default defineConfig({
|
|
|
3340
3966
|
],
|
|
3341
3967
|
});
|
|
3342
3968
|
`;
|
|
3343
|
-
|
|
3969
|
+
writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
|
|
3344
3970
|
}
|
|
3345
3971
|
function generateDrizzleConfig(fnDir, scope, fnName) {
|
|
3346
3972
|
const schemaName = `spfn_${toSnakeCase(fnName)}`;
|
|
@@ -3360,7 +3986,7 @@ export default defineConfig({
|
|
|
3360
3986
|
schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
|
|
3361
3987
|
});
|
|
3362
3988
|
`;
|
|
3363
|
-
|
|
3989
|
+
writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
|
|
3364
3990
|
}
|
|
3365
3991
|
function generateExampleGenerator(fnDir, scope, fnName) {
|
|
3366
3992
|
const pascalName = toPascalCase(fnName);
|
|
@@ -3429,10 +4055,10 @@ export const moduleName = '${fnName}';
|
|
|
3429
4055
|
};
|
|
3430
4056
|
}
|
|
3431
4057
|
`;
|
|
3432
|
-
const generatorsDir =
|
|
4058
|
+
const generatorsDir = join20(fnDir, "src/server/generators");
|
|
3433
4059
|
mkdirSync3(generatorsDir, { recursive: true });
|
|
3434
|
-
|
|
3435
|
-
|
|
4060
|
+
writeFileSync12(
|
|
4061
|
+
join20(generatorsDir, "example-generator.ts"),
|
|
3436
4062
|
content
|
|
3437
4063
|
);
|
|
3438
4064
|
const indexContent = `/**
|
|
@@ -3446,8 +4072,8 @@ export const moduleName = '${fnName}';
|
|
|
3446
4072
|
|
|
3447
4073
|
export { create${pascalName}ExampleGenerator } from './example-generator.js';
|
|
3448
4074
|
`;
|
|
3449
|
-
|
|
3450
|
-
|
|
4075
|
+
writeFileSync12(
|
|
4076
|
+
join20(generatorsDir, "index.ts"),
|
|
3451
4077
|
indexContent
|
|
3452
4078
|
);
|
|
3453
4079
|
}
|
|
@@ -3936,15 +4562,15 @@ Contributions are welcome! Please follow the development workflow above.
|
|
|
3936
4562
|
|
|
3937
4563
|
MIT
|
|
3938
4564
|
`;
|
|
3939
|
-
|
|
4565
|
+
writeFileSync12(join20(fnDir, "README.md"), content);
|
|
3940
4566
|
}
|
|
3941
4567
|
|
|
3942
4568
|
// src/commands/generate/generators/entity.ts
|
|
3943
|
-
import { join as
|
|
3944
|
-
import { writeFileSync as
|
|
4569
|
+
import { join as join21 } from "path";
|
|
4570
|
+
import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
|
|
3945
4571
|
function generateSchema(fnDir, scope, fnName) {
|
|
3946
|
-
const schemaFilePath =
|
|
3947
|
-
if (
|
|
4572
|
+
const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
|
|
4573
|
+
if (existsSync23(schemaFilePath)) {
|
|
3948
4574
|
return;
|
|
3949
4575
|
}
|
|
3950
4576
|
const packageName = `${scope}/${fnName}`;
|
|
@@ -3955,7 +4581,7 @@ function generateSchema(fnDir, scope, fnName) {
|
|
|
3955
4581
|
PACKAGE_NAME: packageName,
|
|
3956
4582
|
SCHEMA_VAR_NAME: schemaVarName
|
|
3957
4583
|
});
|
|
3958
|
-
|
|
4584
|
+
writeFileSync13(schemaFilePath, content);
|
|
3959
4585
|
}
|
|
3960
4586
|
function generateEntity(fnDir, scope, fnName, entityName) {
|
|
3961
4587
|
const safeScope = toSafeSchemaName(scope);
|
|
@@ -3974,8 +4600,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
|
|
|
3974
4600
|
SCHEMA_VAR_NAME: schemaVarName,
|
|
3975
4601
|
SCHEMA_FILE_NAME: schemaFileName
|
|
3976
4602
|
});
|
|
3977
|
-
|
|
3978
|
-
|
|
4603
|
+
writeFileSync13(
|
|
4604
|
+
join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
|
|
3979
4605
|
content
|
|
3980
4606
|
);
|
|
3981
4607
|
}
|
|
@@ -3983,12 +4609,12 @@ function generateEntitiesIndex(fnDir, entities) {
|
|
|
3983
4609
|
const schemaExport = `export * from './schema';`;
|
|
3984
4610
|
const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
|
|
3985
4611
|
const content = [schemaExport, entityExports].filter(Boolean).join("\n");
|
|
3986
|
-
|
|
4612
|
+
writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
|
|
3987
4613
|
}
|
|
3988
4614
|
|
|
3989
4615
|
// src/commands/generate/generators/repository.ts
|
|
3990
|
-
import { join as
|
|
3991
|
-
import { writeFileSync as
|
|
4616
|
+
import { join as join22 } from "path";
|
|
4617
|
+
import { writeFileSync as writeFileSync14 } from "fs";
|
|
3992
4618
|
function generateRepository(fnDir, entityName) {
|
|
3993
4619
|
const pascalName = toPascalCase(entityName);
|
|
3994
4620
|
const repoName = `${entityName}Repository`;
|
|
@@ -3997,19 +4623,19 @@ function generateRepository(fnDir, entityName) {
|
|
|
3997
4623
|
ENTITY_NAME: entityName,
|
|
3998
4624
|
REPO_NAME: repoName
|
|
3999
4625
|
});
|
|
4000
|
-
|
|
4001
|
-
|
|
4626
|
+
writeFileSync14(
|
|
4627
|
+
join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
|
|
4002
4628
|
content
|
|
4003
4629
|
);
|
|
4004
4630
|
}
|
|
4005
4631
|
function generateRepositoriesIndex(fnDir, entities) {
|
|
4006
4632
|
const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
|
|
4007
|
-
|
|
4633
|
+
writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
|
|
4008
4634
|
}
|
|
4009
4635
|
|
|
4010
4636
|
// src/commands/generate/generators/route.ts
|
|
4011
|
-
import { join as
|
|
4012
|
-
import { mkdirSync as mkdirSync4, writeFileSync as
|
|
4637
|
+
import { join as join23 } from "path";
|
|
4638
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
4013
4639
|
function generateRoute(fnDir, entityName) {
|
|
4014
4640
|
const pascalName = toPascalCase(entityName);
|
|
4015
4641
|
const repoName = `${entityName}Repository`;
|
|
@@ -4020,29 +4646,29 @@ function generateRoute(fnDir, entityName) {
|
|
|
4020
4646
|
REPO_NAME: repoName,
|
|
4021
4647
|
KEBAB_NAME: kebabName
|
|
4022
4648
|
});
|
|
4023
|
-
const routeDir =
|
|
4649
|
+
const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
|
|
4024
4650
|
mkdirSync4(routeDir, { recursive: true });
|
|
4025
|
-
|
|
4651
|
+
writeFileSync15(join23(routeDir, "index.ts"), content);
|
|
4026
4652
|
}
|
|
4027
4653
|
|
|
4028
4654
|
// src/commands/generate/generators/contract.ts
|
|
4029
|
-
import { join as
|
|
4030
|
-
import { writeFileSync as
|
|
4655
|
+
import { join as join24 } from "path";
|
|
4656
|
+
import { writeFileSync as writeFileSync16 } from "fs";
|
|
4031
4657
|
function generateContract(fnDir, entityName) {
|
|
4032
4658
|
const pascalName = toPascalCase(entityName);
|
|
4033
4659
|
const content = loadTemplate("contract", {
|
|
4034
4660
|
PASCAL_NAME: pascalName,
|
|
4035
4661
|
ENTITY_NAME: entityName
|
|
4036
4662
|
});
|
|
4037
|
-
|
|
4038
|
-
|
|
4663
|
+
writeFileSync16(
|
|
4664
|
+
join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
|
|
4039
4665
|
content
|
|
4040
4666
|
);
|
|
4041
4667
|
}
|
|
4042
4668
|
|
|
4043
4669
|
// src/commands/generate/generators/index-files.ts
|
|
4044
|
-
import { join as
|
|
4045
|
-
import { writeFileSync as
|
|
4670
|
+
import { join as join25 } from "path";
|
|
4671
|
+
import { writeFileSync as writeFileSync17 } from "fs";
|
|
4046
4672
|
function generateMainIndex(fnDir, fnName) {
|
|
4047
4673
|
const content = `/**
|
|
4048
4674
|
* @spfn/${fnName}
|
|
@@ -4068,7 +4694,7 @@ export * from '@/lib/types/index';
|
|
|
4068
4694
|
|
|
4069
4695
|
export * from '@/server/entities/index';
|
|
4070
4696
|
`;
|
|
4071
|
-
|
|
4697
|
+
writeFileSync17(join25(fnDir, "src/index.ts"), content);
|
|
4072
4698
|
}
|
|
4073
4699
|
function generateServerIndex(fnDir) {
|
|
4074
4700
|
const content = `/**
|
|
@@ -4103,7 +4729,7 @@ export * from '@/server/repositories/index';
|
|
|
4103
4729
|
|
|
4104
4730
|
// TODO: Export helpers here
|
|
4105
4731
|
`;
|
|
4106
|
-
|
|
4732
|
+
writeFileSync17(join25(fnDir, "src/server.ts"), content);
|
|
4107
4733
|
}
|
|
4108
4734
|
function generateClientIndex(fnDir) {
|
|
4109
4735
|
const content = `/**
|
|
@@ -4136,7 +4762,7 @@ export * from './client/store';
|
|
|
4136
4762
|
|
|
4137
4763
|
export * from './client/components';
|
|
4138
4764
|
`;
|
|
4139
|
-
|
|
4765
|
+
writeFileSync17(join25(fnDir, "src/client.ts"), content);
|
|
4140
4766
|
}
|
|
4141
4767
|
function generateTypesFile(fnDir, fnName) {
|
|
4142
4768
|
const content = `/**
|
|
@@ -4148,7 +4774,7 @@ function generateTypesFile(fnDir, fnName) {
|
|
|
4148
4774
|
|
|
4149
4775
|
export * from '@/lib/types/index';
|
|
4150
4776
|
`;
|
|
4151
|
-
|
|
4777
|
+
writeFileSync17(join25(fnDir, "src/types.ts"), content);
|
|
4152
4778
|
}
|
|
4153
4779
|
|
|
4154
4780
|
// src/commands/generate/generators/structure.ts
|
|
@@ -4170,7 +4796,7 @@ async function generateFunctionStructure(options) {
|
|
|
4170
4796
|
"src/client/store",
|
|
4171
4797
|
"src/client/components"
|
|
4172
4798
|
];
|
|
4173
|
-
dirs.forEach((dir) => mkdirSync5(
|
|
4799
|
+
dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
|
|
4174
4800
|
generatePackageJson(fnDir, scope, fnName, description);
|
|
4175
4801
|
generateTsConfig(fnDir);
|
|
4176
4802
|
generateTsupConfig(fnDir);
|
|
@@ -4190,15 +4816,15 @@ async function generateFunctionStructure(options) {
|
|
|
4190
4816
|
generateEntitiesIndex(fnDir, entities);
|
|
4191
4817
|
generateRepositoriesIndex(fnDir, entities);
|
|
4192
4818
|
} else {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4819
|
+
writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
|
|
4820
|
+
writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
|
|
4821
|
+
}
|
|
4822
|
+
writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
|
|
4823
|
+
writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
|
|
4824
|
+
writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
|
|
4825
|
+
writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
|
|
4826
|
+
writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
|
|
4827
|
+
writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
|
|
4202
4828
|
generateMainIndex(fnDir, fnName);
|
|
4203
4829
|
generateServerIndex(fnDir);
|
|
4204
4830
|
generateClientIndex(fnDir);
|
|
@@ -4222,8 +4848,8 @@ async function generateFunction(name, options) {
|
|
|
4222
4848
|
logger.error("Function name is required");
|
|
4223
4849
|
process.exit(1);
|
|
4224
4850
|
}
|
|
4225
|
-
const fnDir =
|
|
4226
|
-
if (
|
|
4851
|
+
const fnDir = join27(cwd, fnName);
|
|
4852
|
+
if (existsSync24(fnDir)) {
|
|
4227
4853
|
logger.error(`Directory ${fnName} already exists at ${fnDir}`);
|
|
4228
4854
|
process.exit(1);
|
|
4229
4855
|
}
|
|
@@ -4268,13 +4894,13 @@ async function generateFunction(name, options) {
|
|
|
4268
4894
|
});
|
|
4269
4895
|
spinner.succeed("Function structure generated");
|
|
4270
4896
|
console.log("");
|
|
4271
|
-
logger.success(`\u2728 Package ${
|
|
4897
|
+
logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
|
|
4272
4898
|
`);
|
|
4273
|
-
logger.info(
|
|
4274
|
-
console.log(` ${
|
|
4275
|
-
console.log(` ${
|
|
4276
|
-
console.log(` ${
|
|
4277
|
-
console.log(` ${
|
|
4899
|
+
logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
|
|
4900
|
+
console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
|
|
4901
|
+
console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
|
|
4902
|
+
console.log(` ${chalk27.gray("3.")} pnpm build`);
|
|
4903
|
+
console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
|
|
4278
4904
|
console.log("");
|
|
4279
4905
|
} catch (error) {
|
|
4280
4906
|
spinner.fail("Failed to generate function");
|
|
@@ -4287,14 +4913,30 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
|
|
|
4287
4913
|
|
|
4288
4914
|
// src/commands/env.ts
|
|
4289
4915
|
import { Command as Command12 } from "commander";
|
|
4290
|
-
import
|
|
4291
|
-
import { existsSync as
|
|
4916
|
+
import chalk28 from "chalk";
|
|
4917
|
+
import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
|
|
4292
4918
|
import { resolve } from "path";
|
|
4293
4919
|
import { parse } from "dotenv";
|
|
4294
|
-
var
|
|
4920
|
+
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
4921
|
+
var BASE_ENV_FILES = {
|
|
4295
4922
|
nextjs: [".env", ".env.local"],
|
|
4296
4923
|
server: [".env.server", ".env.server.local"]
|
|
4297
4924
|
};
|
|
4925
|
+
function getEnvFilesForEnvironment(nodeEnv) {
|
|
4926
|
+
const files = [".env"];
|
|
4927
|
+
if (nodeEnv) {
|
|
4928
|
+
files.push(`.env.${nodeEnv}`);
|
|
4929
|
+
}
|
|
4930
|
+
if (nodeEnv !== "test") {
|
|
4931
|
+
files.push(".env.local");
|
|
4932
|
+
}
|
|
4933
|
+
if (nodeEnv) {
|
|
4934
|
+
files.push(`.env.${nodeEnv}.local`);
|
|
4935
|
+
}
|
|
4936
|
+
files.push(".env.server");
|
|
4937
|
+
files.push(".env.server.local");
|
|
4938
|
+
return files;
|
|
4939
|
+
}
|
|
4298
4940
|
function getTargetFile(schema) {
|
|
4299
4941
|
const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
|
|
4300
4942
|
if (isNextjs) {
|
|
@@ -4320,27 +4962,26 @@ async function loadEnvSchema(packageName) {
|
|
|
4320
4962
|
}
|
|
4321
4963
|
function formatType(type) {
|
|
4322
4964
|
const typeColors = {
|
|
4323
|
-
string:
|
|
4324
|
-
number:
|
|
4325
|
-
boolean:
|
|
4326
|
-
url:
|
|
4327
|
-
enum:
|
|
4328
|
-
json:
|
|
4965
|
+
string: chalk28.green,
|
|
4966
|
+
number: chalk28.blue,
|
|
4967
|
+
boolean: chalk28.yellow,
|
|
4968
|
+
url: chalk28.cyan,
|
|
4969
|
+
enum: chalk28.magenta,
|
|
4970
|
+
json: chalk28.red
|
|
4329
4971
|
};
|
|
4330
|
-
|
|
4331
|
-
return colorFn(type);
|
|
4972
|
+
return (typeColors[type] || chalk28.white)(type);
|
|
4332
4973
|
}
|
|
4333
4974
|
function formatDefault(value, type) {
|
|
4334
4975
|
if (value === void 0) {
|
|
4335
|
-
return
|
|
4976
|
+
return chalk28.dim("(none)");
|
|
4336
4977
|
}
|
|
4337
4978
|
if (type === "string" || type === "url") {
|
|
4338
|
-
return
|
|
4979
|
+
return chalk28.green(`"${value}"`);
|
|
4339
4980
|
}
|
|
4340
4981
|
if (type === "boolean") {
|
|
4341
|
-
return value ?
|
|
4982
|
+
return value ? chalk28.green("true") : chalk28.red("false");
|
|
4342
4983
|
}
|
|
4343
|
-
return
|
|
4984
|
+
return chalk28.cyan(String(value));
|
|
4344
4985
|
}
|
|
4345
4986
|
async function listEnvVars(options) {
|
|
4346
4987
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4354,28 +4995,28 @@ async function listEnvVars(options) {
|
|
|
4354
4995
|
acc[target].push([key, schema]);
|
|
4355
4996
|
return acc;
|
|
4356
4997
|
}, {});
|
|
4357
|
-
console.log(
|
|
4998
|
+
console.log(chalk28.blue.bold(`
|
|
4358
4999
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4359
5000
|
`));
|
|
4360
5001
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4361
|
-
console.log(
|
|
5002
|
+
console.log(chalk28.bold.magenta(`
|
|
4362
5003
|
${file}`));
|
|
4363
|
-
console.log(
|
|
5004
|
+
console.log(chalk28.dim("\u2500".repeat(50)));
|
|
4364
5005
|
for (const [key, schema] of vars) {
|
|
4365
5006
|
printEnvVar(key, schema);
|
|
4366
5007
|
}
|
|
4367
5008
|
}
|
|
4368
5009
|
} else {
|
|
4369
|
-
console.log(
|
|
5010
|
+
console.log(chalk28.blue.bold(`
|
|
4370
5011
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4371
5012
|
`));
|
|
4372
5013
|
for (const [key, schema] of allVars) {
|
|
4373
5014
|
printEnvVar(key, schema, true);
|
|
4374
5015
|
}
|
|
4375
5016
|
}
|
|
4376
|
-
console.log(
|
|
5017
|
+
console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4377
5018
|
} catch (error) {
|
|
4378
|
-
console.error(
|
|
5019
|
+
console.error(chalk28.red(`
|
|
4379
5020
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4380
5021
|
`));
|
|
4381
5022
|
process.exit(1);
|
|
@@ -4383,17 +5024,17 @@ ${file}`));
|
|
|
4383
5024
|
}
|
|
4384
5025
|
function printEnvVar(key, schema, showFile = false) {
|
|
4385
5026
|
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(` ${
|
|
5027
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
|
|
5028
|
+
const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
|
|
5029
|
+
const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
5030
|
+
console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
5031
|
+
console.log(` ${chalk28.dim(schema.description)}`);
|
|
4391
5032
|
if (schema.default !== void 0) {
|
|
4392
|
-
console.log(` ${
|
|
5033
|
+
console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4393
5034
|
}
|
|
4394
5035
|
if (schema.examples && schema.examples.length > 0) {
|
|
4395
5036
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4396
|
-
console.log(` ${
|
|
5037
|
+
console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
|
|
4397
5038
|
}
|
|
4398
5039
|
console.log();
|
|
4399
5040
|
}
|
|
@@ -4401,7 +5042,7 @@ async function showEnvStats(options) {
|
|
|
4401
5042
|
const packageName = options.package || "@spfn/core";
|
|
4402
5043
|
try {
|
|
4403
5044
|
const envSchema = await loadEnvSchema(packageName);
|
|
4404
|
-
console.log(
|
|
5045
|
+
console.log(chalk28.blue.bold(`
|
|
4405
5046
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4406
5047
|
`));
|
|
4407
5048
|
const allVars = Object.entries(envSchema);
|
|
@@ -4423,24 +5064,24 @@ async function showEnvStats(options) {
|
|
|
4423
5064
|
acc[file] = (acc[file] || 0) + 1;
|
|
4424
5065
|
return acc;
|
|
4425
5066
|
}, {});
|
|
4426
|
-
console.log(`${
|
|
4427
|
-
console.log(`${
|
|
4428
|
-
console.log(`${
|
|
4429
|
-
console.log(`${
|
|
4430
|
-
console.log(
|
|
4431
|
-
console.log(` ${
|
|
4432
|
-
console.log(` ${
|
|
4433
|
-
console.log(
|
|
5067
|
+
console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
|
|
5068
|
+
console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
|
|
5069
|
+
console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
|
|
5070
|
+
console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
|
|
5071
|
+
console.log(chalk28.bold("\nBy Target:"));
|
|
5072
|
+
console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
|
|
5073
|
+
console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
|
|
5074
|
+
console.log(chalk28.bold("\nBy File:"));
|
|
4434
5075
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4435
|
-
console.log(` ${
|
|
5076
|
+
console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
|
|
4436
5077
|
}
|
|
4437
|
-
console.log(
|
|
5078
|
+
console.log(chalk28.bold("\nBy Type:"));
|
|
4438
5079
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4439
|
-
console.log(` ${formatType(type)}: ${
|
|
5080
|
+
console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
|
|
4440
5081
|
}
|
|
4441
5082
|
console.log();
|
|
4442
5083
|
} catch (error) {
|
|
4443
|
-
console.error(
|
|
5084
|
+
console.error(chalk28.red(`
|
|
4444
5085
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4445
5086
|
`));
|
|
4446
5087
|
process.exit(1);
|
|
@@ -4460,26 +5101,26 @@ async function searchEnvVars(query, options) {
|
|
|
4460
5101
|
}
|
|
4461
5102
|
}
|
|
4462
5103
|
if (results.length === 0) {
|
|
4463
|
-
console.log(
|
|
5104
|
+
console.log(chalk28.yellow(`
|
|
4464
5105
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4465
5106
|
`));
|
|
4466
5107
|
return;
|
|
4467
5108
|
}
|
|
4468
|
-
console.log(
|
|
5109
|
+
console.log(chalk28.blue.bold(`
|
|
4469
5110
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4470
5111
|
`));
|
|
4471
5112
|
for (const [key, schema] of results) {
|
|
4472
5113
|
const typeStr = formatType(schema.type);
|
|
4473
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4474
|
-
console.log(`${
|
|
4475
|
-
console.log(` ${
|
|
5114
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
|
|
5115
|
+
console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
|
|
5116
|
+
console.log(` ${chalk28.dim(schema.description)}`);
|
|
4476
5117
|
if (schema.default !== void 0) {
|
|
4477
|
-
console.log(` ${
|
|
5118
|
+
console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4478
5119
|
}
|
|
4479
5120
|
console.log();
|
|
4480
5121
|
}
|
|
4481
5122
|
} catch (error) {
|
|
4482
|
-
console.error(
|
|
5123
|
+
console.error(chalk28.red(`
|
|
4483
5124
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4484
5125
|
`));
|
|
4485
5126
|
process.exit(1);
|
|
@@ -4489,8 +5130,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
|
|
|
4489
5130
|
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
5131
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4491
5132
|
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);
|
|
5133
|
+
function validateEnvOption(envValue) {
|
|
5134
|
+
if (!VALID_ENVS.includes(envValue)) {
|
|
5135
|
+
console.error(chalk28.red(`
|
|
5136
|
+
\u274C Invalid environment: "${envValue}"`));
|
|
5137
|
+
console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
5138
|
+
`));
|
|
5139
|
+
process.exit(1);
|
|
5140
|
+
}
|
|
5141
|
+
return envValue;
|
|
5142
|
+
}
|
|
4492
5143
|
async function initEnvFiles(options) {
|
|
4493
5144
|
const packageName = options.package || "@spfn/core";
|
|
5145
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4494
5146
|
const cwd = process.cwd();
|
|
4495
5147
|
try {
|
|
4496
5148
|
const envSchema = await loadEnvSchema(packageName);
|
|
@@ -4502,31 +5154,57 @@ async function initEnvFiles(options) {
|
|
|
4502
5154
|
acc[exampleFile].push([key, schema]);
|
|
4503
5155
|
return acc;
|
|
4504
5156
|
}, {});
|
|
4505
|
-
|
|
5157
|
+
if (targetEnv) {
|
|
5158
|
+
console.log(chalk28.blue.bold(`
|
|
5159
|
+
\u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
|
|
5160
|
+
`));
|
|
5161
|
+
const envSpecificFiles = {};
|
|
5162
|
+
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
5163
|
+
if (committedVars.length > 0) {
|
|
5164
|
+
envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
|
|
5165
|
+
}
|
|
5166
|
+
const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
|
|
5167
|
+
if (sensitiveVars.length > 0) {
|
|
5168
|
+
envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
|
|
5169
|
+
}
|
|
5170
|
+
const allGrouped = { ...grouped, ...envSpecificFiles };
|
|
5171
|
+
for (const [file, vars] of Object.entries(allGrouped)) {
|
|
5172
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5173
|
+
}
|
|
5174
|
+
} else {
|
|
5175
|
+
console.log(chalk28.blue.bold(`
|
|
4506
5176
|
\u{1F680} Generating .env template files
|
|
4507
5177
|
`));
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
if (existsSync23(filePath) && !options.force) {
|
|
4511
|
-
console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4512
|
-
continue;
|
|
5178
|
+
for (const [file, vars] of Object.entries(grouped)) {
|
|
5179
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4513
5180
|
}
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
console.log(
|
|
4519
|
-
console.log(
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
5181
|
+
}
|
|
5182
|
+
console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
5183
|
+
console.log(chalk28.dim(" cp .env.example .env"));
|
|
5184
|
+
console.log(chalk28.dim(" cp .env.local.example .env.local"));
|
|
5185
|
+
console.log(chalk28.dim(" cp .env.server.example .env.server"));
|
|
5186
|
+
console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
|
|
5187
|
+
if (targetEnv) {
|
|
5188
|
+
console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
5189
|
+
console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
5190
|
+
}
|
|
5191
|
+
console.log("");
|
|
4523
5192
|
} catch (error) {
|
|
4524
|
-
console.error(
|
|
5193
|
+
console.error(chalk28.red(`
|
|
4525
5194
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4526
5195
|
`));
|
|
4527
5196
|
process.exit(1);
|
|
4528
5197
|
}
|
|
4529
5198
|
}
|
|
5199
|
+
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5200
|
+
const filePath = resolve(cwd, file);
|
|
5201
|
+
if (existsSync25(filePath) && !force) {
|
|
5202
|
+
console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
|
|
5206
|
+
console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5207
|
+
}
|
|
4530
5208
|
function generateEnvFileContent(vars) {
|
|
4531
5209
|
const lines = [
|
|
4532
5210
|
"# Auto-generated by spfn env init",
|
|
@@ -4554,28 +5232,30 @@ function generateEnvFileContent(vars) {
|
|
|
4554
5232
|
}
|
|
4555
5233
|
async function checkEnvFiles(options) {
|
|
4556
5234
|
const packageName = options.package || "@spfn/core";
|
|
5235
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4557
5236
|
const cwd = process.cwd();
|
|
4558
5237
|
try {
|
|
4559
5238
|
const envSchema = await loadEnvSchema(packageName);
|
|
4560
5239
|
const allVars = Object.entries(envSchema);
|
|
4561
|
-
|
|
4562
|
-
|
|
5240
|
+
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5241
|
+
console.log(chalk28.blue.bold(`
|
|
5242
|
+
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4563
5243
|
`));
|
|
4564
|
-
const
|
|
5244
|
+
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
4565
5245
|
const loadedEnv = {};
|
|
4566
5246
|
const issues = [];
|
|
4567
5247
|
const warnings = [];
|
|
4568
|
-
for (const file of
|
|
5248
|
+
for (const file of filesToCheck) {
|
|
4569
5249
|
const filePath = resolve(cwd, file);
|
|
4570
|
-
if (!
|
|
5250
|
+
if (!existsSync25(filePath)) {
|
|
4571
5251
|
continue;
|
|
4572
5252
|
}
|
|
4573
|
-
const content =
|
|
5253
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4574
5254
|
const parsed = parse(content);
|
|
4575
5255
|
for (const [key, value] of Object.entries(parsed)) {
|
|
4576
5256
|
loadedEnv[key] = { value: value || "", file };
|
|
4577
5257
|
}
|
|
4578
|
-
console.log(
|
|
5258
|
+
console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
|
|
4579
5259
|
}
|
|
4580
5260
|
console.log("");
|
|
4581
5261
|
for (const [key, schema] of allVars) {
|
|
@@ -4583,21 +5263,21 @@ async function checkEnvFiles(options) {
|
|
|
4583
5263
|
const found = loadedEnv[key];
|
|
4584
5264
|
if (!found) {
|
|
4585
5265
|
if (schema.required && schema.default === void 0) {
|
|
4586
|
-
issues.push(`${
|
|
5266
|
+
issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
|
|
4587
5267
|
}
|
|
4588
5268
|
continue;
|
|
4589
5269
|
}
|
|
4590
|
-
const isNextjsFile =
|
|
4591
|
-
const isServerFile =
|
|
5270
|
+
const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
|
|
5271
|
+
const isServerFile = BASE_ENV_FILES.server.includes(found.file);
|
|
4592
5272
|
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
4593
5273
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4594
5274
|
if (schema.sensitive) {
|
|
4595
5275
|
issues.push(
|
|
4596
|
-
`${
|
|
5276
|
+
`${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
5277
|
);
|
|
4598
5278
|
} else {
|
|
4599
5279
|
warnings.push(
|
|
4600
|
-
`${
|
|
5280
|
+
`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
|
|
4601
5281
|
);
|
|
4602
5282
|
}
|
|
4603
5283
|
}
|
|
@@ -4605,51 +5285,64 @@ async function checkEnvFiles(options) {
|
|
|
4605
5285
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4606
5286
|
const inSchema = allVars.some(([k]) => k === key);
|
|
4607
5287
|
if (!inSchema) {
|
|
4608
|
-
warnings.push(`${
|
|
5288
|
+
warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
|
|
4609
5289
|
}
|
|
4610
5290
|
}
|
|
4611
5291
|
if (issues.length > 0) {
|
|
4612
|
-
console.log(
|
|
5292
|
+
console.log(chalk28.red.bold("Issues:"));
|
|
4613
5293
|
for (const issue of issues) {
|
|
4614
5294
|
console.log(` ${issue}`);
|
|
4615
5295
|
}
|
|
4616
5296
|
console.log("");
|
|
4617
5297
|
}
|
|
4618
5298
|
if (warnings.length > 0) {
|
|
4619
|
-
console.log(
|
|
5299
|
+
console.log(chalk28.yellow.bold("Warnings:"));
|
|
4620
5300
|
for (const warning of warnings) {
|
|
4621
5301
|
console.log(` ${warning}`);
|
|
4622
5302
|
}
|
|
4623
5303
|
console.log("");
|
|
4624
5304
|
}
|
|
4625
5305
|
if (issues.length === 0 && warnings.length === 0) {
|
|
4626
|
-
console.log(
|
|
5306
|
+
console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4627
5307
|
} else {
|
|
4628
|
-
console.log(
|
|
5308
|
+
console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4629
5309
|
`));
|
|
4630
5310
|
if (issues.length > 0) {
|
|
4631
5311
|
process.exit(1);
|
|
4632
5312
|
}
|
|
4633
5313
|
}
|
|
4634
5314
|
} catch (error) {
|
|
4635
|
-
console.error(
|
|
5315
|
+
console.error(chalk28.red(`
|
|
4636
5316
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4637
5317
|
`));
|
|
4638
5318
|
process.exit(1);
|
|
4639
5319
|
}
|
|
4640
5320
|
}
|
|
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);
|
|
5321
|
+
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);
|
|
5322
|
+
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
5323
|
async function validateEnvVars(options) {
|
|
4644
5324
|
const packages = options.packages || ["@spfn/core"];
|
|
4645
|
-
|
|
5325
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5326
|
+
if (targetEnv) {
|
|
5327
|
+
const { loadEnv: loadEnv8 } = await import("@spfn/core/env/loader");
|
|
5328
|
+
const result = loadEnv8({ nodeEnv: targetEnv });
|
|
5329
|
+
console.log(chalk28.blue.bold(`
|
|
5330
|
+
\u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
|
|
5331
|
+
`));
|
|
5332
|
+
if (result.loadedFiles.length > 0) {
|
|
5333
|
+
console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5334
|
+
}
|
|
5335
|
+
console.log("");
|
|
5336
|
+
} else {
|
|
5337
|
+
console.log(chalk28.blue.bold(`
|
|
4646
5338
|
\u{1F50D} Validating environment variables
|
|
4647
5339
|
`));
|
|
5340
|
+
}
|
|
4648
5341
|
const allErrors = [];
|
|
4649
5342
|
const allWarnings = [];
|
|
4650
5343
|
for (const packageName of packages) {
|
|
4651
5344
|
try {
|
|
4652
|
-
console.log(
|
|
5345
|
+
console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
|
|
4653
5346
|
const envSchema = await loadEnvSchema(packageName);
|
|
4654
5347
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4655
5348
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -4662,10 +5355,10 @@ async function validateEnvVars(options) {
|
|
|
4662
5355
|
}
|
|
4663
5356
|
} catch (error) {
|
|
4664
5357
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4665
|
-
console.log(
|
|
5358
|
+
console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4666
5359
|
continue;
|
|
4667
5360
|
}
|
|
4668
|
-
console.error(
|
|
5361
|
+
console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4669
5362
|
if (options.strict) {
|
|
4670
5363
|
process.exit(1);
|
|
4671
5364
|
}
|
|
@@ -4673,42 +5366,43 @@ async function validateEnvVars(options) {
|
|
|
4673
5366
|
}
|
|
4674
5367
|
console.log("");
|
|
4675
5368
|
if (allErrors.length > 0) {
|
|
4676
|
-
console.log(
|
|
5369
|
+
console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4677
5370
|
`));
|
|
4678
5371
|
for (const error of allErrors) {
|
|
4679
|
-
console.log(` ${
|
|
4680
|
-
console.log(` ${
|
|
4681
|
-
console.log(` ${
|
|
5372
|
+
console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
|
|
5373
|
+
console.log(` ${chalk28.dim(error.message)}`);
|
|
5374
|
+
console.log(` ${chalk28.dim(`from ${error.package}`)}`);
|
|
4682
5375
|
console.log("");
|
|
4683
5376
|
}
|
|
4684
5377
|
}
|
|
4685
5378
|
if (allWarnings.length > 0) {
|
|
4686
|
-
console.log(
|
|
5379
|
+
console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4687
5380
|
`));
|
|
4688
5381
|
for (const warning of allWarnings) {
|
|
4689
|
-
console.log(` ${
|
|
4690
|
-
console.log(` ${
|
|
5382
|
+
console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
|
|
5383
|
+
console.log(` ${chalk28.dim(warning.message)}`);
|
|
4691
5384
|
console.log("");
|
|
4692
5385
|
}
|
|
4693
5386
|
}
|
|
4694
5387
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4695
|
-
console.log(
|
|
5388
|
+
console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4696
5389
|
} else if (allErrors.length === 0) {
|
|
4697
|
-
console.log(
|
|
4698
|
-
console.log(
|
|
5390
|
+
console.log(chalk28.green("\u2705 No errors found."));
|
|
5391
|
+
console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4699
5392
|
`));
|
|
4700
5393
|
} else {
|
|
4701
|
-
console.log(
|
|
5394
|
+
console.log(chalk28.red(`
|
|
4702
5395
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4703
5396
|
`));
|
|
4704
5397
|
process.exit(1);
|
|
4705
5398
|
}
|
|
4706
5399
|
}
|
|
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);
|
|
5400
|
+
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
5401
|
|
|
4709
5402
|
// src/index.ts
|
|
5403
|
+
init_version();
|
|
4710
5404
|
var program = new Command13();
|
|
4711
|
-
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(
|
|
5405
|
+
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
4712
5406
|
program.addCommand(createCommand);
|
|
4713
5407
|
program.addCommand(initCommand);
|
|
4714
5408
|
program.addCommand(addCommand);
|