spfn 0.2.0-beta.2 → 0.2.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +585 -223
- package/dist/templates/lib/api-client.ts +65 -34
- package/dist/templates/server/router.ts +0 -14
- package/package.json +3 -3
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.20";
|
|
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;
|
|
873
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");
|
|
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;
|
|
@@ -961,7 +1045,7 @@ __export(function_migrations_exports, {
|
|
|
961
1045
|
import chalk11 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");
|
|
@@ -1009,7 +1093,7 @@ async function executeFunctionMigrations(functionMigrations) {
|
|
|
1009
1093
|
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
1010
1094
|
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
1011
1095
|
const postgres = await import("postgres");
|
|
1012
|
-
|
|
1096
|
+
loadEnv2();
|
|
1013
1097
|
if (!env2.DATABASE_URL) {
|
|
1014
1098
|
throw new Error("DATABASE_URL not found in environment");
|
|
1015
1099
|
}
|
|
@@ -1019,7 +1103,10 @@ async function executeFunctionMigrations(functionMigrations) {
|
|
|
1019
1103
|
for (const func of functionMigrations) {
|
|
1020
1104
|
console.log(chalk11.blue(`
|
|
1021
1105
|
\u{1F4E6} Running ${func.packageName} migrations...`));
|
|
1022
|
-
await migrate(db, {
|
|
1106
|
+
await migrate(db, {
|
|
1107
|
+
migrationsFolder: func.migrationsDir,
|
|
1108
|
+
migrationsTable: "__spfn_fn_migrations"
|
|
1109
|
+
});
|
|
1023
1110
|
console.log(chalk11.green(` \u2713 ${func.packageName} migrations applied`));
|
|
1024
1111
|
executedCount++;
|
|
1025
1112
|
}
|
|
@@ -1179,10 +1266,30 @@ init_init();
|
|
|
1179
1266
|
init_logger();
|
|
1180
1267
|
init_package_manager();
|
|
1181
1268
|
import { Command as Command4 } from "commander";
|
|
1182
|
-
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
|
|
1269
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync, unlinkSync, watch } from "fs";
|
|
1183
1270
|
import { join as join11 } from "path";
|
|
1184
1271
|
import { execa as execa4 } from "execa";
|
|
1185
1272
|
import chokidar from "chokidar";
|
|
1273
|
+
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
1274
|
+
return new Promise((resolve2, reject) => {
|
|
1275
|
+
if (existsSync11(filePath)) {
|
|
1276
|
+
unlinkSync(filePath);
|
|
1277
|
+
}
|
|
1278
|
+
const timer = setTimeout(() => {
|
|
1279
|
+
watcher.close();
|
|
1280
|
+
reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
|
|
1281
|
+
}, timeoutMs);
|
|
1282
|
+
const dir = join11(filePath, "..");
|
|
1283
|
+
const fileName = filePath.split("/").pop();
|
|
1284
|
+
const watcher = watch(dir, (event, name) => {
|
|
1285
|
+
if (name === fileName && existsSync11(filePath)) {
|
|
1286
|
+
watcher.close();
|
|
1287
|
+
clearTimeout(timer);
|
|
1288
|
+
resolve2(readFileSync3(filePath, "utf-8").trim());
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1186
1293
|
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
1294
|
process.setMaxListeners(20);
|
|
1188
1295
|
if (!process.env.NODE_ENV) {
|
|
@@ -1210,17 +1317,22 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1210
1317
|
if (options.host) configParts.push(`host: '${options.host}'`);
|
|
1211
1318
|
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
1212
1319
|
configParts.push("debug: true");
|
|
1320
|
+
const readyFile = join11(tempDir, "server-ready");
|
|
1213
1321
|
writeFileSync6(serverEntry, `
|
|
1322
|
+
import { writeFileSync } from 'fs';
|
|
1323
|
+
|
|
1214
1324
|
// Load environment variables FIRST (before any imports that depend on them)
|
|
1215
|
-
// Use centralized environment loader for standard dotenv priority
|
|
1216
1325
|
await import('@spfn/core/config');
|
|
1217
1326
|
|
|
1218
1327
|
// Import and start server
|
|
1219
1328
|
const { startServer } = await import('@spfn/core/server');
|
|
1220
1329
|
|
|
1221
|
-
await startServer({
|
|
1330
|
+
const instance = await startServer({
|
|
1222
1331
|
${configParts.join(",\n ")}
|
|
1223
1332
|
});
|
|
1333
|
+
|
|
1334
|
+
// Signal ready with actual port
|
|
1335
|
+
writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
|
|
1224
1336
|
`);
|
|
1225
1337
|
writeFileSync6(watcherEntry, `
|
|
1226
1338
|
// Load environment variables
|
|
@@ -1277,7 +1389,9 @@ catch (error)
|
|
|
1277
1389
|
const pm = detectPackageManager(cwd);
|
|
1278
1390
|
if (options.serverOnly || !hasNext) {
|
|
1279
1391
|
const watchMode2 = options.watch === true;
|
|
1280
|
-
|
|
1392
|
+
const host = options.host ?? process.env.HOST ?? "localhost";
|
|
1393
|
+
const port = options.port ?? process.env.PORT ?? "4000";
|
|
1394
|
+
logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
|
|
1281
1395
|
`);
|
|
1282
1396
|
let serverProcess2 = null;
|
|
1283
1397
|
let watcherProcess2 = null;
|
|
@@ -1471,7 +1585,13 @@ catch (error)
|
|
|
1471
1585
|
process.on("SIGTERM", cleanup);
|
|
1472
1586
|
startWatcher();
|
|
1473
1587
|
startServer();
|
|
1474
|
-
|
|
1588
|
+
try {
|
|
1589
|
+
const port = await waitForReadyFile(readyFile);
|
|
1590
|
+
logger.info(`[SPFN] Server ready on port ${port}, starting Next.js...
|
|
1591
|
+
`);
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
|
|
1594
|
+
}
|
|
1475
1595
|
startNext();
|
|
1476
1596
|
await new Promise((resolve2) => {
|
|
1477
1597
|
const keepAlive = setInterval(() => {
|
|
@@ -1522,7 +1642,7 @@ async function buildProject(options) {
|
|
|
1522
1642
|
const orchestrator = new CodegenOrchestrator({
|
|
1523
1643
|
generators,
|
|
1524
1644
|
cwd,
|
|
1525
|
-
debug:
|
|
1645
|
+
debug: true
|
|
1526
1646
|
});
|
|
1527
1647
|
await orchestrator.generateAll();
|
|
1528
1648
|
spinner.succeed("API client generated");
|
|
@@ -1832,7 +1952,7 @@ async function runGenerators() {
|
|
|
1832
1952
|
const orchestrator = new CodegenOrchestrator({
|
|
1833
1953
|
generators,
|
|
1834
1954
|
cwd,
|
|
1835
|
-
debug:
|
|
1955
|
+
debug: true
|
|
1836
1956
|
});
|
|
1837
1957
|
await orchestrator.generateAll();
|
|
1838
1958
|
console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
|
|
@@ -1991,14 +2111,14 @@ import { Command as Command9 } from "commander";
|
|
|
1991
2111
|
import chalk10 from "chalk";
|
|
1992
2112
|
|
|
1993
2113
|
// src/commands/db/utils/drizzle.ts
|
|
1994
|
-
import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
|
|
2114
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
|
|
1995
2115
|
import { spawn } from "child_process";
|
|
1996
2116
|
import chalk9 from "chalk";
|
|
1997
2117
|
import ora6 from "ora";
|
|
1998
2118
|
import { env } from "@spfn/core/config";
|
|
1999
|
-
import {
|
|
2119
|
+
import { loadEnv } from "@spfn/core/server";
|
|
2000
2120
|
function validateDatabasePrerequisites() {
|
|
2001
|
-
|
|
2121
|
+
loadEnv();
|
|
2002
2122
|
if (!env.DATABASE_URL) {
|
|
2003
2123
|
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2004
2124
|
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
@@ -2010,7 +2130,7 @@ async function runDrizzleCommand(command) {
|
|
|
2010
2130
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2011
2131
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2012
2132
|
if (!hasUserConfig) {
|
|
2013
|
-
|
|
2133
|
+
loadEnv();
|
|
2014
2134
|
if (!env.DATABASE_URL) {
|
|
2015
2135
|
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2016
2136
|
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
@@ -2020,7 +2140,10 @@ async function runDrizzleCommand(command) {
|
|
|
2020
2140
|
const configContent = generateDrizzleConfigFile({
|
|
2021
2141
|
cwd: process.cwd(),
|
|
2022
2142
|
// Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
|
|
2023
|
-
disablePackageDiscovery: true
|
|
2143
|
+
disablePackageDiscovery: true,
|
|
2144
|
+
// Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
|
|
2145
|
+
expandGlobs: true,
|
|
2146
|
+
autoDetectSchemas: true
|
|
2024
2147
|
});
|
|
2025
2148
|
writeFileSync9(tempConfigPath, configContent);
|
|
2026
2149
|
console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
|
|
@@ -2031,11 +2154,12 @@ async function runDrizzleCommand(command) {
|
|
|
2031
2154
|
const drizzleProcess = spawn("drizzle-kit", args, {
|
|
2032
2155
|
stdio: "inherit",
|
|
2033
2156
|
// Allow interactive input
|
|
2034
|
-
shell: true
|
|
2157
|
+
shell: true,
|
|
2158
|
+
env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
|
|
2035
2159
|
});
|
|
2036
2160
|
const cleanup = () => {
|
|
2037
2161
|
if (!hasUserConfig && existsSync15(tempConfigPath)) {
|
|
2038
|
-
|
|
2162
|
+
unlinkSync2(tempConfigPath);
|
|
2039
2163
|
}
|
|
2040
2164
|
};
|
|
2041
2165
|
drizzleProcess.on("close", (code) => {
|
|
@@ -2107,6 +2231,8 @@ async function dbPush() {
|
|
|
2107
2231
|
|
|
2108
2232
|
// src/commands/db/migrate.ts
|
|
2109
2233
|
import chalk16 from "chalk";
|
|
2234
|
+
import { join as join16 } from "path";
|
|
2235
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2110
2236
|
|
|
2111
2237
|
// src/commands/db/backup.ts
|
|
2112
2238
|
import { promises as fs3 } from "fs";
|
|
@@ -2341,8 +2467,10 @@ async function listBackupFiles() {
|
|
|
2341
2467
|
|
|
2342
2468
|
// src/commands/db/backup.ts
|
|
2343
2469
|
import { env as env3 } from "@spfn/core/config";
|
|
2470
|
+
import { loadEnv as loadEnv3 } from "@spfn/core/server";
|
|
2344
2471
|
async function dbBackup(options) {
|
|
2345
2472
|
console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2473
|
+
loadEnv3();
|
|
2346
2474
|
const dbUrl = env3.DATABASE_URL;
|
|
2347
2475
|
if (!dbUrl) {
|
|
2348
2476
|
console.error(chalk15.red("\u274C DATABASE_URL not found in environment"));
|
|
@@ -2457,7 +2585,10 @@ async function dbBackup(options) {
|
|
|
2457
2585
|
}
|
|
2458
2586
|
|
|
2459
2587
|
// src/commands/db/migrate.ts
|
|
2460
|
-
import "@spfn/core/config";
|
|
2588
|
+
import { env as env4 } from "@spfn/core/config";
|
|
2589
|
+
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
2590
|
+
var FUNCTION_MIGRATIONS_TABLE = "__spfn_fn_migrations";
|
|
2591
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
2461
2592
|
async function dbMigrate(options = {}) {
|
|
2462
2593
|
try {
|
|
2463
2594
|
validateDatabasePrerequisites();
|
|
@@ -2473,35 +2604,62 @@ async function dbMigrate(options = {}) {
|
|
|
2473
2604
|
});
|
|
2474
2605
|
console.log("");
|
|
2475
2606
|
}
|
|
2476
|
-
const {
|
|
2607
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
2608
|
+
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
2609
|
+
const postgres = await import("postgres");
|
|
2610
|
+
loadEnv4();
|
|
2611
|
+
if (!env4.DATABASE_URL) {
|
|
2612
|
+
console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
|
|
2613
|
+
process.exit(1);
|
|
2614
|
+
}
|
|
2615
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2477
2616
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2478
2617
|
if (functions.length > 0) {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
console.log(chalk16.dim(` - ${func.packageName}`));
|
|
2482
|
-
});
|
|
2618
|
+
const fnConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2619
|
+
const fnDb = drizzle(fnConn);
|
|
2483
2620
|
try {
|
|
2484
|
-
|
|
2621
|
+
console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
|
|
2622
|
+
functions.forEach((func) => {
|
|
2623
|
+
console.log(chalk16.dim(` - ${func.packageName}`));
|
|
2624
|
+
});
|
|
2625
|
+
for (const func of functions) {
|
|
2626
|
+
console.log(chalk16.blue(`
|
|
2627
|
+
\u{1F4E6} Running ${func.packageName} migrations...`));
|
|
2628
|
+
await migrate(fnDb, {
|
|
2629
|
+
migrationsFolder: func.migrationsDir,
|
|
2630
|
+
migrationsTable: FUNCTION_MIGRATIONS_TABLE
|
|
2631
|
+
});
|
|
2632
|
+
console.log(chalk16.green(` \u2713 ${func.packageName} migrations applied`));
|
|
2633
|
+
}
|
|
2485
2634
|
console.log(chalk16.green("\u2705 Function migrations applied\n"));
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2489
|
-
process.exit(1);
|
|
2635
|
+
} finally {
|
|
2636
|
+
await fnConn.end();
|
|
2490
2637
|
}
|
|
2491
2638
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2639
|
+
const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
|
|
2640
|
+
if (existsSync18(projectMigrationsDir)) {
|
|
2641
|
+
const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2642
|
+
const projDb = drizzle(projConn);
|
|
2643
|
+
try {
|
|
2644
|
+
console.log(chalk16.blue("\u{1F4E6} Running project migrations..."));
|
|
2645
|
+
await migrate(projDb, {
|
|
2646
|
+
migrationsFolder: projectMigrationsDir,
|
|
2647
|
+
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
2648
|
+
});
|
|
2649
|
+
console.log(chalk16.green("\u2705 Project migrations applied successfully"));
|
|
2650
|
+
} finally {
|
|
2651
|
+
await projConn.end();
|
|
2652
|
+
}
|
|
2653
|
+
} else {
|
|
2654
|
+
console.log(chalk16.dim("No project migrations found (src/server/drizzle)"));
|
|
2655
|
+
}
|
|
2498
2656
|
}
|
|
2499
2657
|
|
|
2500
2658
|
// src/commands/db/studio.ts
|
|
2501
2659
|
import chalk17 from "chalk";
|
|
2502
|
-
import { existsSync as
|
|
2660
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
2503
2661
|
import { spawn as spawn3 } from "child_process";
|
|
2504
|
-
import { env as
|
|
2662
|
+
import { env as env5 } from "@spfn/core/config";
|
|
2505
2663
|
import "@spfn/core/config";
|
|
2506
2664
|
async function dbStudio(requestedPort) {
|
|
2507
2665
|
console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
@@ -2518,12 +2676,12 @@ async function dbStudio(requestedPort) {
|
|
|
2518
2676
|
console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
2519
2677
|
process.exit(1);
|
|
2520
2678
|
}
|
|
2521
|
-
const hasUserConfig =
|
|
2679
|
+
const hasUserConfig = existsSync19("./drizzle.config.ts");
|
|
2522
2680
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2523
2681
|
try {
|
|
2524
2682
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2525
2683
|
if (!hasUserConfig) {
|
|
2526
|
-
if (!
|
|
2684
|
+
if (!env5.DATABASE_URL) {
|
|
2527
2685
|
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2528
2686
|
console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2529
2687
|
process.exit(1);
|
|
@@ -2540,11 +2698,12 @@ async function dbStudio(requestedPort) {
|
|
|
2540
2698
|
}
|
|
2541
2699
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
2542
2700
|
stdio: "inherit",
|
|
2543
|
-
shell: true
|
|
2701
|
+
shell: true,
|
|
2702
|
+
env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
|
|
2544
2703
|
});
|
|
2545
2704
|
const cleanup = () => {
|
|
2546
|
-
if (!hasUserConfig &&
|
|
2547
|
-
|
|
2705
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
2706
|
+
unlinkSync3(tempConfigPath);
|
|
2548
2707
|
}
|
|
2549
2708
|
};
|
|
2550
2709
|
studioProcess.on("exit", (code) => {
|
|
@@ -2573,8 +2732,8 @@ async function dbStudio(requestedPort) {
|
|
|
2573
2732
|
process.exit(0);
|
|
2574
2733
|
});
|
|
2575
2734
|
} catch (error) {
|
|
2576
|
-
if (!hasUserConfig &&
|
|
2577
|
-
|
|
2735
|
+
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
2736
|
+
unlinkSync3(tempConfigPath);
|
|
2578
2737
|
}
|
|
2579
2738
|
console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
|
|
2580
2739
|
console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
|
|
@@ -2626,10 +2785,12 @@ import { spawn as spawn4 } from "child_process";
|
|
|
2626
2785
|
import chalk20 from "chalk";
|
|
2627
2786
|
import ora9 from "ora";
|
|
2628
2787
|
import prompts4 from "prompts";
|
|
2629
|
-
import { env as
|
|
2788
|
+
import { env as env6 } from "@spfn/core/config";
|
|
2789
|
+
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
2630
2790
|
async function dbRestore(backupFile, options = {}) {
|
|
2631
2791
|
console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
2632
|
-
|
|
2792
|
+
loadEnv5();
|
|
2793
|
+
const dbUrl = env6.DATABASE_URL;
|
|
2633
2794
|
if (!dbUrl) {
|
|
2634
2795
|
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
2635
2796
|
console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
@@ -2678,29 +2839,29 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2678
2839
|
if (metadata.backup.schemaOnly) {
|
|
2679
2840
|
console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
2680
2841
|
}
|
|
2681
|
-
const
|
|
2842
|
+
const warnings2 = [];
|
|
2682
2843
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
2683
2844
|
collectGitInfo(),
|
|
2684
2845
|
collectMigrationInfo(dbUrl)
|
|
2685
2846
|
]);
|
|
2686
2847
|
if (metadata.git && currentGitInfo) {
|
|
2687
2848
|
if (metadata.git.commit !== currentGitInfo.commit) {
|
|
2688
|
-
|
|
2849
|
+
warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
|
|
2689
2850
|
}
|
|
2690
2851
|
if (metadata.git.branch !== currentGitInfo.branch) {
|
|
2691
|
-
|
|
2852
|
+
warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
|
|
2692
2853
|
}
|
|
2693
2854
|
}
|
|
2694
2855
|
if (metadata.migrations && currentMigrationInfo) {
|
|
2695
2856
|
if (metadata.migrations.hash !== currentMigrationInfo.hash) {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2857
|
+
warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
|
|
2858
|
+
warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
|
|
2859
|
+
warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
|
|
2699
2860
|
}
|
|
2700
2861
|
}
|
|
2701
|
-
if (
|
|
2862
|
+
if (warnings2.length > 0) {
|
|
2702
2863
|
console.log(chalk20.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
2703
|
-
|
|
2864
|
+
warnings2.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
|
|
2704
2865
|
console.log("");
|
|
2705
2866
|
}
|
|
2706
2867
|
}
|
|
@@ -2728,6 +2889,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2728
2889
|
args.push("-p", dbInfo.port);
|
|
2729
2890
|
args.push("-U", dbInfo.user);
|
|
2730
2891
|
args.push("-d", dbInfo.database);
|
|
2892
|
+
args.push("--verbose");
|
|
2731
2893
|
if (options.drop) {
|
|
2732
2894
|
args.push("--clean");
|
|
2733
2895
|
}
|
|
@@ -2750,8 +2912,10 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2750
2912
|
args.push("-p", dbInfo.port);
|
|
2751
2913
|
args.push("-U", dbInfo.user);
|
|
2752
2914
|
args.push("-d", dbInfo.database);
|
|
2915
|
+
args.push("-v", "ON_ERROR_STOP=1");
|
|
2753
2916
|
args.push("-f", file);
|
|
2754
2917
|
}
|
|
2918
|
+
const verbose = options.verbose ?? false;
|
|
2755
2919
|
const spinner = ora9("Restoring backup...").start();
|
|
2756
2920
|
const restoreProcess = spawn4(command, args, {
|
|
2757
2921
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -2760,19 +2924,81 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2760
2924
|
PGPASSWORD: dbInfo.password
|
|
2761
2925
|
}
|
|
2762
2926
|
});
|
|
2763
|
-
|
|
2927
|
+
const warnings = [];
|
|
2928
|
+
const errors = [];
|
|
2929
|
+
let objectCount = 0;
|
|
2930
|
+
let lastObject = "";
|
|
2764
2931
|
restoreProcess.stderr?.on("data", (data) => {
|
|
2765
|
-
|
|
2932
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
2933
|
+
for (const line of lines) {
|
|
2934
|
+
if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
|
|
2935
|
+
warnings.push(line.trim());
|
|
2936
|
+
} else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
|
|
2937
|
+
errors.push(line.trim());
|
|
2938
|
+
}
|
|
2939
|
+
const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
|
|
2940
|
+
if (objectMatch) {
|
|
2941
|
+
objectCount = Number(objectMatch[2]);
|
|
2942
|
+
const current = Number(objectMatch[1]);
|
|
2943
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
2944
|
+
lastObject = desc;
|
|
2945
|
+
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
2946
|
+
} else if (isCustomFormat) {
|
|
2947
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
2948
|
+
if (desc && !/warning:|error:/i.test(desc)) {
|
|
2949
|
+
lastObject = desc;
|
|
2950
|
+
spinner.text = `Restoring backup... ${desc}`;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
if (verbose) {
|
|
2954
|
+
spinner.stop();
|
|
2955
|
+
console.log(chalk20.dim(` ${line.trim()}`));
|
|
2956
|
+
spinner.start();
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
restoreProcess.stdout?.on("data", (data) => {
|
|
2961
|
+
if (verbose) {
|
|
2962
|
+
spinner.stop();
|
|
2963
|
+
console.log(chalk20.dim(` ${data.toString().trim()}`));
|
|
2964
|
+
spinner.start();
|
|
2965
|
+
}
|
|
2766
2966
|
});
|
|
2767
2967
|
await new Promise((resolve2, reject) => {
|
|
2768
2968
|
restoreProcess.on("close", (code) => {
|
|
2769
2969
|
if (code === 0) {
|
|
2770
|
-
|
|
2970
|
+
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
2971
|
+
spinner.succeed(`Restore completed${summary}`);
|
|
2972
|
+
if (warnings.length > 0) {
|
|
2973
|
+
console.log(chalk20.yellow(`
|
|
2974
|
+
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
2975
|
+
`));
|
|
2976
|
+
for (const w of warnings) {
|
|
2977
|
+
console.log(chalk20.yellow(` - ${w}`));
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2771
2980
|
console.log(chalk20.green("\n\u2705 Database restored successfully"));
|
|
2772
2981
|
resolve2();
|
|
2773
2982
|
} else {
|
|
2774
2983
|
spinner.fail("Restore failed");
|
|
2775
|
-
|
|
2984
|
+
if (errors.length > 0) {
|
|
2985
|
+
console.error(chalk20.red(`
|
|
2986
|
+
\u274C Errors (${errors.length}):
|
|
2987
|
+
`));
|
|
2988
|
+
for (const e of errors) {
|
|
2989
|
+
console.error(chalk20.red(` - ${e}`));
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
if (warnings.length > 0) {
|
|
2993
|
+
console.log(chalk20.yellow(`
|
|
2994
|
+
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
2995
|
+
`));
|
|
2996
|
+
for (const w of warnings) {
|
|
2997
|
+
console.log(chalk20.yellow(` - ${w}`));
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
3001
|
+
reject(new Error(fallback));
|
|
2776
3002
|
}
|
|
2777
3003
|
});
|
|
2778
3004
|
restoreProcess.on("error", (error) => {
|
|
@@ -2780,8 +3006,11 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
2780
3006
|
reject(error);
|
|
2781
3007
|
});
|
|
2782
3008
|
}).catch((error) => {
|
|
2783
|
-
|
|
2784
|
-
|
|
3009
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3010
|
+
if (msg) {
|
|
3011
|
+
console.error(chalk20.red(`
|
|
3012
|
+
\u274C ${msg}`));
|
|
3013
|
+
}
|
|
2785
3014
|
process.exit(1);
|
|
2786
3015
|
});
|
|
2787
3016
|
}
|
|
@@ -2881,14 +3110,14 @@ dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").op
|
|
|
2881
3110
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
2882
3111
|
dbCommand.command("check").description("Check database connection").action(dbCheck);
|
|
2883
3112
|
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));
|
|
3113
|
+
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
3114
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
2886
3115
|
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));
|
|
2887
3116
|
|
|
2888
3117
|
// src/commands/add.ts
|
|
2889
3118
|
import { Command as Command10 } from "commander";
|
|
2890
|
-
import { existsSync as
|
|
2891
|
-
import { join as
|
|
3119
|
+
import { existsSync as existsSync20, readFileSync as readFileSync6 } from "fs";
|
|
3120
|
+
import { join as join17 } from "path";
|
|
2892
3121
|
import { exec as exec2 } from "child_process";
|
|
2893
3122
|
import { promisify as promisify2 } from "util";
|
|
2894
3123
|
import chalk23 from "chalk";
|
|
@@ -2906,9 +3135,9 @@ async function addPackage(packageName) {
|
|
|
2906
3135
|
\u{1F4E6} Setting up ${packageName}...
|
|
2907
3136
|
`));
|
|
2908
3137
|
try {
|
|
2909
|
-
const pkgPath =
|
|
2910
|
-
const pkgJsonPath =
|
|
2911
|
-
if (!
|
|
3138
|
+
const pkgPath = join17(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3139
|
+
const pkgJsonPath = join17(pkgPath, "package.json");
|
|
3140
|
+
if (!existsSync20(pkgJsonPath)) {
|
|
2912
3141
|
const installSpinner = ora11("Installing package...").start();
|
|
2913
3142
|
try {
|
|
2914
3143
|
await execAsync2(`pnpm add ${packageName}`);
|
|
@@ -2920,7 +3149,7 @@ async function addPackage(packageName) {
|
|
|
2920
3149
|
} else {
|
|
2921
3150
|
console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
|
|
2922
3151
|
}
|
|
2923
|
-
if (!
|
|
3152
|
+
if (!existsSync20(pkgJsonPath)) {
|
|
2924
3153
|
throw new Error(`Package ${packageName} not found after installation`);
|
|
2925
3154
|
}
|
|
2926
3155
|
const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
|
|
@@ -2928,8 +3157,8 @@ async function addPackage(packageName) {
|
|
|
2928
3157
|
console.log(chalk23.blue(`
|
|
2929
3158
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
2930
3159
|
`));
|
|
2931
|
-
const { env:
|
|
2932
|
-
if (!
|
|
3160
|
+
const { env: env7 } = await import("@spfn/core/config");
|
|
3161
|
+
if (!env7.DATABASE_URL) {
|
|
2933
3162
|
console.log(chalk23.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
2934
3163
|
console.log(chalk23.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
2935
3164
|
console.log(chalk23.gray(` pnpm spfn db push
|
|
@@ -2976,8 +3205,8 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
|
|
|
2976
3205
|
init_logger();
|
|
2977
3206
|
import { Command as Command11 } from "commander";
|
|
2978
3207
|
import ora12 from "ora";
|
|
2979
|
-
import { join as
|
|
2980
|
-
import { existsSync as
|
|
3208
|
+
import { join as join26 } from "path";
|
|
3209
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2981
3210
|
import chalk25 from "chalk";
|
|
2982
3211
|
|
|
2983
3212
|
// src/commands/generate/prompts.ts
|
|
@@ -3070,11 +3299,11 @@ async function confirmConfiguration(config) {
|
|
|
3070
3299
|
}
|
|
3071
3300
|
|
|
3072
3301
|
// src/commands/generate/generators/structure.ts
|
|
3073
|
-
import { join as
|
|
3302
|
+
import { join as join25 } from "path";
|
|
3074
3303
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
3075
3304
|
|
|
3076
3305
|
// src/commands/generate/generators/config.ts
|
|
3077
|
-
import { join as
|
|
3306
|
+
import { join as join19 } from "path";
|
|
3078
3307
|
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
|
|
3079
3308
|
|
|
3080
3309
|
// src/commands/generate/string-utils.ts
|
|
@@ -3104,29 +3333,29 @@ function toSafeSchemaName(str) {
|
|
|
3104
3333
|
}
|
|
3105
3334
|
|
|
3106
3335
|
// src/commands/generate/template-loader.ts
|
|
3107
|
-
import { readFileSync as readFileSync7, existsSync as
|
|
3108
|
-
import { join as
|
|
3336
|
+
import { readFileSync as readFileSync7, existsSync as existsSync21 } from "fs";
|
|
3337
|
+
import { join as join18, dirname as dirname2 } from "path";
|
|
3109
3338
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3110
3339
|
function findTemplatesPath2() {
|
|
3111
3340
|
const __filename = fileURLToPath2(import.meta.url);
|
|
3112
3341
|
const __dirname2 = dirname2(__filename);
|
|
3113
|
-
const distPath =
|
|
3114
|
-
if (
|
|
3342
|
+
const distPath = join18(__dirname2, "commands", "generate", "templates");
|
|
3343
|
+
if (existsSync21(distPath)) {
|
|
3115
3344
|
return distPath;
|
|
3116
3345
|
}
|
|
3117
|
-
const sameDirPath =
|
|
3118
|
-
if (
|
|
3346
|
+
const sameDirPath = join18(__dirname2, "templates");
|
|
3347
|
+
if (existsSync21(sameDirPath)) {
|
|
3119
3348
|
return sameDirPath;
|
|
3120
3349
|
}
|
|
3121
|
-
const srcPath =
|
|
3122
|
-
if (
|
|
3350
|
+
const srcPath = join18(__dirname2, "..", "..", "src", "commands", "generate", "templates");
|
|
3351
|
+
if (existsSync21(srcPath)) {
|
|
3123
3352
|
return srcPath;
|
|
3124
3353
|
}
|
|
3125
3354
|
throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
|
|
3126
3355
|
}
|
|
3127
3356
|
function loadTemplate(templateName, variables) {
|
|
3128
3357
|
const templatesDir = findTemplatesPath2();
|
|
3129
|
-
const templatePath =
|
|
3358
|
+
const templatePath = join18(templatesDir, `${templateName}.template`);
|
|
3130
3359
|
let content = readFileSync7(templatePath, "utf-8");
|
|
3131
3360
|
for (const [key, value] of Object.entries(variables)) {
|
|
3132
3361
|
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
@@ -3230,7 +3459,7 @@ function generatePackageJson(fnDir, scope, fnName, description) {
|
|
|
3230
3459
|
}
|
|
3231
3460
|
};
|
|
3232
3461
|
writeFileSync11(
|
|
3233
|
-
|
|
3462
|
+
join19(fnDir, "package.json"),
|
|
3234
3463
|
JSON.stringify(content, null, 4) + "\n"
|
|
3235
3464
|
);
|
|
3236
3465
|
}
|
|
@@ -3264,7 +3493,7 @@ function generateTsConfig(fnDir) {
|
|
|
3264
3493
|
exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
|
|
3265
3494
|
};
|
|
3266
3495
|
writeFileSync11(
|
|
3267
|
-
|
|
3496
|
+
join19(fnDir, "tsconfig.json"),
|
|
3268
3497
|
JSON.stringify(content, null, 4) + "\n"
|
|
3269
3498
|
);
|
|
3270
3499
|
}
|
|
@@ -3340,7 +3569,7 @@ export default defineConfig({
|
|
|
3340
3569
|
],
|
|
3341
3570
|
});
|
|
3342
3571
|
`;
|
|
3343
|
-
writeFileSync11(
|
|
3572
|
+
writeFileSync11(join19(fnDir, "tsup.config.ts"), content);
|
|
3344
3573
|
}
|
|
3345
3574
|
function generateDrizzleConfig(fnDir, scope, fnName) {
|
|
3346
3575
|
const schemaName = `spfn_${toSnakeCase(fnName)}`;
|
|
@@ -3360,7 +3589,7 @@ export default defineConfig({
|
|
|
3360
3589
|
schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
|
|
3361
3590
|
});
|
|
3362
3591
|
`;
|
|
3363
|
-
writeFileSync11(
|
|
3592
|
+
writeFileSync11(join19(fnDir, "drizzle.config.ts"), content);
|
|
3364
3593
|
}
|
|
3365
3594
|
function generateExampleGenerator(fnDir, scope, fnName) {
|
|
3366
3595
|
const pascalName = toPascalCase(fnName);
|
|
@@ -3429,10 +3658,10 @@ export const moduleName = '${fnName}';
|
|
|
3429
3658
|
};
|
|
3430
3659
|
}
|
|
3431
3660
|
`;
|
|
3432
|
-
const generatorsDir =
|
|
3661
|
+
const generatorsDir = join19(fnDir, "src/server/generators");
|
|
3433
3662
|
mkdirSync3(generatorsDir, { recursive: true });
|
|
3434
3663
|
writeFileSync11(
|
|
3435
|
-
|
|
3664
|
+
join19(generatorsDir, "example-generator.ts"),
|
|
3436
3665
|
content
|
|
3437
3666
|
);
|
|
3438
3667
|
const indexContent = `/**
|
|
@@ -3447,7 +3676,7 @@ export const moduleName = '${fnName}';
|
|
|
3447
3676
|
export { create${pascalName}ExampleGenerator } from './example-generator.js';
|
|
3448
3677
|
`;
|
|
3449
3678
|
writeFileSync11(
|
|
3450
|
-
|
|
3679
|
+
join19(generatorsDir, "index.ts"),
|
|
3451
3680
|
indexContent
|
|
3452
3681
|
);
|
|
3453
3682
|
}
|
|
@@ -3936,15 +4165,15 @@ Contributions are welcome! Please follow the development workflow above.
|
|
|
3936
4165
|
|
|
3937
4166
|
MIT
|
|
3938
4167
|
`;
|
|
3939
|
-
writeFileSync11(
|
|
4168
|
+
writeFileSync11(join19(fnDir, "README.md"), content);
|
|
3940
4169
|
}
|
|
3941
4170
|
|
|
3942
4171
|
// src/commands/generate/generators/entity.ts
|
|
3943
|
-
import { join as
|
|
3944
|
-
import { writeFileSync as writeFileSync12, existsSync as
|
|
4172
|
+
import { join as join20 } from "path";
|
|
4173
|
+
import { writeFileSync as writeFileSync12, existsSync as existsSync22 } from "fs";
|
|
3945
4174
|
function generateSchema(fnDir, scope, fnName) {
|
|
3946
|
-
const schemaFilePath =
|
|
3947
|
-
if (
|
|
4175
|
+
const schemaFilePath = join20(fnDir, "src/server/entities/schema.ts");
|
|
4176
|
+
if (existsSync22(schemaFilePath)) {
|
|
3948
4177
|
return;
|
|
3949
4178
|
}
|
|
3950
4179
|
const packageName = `${scope}/${fnName}`;
|
|
@@ -3975,7 +4204,7 @@ function generateEntity(fnDir, scope, fnName, entityName) {
|
|
|
3975
4204
|
SCHEMA_FILE_NAME: schemaFileName
|
|
3976
4205
|
});
|
|
3977
4206
|
writeFileSync12(
|
|
3978
|
-
|
|
4207
|
+
join20(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
|
|
3979
4208
|
content
|
|
3980
4209
|
);
|
|
3981
4210
|
}
|
|
@@ -3983,11 +4212,11 @@ function generateEntitiesIndex(fnDir, entities) {
|
|
|
3983
4212
|
const schemaExport = `export * from './schema';`;
|
|
3984
4213
|
const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
|
|
3985
4214
|
const content = [schemaExport, entityExports].filter(Boolean).join("\n");
|
|
3986
|
-
writeFileSync12(
|
|
4215
|
+
writeFileSync12(join20(fnDir, "src/server/entities/index.ts"), content + "\n");
|
|
3987
4216
|
}
|
|
3988
4217
|
|
|
3989
4218
|
// src/commands/generate/generators/repository.ts
|
|
3990
|
-
import { join as
|
|
4219
|
+
import { join as join21 } from "path";
|
|
3991
4220
|
import { writeFileSync as writeFileSync13 } from "fs";
|
|
3992
4221
|
function generateRepository(fnDir, entityName) {
|
|
3993
4222
|
const pascalName = toPascalCase(entityName);
|
|
@@ -3998,17 +4227,17 @@ function generateRepository(fnDir, entityName) {
|
|
|
3998
4227
|
REPO_NAME: repoName
|
|
3999
4228
|
});
|
|
4000
4229
|
writeFileSync13(
|
|
4001
|
-
|
|
4230
|
+
join21(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
|
|
4002
4231
|
content
|
|
4003
4232
|
);
|
|
4004
4233
|
}
|
|
4005
4234
|
function generateRepositoriesIndex(fnDir, entities) {
|
|
4006
4235
|
const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
|
|
4007
|
-
writeFileSync13(
|
|
4236
|
+
writeFileSync13(join21(fnDir, "src/server/repositories/index.ts"), exports + "\n");
|
|
4008
4237
|
}
|
|
4009
4238
|
|
|
4010
4239
|
// src/commands/generate/generators/route.ts
|
|
4011
|
-
import { join as
|
|
4240
|
+
import { join as join22 } from "path";
|
|
4012
4241
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
|
|
4013
4242
|
function generateRoute(fnDir, entityName) {
|
|
4014
4243
|
const pascalName = toPascalCase(entityName);
|
|
@@ -4020,13 +4249,13 @@ function generateRoute(fnDir, entityName) {
|
|
|
4020
4249
|
REPO_NAME: repoName,
|
|
4021
4250
|
KEBAB_NAME: kebabName
|
|
4022
4251
|
});
|
|
4023
|
-
const routeDir =
|
|
4252
|
+
const routeDir = join22(fnDir, `src/server/routes/${kebabName}`);
|
|
4024
4253
|
mkdirSync4(routeDir, { recursive: true });
|
|
4025
|
-
writeFileSync14(
|
|
4254
|
+
writeFileSync14(join22(routeDir, "index.ts"), content);
|
|
4026
4255
|
}
|
|
4027
4256
|
|
|
4028
4257
|
// src/commands/generate/generators/contract.ts
|
|
4029
|
-
import { join as
|
|
4258
|
+
import { join as join23 } from "path";
|
|
4030
4259
|
import { writeFileSync as writeFileSync15 } from "fs";
|
|
4031
4260
|
function generateContract(fnDir, entityName) {
|
|
4032
4261
|
const pascalName = toPascalCase(entityName);
|
|
@@ -4035,13 +4264,13 @@ function generateContract(fnDir, entityName) {
|
|
|
4035
4264
|
ENTITY_NAME: entityName
|
|
4036
4265
|
});
|
|
4037
4266
|
writeFileSync15(
|
|
4038
|
-
|
|
4267
|
+
join23(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
|
|
4039
4268
|
content
|
|
4040
4269
|
);
|
|
4041
4270
|
}
|
|
4042
4271
|
|
|
4043
4272
|
// src/commands/generate/generators/index-files.ts
|
|
4044
|
-
import { join as
|
|
4273
|
+
import { join as join24 } from "path";
|
|
4045
4274
|
import { writeFileSync as writeFileSync16 } from "fs";
|
|
4046
4275
|
function generateMainIndex(fnDir, fnName) {
|
|
4047
4276
|
const content = `/**
|
|
@@ -4068,7 +4297,7 @@ export * from '@/lib/types/index';
|
|
|
4068
4297
|
|
|
4069
4298
|
export * from '@/server/entities/index';
|
|
4070
4299
|
`;
|
|
4071
|
-
writeFileSync16(
|
|
4300
|
+
writeFileSync16(join24(fnDir, "src/index.ts"), content);
|
|
4072
4301
|
}
|
|
4073
4302
|
function generateServerIndex(fnDir) {
|
|
4074
4303
|
const content = `/**
|
|
@@ -4103,7 +4332,7 @@ export * from '@/server/repositories/index';
|
|
|
4103
4332
|
|
|
4104
4333
|
// TODO: Export helpers here
|
|
4105
4334
|
`;
|
|
4106
|
-
writeFileSync16(
|
|
4335
|
+
writeFileSync16(join24(fnDir, "src/server.ts"), content);
|
|
4107
4336
|
}
|
|
4108
4337
|
function generateClientIndex(fnDir) {
|
|
4109
4338
|
const content = `/**
|
|
@@ -4136,7 +4365,7 @@ export * from './client/store';
|
|
|
4136
4365
|
|
|
4137
4366
|
export * from './client/components';
|
|
4138
4367
|
`;
|
|
4139
|
-
writeFileSync16(
|
|
4368
|
+
writeFileSync16(join24(fnDir, "src/client.ts"), content);
|
|
4140
4369
|
}
|
|
4141
4370
|
function generateTypesFile(fnDir, fnName) {
|
|
4142
4371
|
const content = `/**
|
|
@@ -4148,7 +4377,7 @@ function generateTypesFile(fnDir, fnName) {
|
|
|
4148
4377
|
|
|
4149
4378
|
export * from '@/lib/types/index';
|
|
4150
4379
|
`;
|
|
4151
|
-
writeFileSync16(
|
|
4380
|
+
writeFileSync16(join24(fnDir, "src/types.ts"), content);
|
|
4152
4381
|
}
|
|
4153
4382
|
|
|
4154
4383
|
// src/commands/generate/generators/structure.ts
|
|
@@ -4170,7 +4399,7 @@ async function generateFunctionStructure(options) {
|
|
|
4170
4399
|
"src/client/store",
|
|
4171
4400
|
"src/client/components"
|
|
4172
4401
|
];
|
|
4173
|
-
dirs.forEach((dir) => mkdirSync5(
|
|
4402
|
+
dirs.forEach((dir) => mkdirSync5(join25(fnDir, dir), { recursive: true }));
|
|
4174
4403
|
generatePackageJson(fnDir, scope, fnName, description);
|
|
4175
4404
|
generateTsConfig(fnDir);
|
|
4176
4405
|
generateTsupConfig(fnDir);
|
|
@@ -4190,15 +4419,15 @@ async function generateFunctionStructure(options) {
|
|
|
4190
4419
|
generateEntitiesIndex(fnDir, entities);
|
|
4191
4420
|
generateRepositoriesIndex(fnDir, entities);
|
|
4192
4421
|
} else {
|
|
4193
|
-
writeFileSync17(
|
|
4194
|
-
writeFileSync17(
|
|
4195
|
-
}
|
|
4196
|
-
writeFileSync17(
|
|
4197
|
-
writeFileSync17(
|
|
4198
|
-
writeFileSync17(
|
|
4199
|
-
writeFileSync17(
|
|
4200
|
-
writeFileSync17(
|
|
4201
|
-
writeFileSync17(
|
|
4422
|
+
writeFileSync17(join25(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
|
|
4423
|
+
writeFileSync17(join25(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
|
|
4424
|
+
}
|
|
4425
|
+
writeFileSync17(join25(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
|
|
4426
|
+
writeFileSync17(join25(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
|
|
4427
|
+
writeFileSync17(join25(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
|
|
4428
|
+
writeFileSync17(join25(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
|
|
4429
|
+
writeFileSync17(join25(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
|
|
4430
|
+
writeFileSync17(join25(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
|
|
4202
4431
|
generateMainIndex(fnDir, fnName);
|
|
4203
4432
|
generateServerIndex(fnDir);
|
|
4204
4433
|
generateClientIndex(fnDir);
|
|
@@ -4222,8 +4451,8 @@ async function generateFunction(name, options) {
|
|
|
4222
4451
|
logger.error("Function name is required");
|
|
4223
4452
|
process.exit(1);
|
|
4224
4453
|
}
|
|
4225
|
-
const fnDir =
|
|
4226
|
-
if (
|
|
4454
|
+
const fnDir = join26(cwd, fnName);
|
|
4455
|
+
if (existsSync23(fnDir)) {
|
|
4227
4456
|
logger.error(`Directory ${fnName} already exists at ${fnDir}`);
|
|
4228
4457
|
process.exit(1);
|
|
4229
4458
|
}
|
|
@@ -4288,13 +4517,29 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
|
|
|
4288
4517
|
// src/commands/env.ts
|
|
4289
4518
|
import { Command as Command12 } from "commander";
|
|
4290
4519
|
import chalk26 from "chalk";
|
|
4291
|
-
import { existsSync as
|
|
4520
|
+
import { existsSync as existsSync24, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
4292
4521
|
import { resolve } from "path";
|
|
4293
4522
|
import { parse } from "dotenv";
|
|
4294
|
-
var
|
|
4523
|
+
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
4524
|
+
var BASE_ENV_FILES = {
|
|
4295
4525
|
nextjs: [".env", ".env.local"],
|
|
4296
4526
|
server: [".env.server", ".env.server.local"]
|
|
4297
4527
|
};
|
|
4528
|
+
function getEnvFilesForEnvironment(nodeEnv) {
|
|
4529
|
+
const files = [".env"];
|
|
4530
|
+
if (nodeEnv) {
|
|
4531
|
+
files.push(`.env.${nodeEnv}`);
|
|
4532
|
+
}
|
|
4533
|
+
if (nodeEnv !== "test") {
|
|
4534
|
+
files.push(".env.local");
|
|
4535
|
+
}
|
|
4536
|
+
if (nodeEnv) {
|
|
4537
|
+
files.push(`.env.${nodeEnv}.local`);
|
|
4538
|
+
}
|
|
4539
|
+
files.push(".env.server");
|
|
4540
|
+
files.push(".env.server.local");
|
|
4541
|
+
return files;
|
|
4542
|
+
}
|
|
4298
4543
|
function getTargetFile(schema) {
|
|
4299
4544
|
const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
|
|
4300
4545
|
if (isNextjs) {
|
|
@@ -4327,8 +4572,7 @@ function formatType(type) {
|
|
|
4327
4572
|
enum: chalk26.magenta,
|
|
4328
4573
|
json: chalk26.red
|
|
4329
4574
|
};
|
|
4330
|
-
|
|
4331
|
-
return colorFn(type);
|
|
4575
|
+
return (typeColors[type] || chalk26.white)(type);
|
|
4332
4576
|
}
|
|
4333
4577
|
function formatDefault(value, type) {
|
|
4334
4578
|
if (value === void 0) {
|
|
@@ -4489,8 +4733,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
|
|
|
4489
4733
|
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
4734
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4491
4735
|
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);
|
|
4736
|
+
function validateEnvOption(envValue) {
|
|
4737
|
+
if (!VALID_ENVS.includes(envValue)) {
|
|
4738
|
+
console.error(chalk26.red(`
|
|
4739
|
+
\u274C Invalid environment: "${envValue}"`));
|
|
4740
|
+
console.log(chalk26.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
4741
|
+
`));
|
|
4742
|
+
process.exit(1);
|
|
4743
|
+
}
|
|
4744
|
+
return envValue;
|
|
4745
|
+
}
|
|
4492
4746
|
async function initEnvFiles(options) {
|
|
4493
4747
|
const packageName = options.package || "@spfn/core";
|
|
4748
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4494
4749
|
const cwd = process.cwd();
|
|
4495
4750
|
try {
|
|
4496
4751
|
const envSchema = await loadEnvSchema(packageName);
|
|
@@ -4502,24 +4757,41 @@ async function initEnvFiles(options) {
|
|
|
4502
4757
|
acc[exampleFile].push([key, schema]);
|
|
4503
4758
|
return acc;
|
|
4504
4759
|
}, {});
|
|
4505
|
-
|
|
4760
|
+
if (targetEnv) {
|
|
4761
|
+
console.log(chalk26.blue.bold(`
|
|
4762
|
+
\u{1F680} Generating .env template files for ${chalk26.cyan(targetEnv)} environment
|
|
4763
|
+
`));
|
|
4764
|
+
const envSpecificFiles = {};
|
|
4765
|
+
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
4766
|
+
if (committedVars.length > 0) {
|
|
4767
|
+
envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
|
|
4768
|
+
}
|
|
4769
|
+
const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
|
|
4770
|
+
if (sensitiveVars.length > 0) {
|
|
4771
|
+
envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
|
|
4772
|
+
}
|
|
4773
|
+
const allGrouped = { ...grouped, ...envSpecificFiles };
|
|
4774
|
+
for (const [file, vars] of Object.entries(allGrouped)) {
|
|
4775
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4776
|
+
}
|
|
4777
|
+
} else {
|
|
4778
|
+
console.log(chalk26.blue.bold(`
|
|
4506
4779
|
\u{1F680} Generating .env template files
|
|
4507
4780
|
`));
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
if (existsSync23(filePath) && !options.force) {
|
|
4511
|
-
console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4512
|
-
continue;
|
|
4781
|
+
for (const [file, vars] of Object.entries(grouped)) {
|
|
4782
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4513
4783
|
}
|
|
4514
|
-
const content = generateEnvFileContent(vars);
|
|
4515
|
-
writeFileSync18(filePath, content, "utf-8");
|
|
4516
|
-
console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4517
4784
|
}
|
|
4518
4785
|
console.log(chalk26.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
4519
4786
|
console.log(chalk26.dim(" cp .env.example .env"));
|
|
4520
4787
|
console.log(chalk26.dim(" cp .env.local.example .env.local"));
|
|
4521
4788
|
console.log(chalk26.dim(" cp .env.server.example .env.server"));
|
|
4522
|
-
console.log(chalk26.dim(" cp .env.server.local.example .env.server.local
|
|
4789
|
+
console.log(chalk26.dim(" cp .env.server.local.example .env.server.local"));
|
|
4790
|
+
if (targetEnv) {
|
|
4791
|
+
console.log(chalk26.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
4792
|
+
console.log(chalk26.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
4793
|
+
}
|
|
4794
|
+
console.log("");
|
|
4523
4795
|
} catch (error) {
|
|
4524
4796
|
console.error(chalk26.red(`
|
|
4525
4797
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
@@ -4527,6 +4799,15 @@ async function initEnvFiles(options) {
|
|
|
4527
4799
|
process.exit(1);
|
|
4528
4800
|
}
|
|
4529
4801
|
}
|
|
4802
|
+
function writeEnvTemplate(cwd, file, vars, force) {
|
|
4803
|
+
const filePath = resolve(cwd, file);
|
|
4804
|
+
if (existsSync24(filePath) && !force) {
|
|
4805
|
+
console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
writeFileSync18(filePath, generateEnvFileContent(vars), "utf-8");
|
|
4809
|
+
console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4810
|
+
}
|
|
4530
4811
|
function generateEnvFileContent(vars) {
|
|
4531
4812
|
const lines = [
|
|
4532
4813
|
"# Auto-generated by spfn env init",
|
|
@@ -4554,20 +4835,22 @@ function generateEnvFileContent(vars) {
|
|
|
4554
4835
|
}
|
|
4555
4836
|
async function checkEnvFiles(options) {
|
|
4556
4837
|
const packageName = options.package || "@spfn/core";
|
|
4838
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4557
4839
|
const cwd = process.cwd();
|
|
4558
4840
|
try {
|
|
4559
4841
|
const envSchema = await loadEnvSchema(packageName);
|
|
4560
4842
|
const allVars = Object.entries(envSchema);
|
|
4843
|
+
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
4561
4844
|
console.log(chalk26.blue.bold(`
|
|
4562
|
-
\u{1F50D} Checking .env files against schema
|
|
4845
|
+
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4563
4846
|
`));
|
|
4564
|
-
const
|
|
4847
|
+
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
4565
4848
|
const loadedEnv = {};
|
|
4566
4849
|
const issues = [];
|
|
4567
4850
|
const warnings = [];
|
|
4568
|
-
for (const file of
|
|
4851
|
+
for (const file of filesToCheck) {
|
|
4569
4852
|
const filePath = resolve(cwd, file);
|
|
4570
|
-
if (!
|
|
4853
|
+
if (!existsSync24(filePath)) {
|
|
4571
4854
|
continue;
|
|
4572
4855
|
}
|
|
4573
4856
|
const content = readFileSync8(filePath, "utf-8");
|
|
@@ -4587,8 +4870,8 @@ async function checkEnvFiles(options) {
|
|
|
4587
4870
|
}
|
|
4588
4871
|
continue;
|
|
4589
4872
|
}
|
|
4590
|
-
const isNextjsFile =
|
|
4591
|
-
const isServerFile =
|
|
4873
|
+
const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
|
|
4874
|
+
const isServerFile = BASE_ENV_FILES.server.includes(found.file);
|
|
4592
4875
|
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
4593
4876
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4594
4877
|
if (schema.sensitive) {
|
|
@@ -4638,12 +4921,91 @@ async function checkEnvFiles(options) {
|
|
|
4638
4921
|
process.exit(1);
|
|
4639
4922
|
}
|
|
4640
4923
|
}
|
|
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);
|
|
4924
|
+
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);
|
|
4925
|
+
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);
|
|
4926
|
+
async function validateEnvVars(options) {
|
|
4927
|
+
const packages = options.packages || ["@spfn/core"];
|
|
4928
|
+
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4929
|
+
if (targetEnv) {
|
|
4930
|
+
const { loadEnv: loadEnv6 } = await import("@spfn/core/env/loader");
|
|
4931
|
+
const result = loadEnv6({ nodeEnv: targetEnv });
|
|
4932
|
+
console.log(chalk26.blue.bold(`
|
|
4933
|
+
\u{1F50D} Validating environment variables for ${chalk26.cyan(targetEnv)}
|
|
4934
|
+
`));
|
|
4935
|
+
if (result.loadedFiles.length > 0) {
|
|
4936
|
+
console.log(chalk26.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
4937
|
+
}
|
|
4938
|
+
console.log("");
|
|
4939
|
+
} else {
|
|
4940
|
+
console.log(chalk26.blue.bold(`
|
|
4941
|
+
\u{1F50D} Validating environment variables
|
|
4942
|
+
`));
|
|
4943
|
+
}
|
|
4944
|
+
const allErrors = [];
|
|
4945
|
+
const allWarnings = [];
|
|
4946
|
+
for (const packageName of packages) {
|
|
4947
|
+
try {
|
|
4948
|
+
console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
|
|
4949
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4950
|
+
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4951
|
+
const registry = createEnvRegistry(envSchema);
|
|
4952
|
+
const result = registry.validateAll();
|
|
4953
|
+
for (const error of result.errors) {
|
|
4954
|
+
allErrors.push({ ...error, package: packageName });
|
|
4955
|
+
}
|
|
4956
|
+
for (const warning of result.warnings) {
|
|
4957
|
+
allWarnings.push({ ...warning, package: packageName });
|
|
4958
|
+
}
|
|
4959
|
+
} catch (error) {
|
|
4960
|
+
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4961
|
+
console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4962
|
+
continue;
|
|
4963
|
+
}
|
|
4964
|
+
console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4965
|
+
if (options.strict) {
|
|
4966
|
+
process.exit(1);
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
console.log("");
|
|
4971
|
+
if (allErrors.length > 0) {
|
|
4972
|
+
console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4973
|
+
`));
|
|
4974
|
+
for (const error of allErrors) {
|
|
4975
|
+
console.log(` ${chalk26.red("\u2717")} ${chalk26.cyan(error.key)}`);
|
|
4976
|
+
console.log(` ${chalk26.dim(error.message)}`);
|
|
4977
|
+
console.log(` ${chalk26.dim(`from ${error.package}`)}`);
|
|
4978
|
+
console.log("");
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
if (allWarnings.length > 0) {
|
|
4982
|
+
console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4983
|
+
`));
|
|
4984
|
+
for (const warning of allWarnings) {
|
|
4985
|
+
console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
|
|
4986
|
+
console.log(` ${chalk26.dim(warning.message)}`);
|
|
4987
|
+
console.log("");
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4991
|
+
console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4992
|
+
} else if (allErrors.length === 0) {
|
|
4993
|
+
console.log(chalk26.green("\u2705 No errors found."));
|
|
4994
|
+
console.log(chalk26.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4995
|
+
`));
|
|
4996
|
+
} else {
|
|
4997
|
+
console.log(chalk26.red(`
|
|
4998
|
+
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4999
|
+
`));
|
|
5000
|
+
process.exit(1);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
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);
|
|
4643
5004
|
|
|
4644
5005
|
// src/index.ts
|
|
5006
|
+
init_version();
|
|
4645
5007
|
var program = new Command13();
|
|
4646
|
-
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(
|
|
5008
|
+
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
4647
5009
|
program.addCommand(createCommand);
|
|
4648
5010
|
program.addCommand(initCommand);
|
|
4649
5011
|
program.addCommand(addCommand);
|