spfn 0.1.0-alpha.88 → 0.2.0-beta.10
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 +2444 -967
- package/dist/templates/config/index.ts +22 -0
- package/dist/templates/config/schema.ts +42 -0
- package/dist/templates/lib/api-client.ts +80 -0
- package/dist/templates/server/config/env.config.ts +123 -0
- package/dist/templates/server/entities/config.ts +1 -0
- package/dist/templates/server/entities/example.entity.ts +12 -0
- package/dist/templates/server/repositories/example.repository.ts +40 -0
- package/dist/templates/server/router.ts +29 -0
- package/dist/templates/server/routes/examples.ts +140 -0
- package/dist/templates/server/routes/health.ts +18 -0
- package/dist/templates/server/routes/root.ts +23 -0
- package/dist/templates/server/server.config.ts +11 -0
- package/dist/templates/server/tsconfig.json +5 -3
- package/package.json +4 -4
- package/dist/chunk-526QKBO7.js +0 -387
- package/dist/chunk-K5K5BTB7.js +0 -293
- package/dist/chunk-QH74KQEW.js +0 -60
- package/dist/db-sync-NBLWUZMH.js +0 -122
- package/dist/function-migrations-AXX6HWXL.js +0 -72
- package/dist/init-EKWKKNUL.js +0 -9
- package/dist/setup-JA2ADEQ6.js +0 -9
- package/dist/templates/lib/contracts/examples.ts +0 -112
- package/dist/templates/lib/contracts/health.ts +0 -17
- package/dist/templates/lib/contracts/index.ts +0 -23
- package/dist/templates/server/routes/examples/index.ts +0 -113
- package/dist/templates/server/routes/health/index.ts +0 -21
- package/dist/templates/server/routes/index/index.ts +0 -27
- package/dist/templates/server/tsup.config.ts +0 -21
package/dist/index.js
CHANGED
|
@@ -1,36 +1,1085 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "./chunk-QH74KQEW.js";
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
11
10
|
|
|
12
|
-
// src/
|
|
13
|
-
import
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
var logger;
|
|
14
|
+
var init_logger = __esm({
|
|
15
|
+
"src/utils/logger.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
logger = {
|
|
18
|
+
info: (message) => {
|
|
19
|
+
console.log(chalk.blue("\u2139"), message);
|
|
20
|
+
},
|
|
21
|
+
success: (message) => {
|
|
22
|
+
console.log(chalk.green("\u2713"), message);
|
|
23
|
+
},
|
|
24
|
+
warn: (message) => {
|
|
25
|
+
console.log(chalk.yellow("\u26A0"), message);
|
|
26
|
+
},
|
|
27
|
+
error: (message) => {
|
|
28
|
+
console.log(chalk.red("\u2717"), message);
|
|
29
|
+
},
|
|
30
|
+
step: (message) => {
|
|
31
|
+
console.log(chalk.cyan("\u25B8"), message);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
14
36
|
|
|
15
|
-
// src/
|
|
16
|
-
import { Command } from "commander";
|
|
37
|
+
// src/utils/package-manager.ts
|
|
17
38
|
import { existsSync } from "fs";
|
|
18
39
|
import { join } from "path";
|
|
19
|
-
|
|
40
|
+
function detectPackageManager(cwd) {
|
|
41
|
+
if (existsSync(join(cwd, "bun.lockb"))) {
|
|
42
|
+
return "bun";
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
45
|
+
return "pnpm";
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
48
|
+
return "yarn";
|
|
49
|
+
}
|
|
50
|
+
let currentDir = cwd;
|
|
51
|
+
let depth = 0;
|
|
52
|
+
const maxDepth = 5;
|
|
53
|
+
while (depth < maxDepth) {
|
|
54
|
+
const parentDir = join(currentDir, "..");
|
|
55
|
+
if (parentDir === currentDir) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (existsSync(join(parentDir, "pnpm-lock.yaml"))) {
|
|
59
|
+
return "pnpm";
|
|
60
|
+
}
|
|
61
|
+
if (existsSync(join(parentDir, "yarn.lock"))) {
|
|
62
|
+
return "yarn";
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(join(parentDir, "bun.lockb"))) {
|
|
65
|
+
return "bun";
|
|
66
|
+
}
|
|
67
|
+
currentDir = parentDir;
|
|
68
|
+
depth++;
|
|
69
|
+
}
|
|
70
|
+
return "npm";
|
|
71
|
+
}
|
|
72
|
+
var init_package_manager = __esm({
|
|
73
|
+
"src/utils/package-manager.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// src/commands/setup.ts
|
|
79
|
+
var setup_exports = {};
|
|
80
|
+
__export(setup_exports, {
|
|
81
|
+
setupCommand: () => setupCommand,
|
|
82
|
+
setupIcons: () => setupIcons
|
|
83
|
+
});
|
|
84
|
+
import { Command } from "commander";
|
|
85
|
+
import { existsSync as existsSync2 } from "fs";
|
|
86
|
+
import { join as join2 } from "path";
|
|
20
87
|
import ora from "ora";
|
|
21
88
|
import { execa } from "execa";
|
|
22
|
-
import
|
|
89
|
+
import fse from "fs-extra";
|
|
90
|
+
import chalk2 from "chalk";
|
|
91
|
+
async function setupIcons() {
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
logger.info("Setting up SVGR for SVG icon management...\n");
|
|
94
|
+
const packageJsonPath = join2(cwd, "package.json");
|
|
95
|
+
if (!existsSync2(packageJsonPath)) {
|
|
96
|
+
logger.error("No package.json found. Please run this in a Next.js project.");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const packageJson = JSON.parse(
|
|
100
|
+
readFileSync(packageJsonPath, "utf-8")
|
|
101
|
+
);
|
|
102
|
+
const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
|
|
103
|
+
if (!hasNext) {
|
|
104
|
+
logger.error("Next.js not detected in dependencies. This setup is for Next.js projects only.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const hasSvgr = packageJson.devDependencies?.["@svgr/webpack"];
|
|
108
|
+
if (hasSvgr) {
|
|
109
|
+
logger.warn("@svgr/webpack is already installed.");
|
|
110
|
+
logger.info("Skipping installation, but will create directory structure...\n");
|
|
111
|
+
}
|
|
112
|
+
if (!hasSvgr) {
|
|
113
|
+
const pm = detectPackageManager(cwd);
|
|
114
|
+
logger.step(`Detected package manager: ${pm}`);
|
|
115
|
+
const spinner2 = ora("Installing @svgr/webpack...").start();
|
|
116
|
+
try {
|
|
117
|
+
await execa(
|
|
118
|
+
pm,
|
|
119
|
+
pm === "npm" ? ["install", "--save-dev", "@svgr/webpack"] : ["add", "-D", "@svgr/webpack"],
|
|
120
|
+
{ cwd }
|
|
121
|
+
);
|
|
122
|
+
spinner2.succeed("@svgr/webpack installed");
|
|
123
|
+
} catch (error) {
|
|
124
|
+
spinner2.fail("Failed to install @svgr/webpack");
|
|
125
|
+
logger.error(String(error));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const spinner = ora("Updating next.config...").start();
|
|
130
|
+
try {
|
|
131
|
+
const possibleConfigs = [
|
|
132
|
+
"next.config.ts",
|
|
133
|
+
"next.config.js",
|
|
134
|
+
"next.config.mjs"
|
|
135
|
+
];
|
|
136
|
+
let configPath = null;
|
|
137
|
+
for (const config of possibleConfigs) {
|
|
138
|
+
const path5 = join2(cwd, config);
|
|
139
|
+
if (existsSync2(path5)) {
|
|
140
|
+
configPath = path5;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!configPath) {
|
|
145
|
+
spinner.warn("next.config not found, creating next.config.ts...");
|
|
146
|
+
configPath = join2(cwd, "next.config.ts");
|
|
147
|
+
const newConfig = `import type { NextConfig } from "next";
|
|
148
|
+
|
|
149
|
+
const nextConfig: NextConfig = {
|
|
150
|
+
webpack(config) {
|
|
151
|
+
// SVGR: Import SVG as React components
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
const fileLoaderRule = (config.module.rules as any[])
|
|
154
|
+
.find((rule: any) => Array.isArray(rule.oneOf))
|
|
155
|
+
?.oneOf.find((rule: any) => rule.test?.test?.('.svg'));
|
|
156
|
+
|
|
157
|
+
if (fileLoaderRule) {
|
|
158
|
+
fileLoaderRule.exclude = /\\.svg$/i;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
config.module.rules.unshift({
|
|
162
|
+
test: /\\.svg$/i,
|
|
163
|
+
use: ['@svgr/webpack'],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return config;
|
|
167
|
+
},
|
|
168
|
+
turbopack: {
|
|
169
|
+
rules: {
|
|
170
|
+
'*.svg': {
|
|
171
|
+
loaders: ['@svgr/webpack'],
|
|
172
|
+
as: '*.js',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default nextConfig;
|
|
179
|
+
`;
|
|
180
|
+
writeFileSync(configPath, newConfig);
|
|
181
|
+
spinner.succeed("Created next.config.ts with SVGR support");
|
|
182
|
+
} else {
|
|
183
|
+
let configContent = readFileSync(configPath, "utf-8");
|
|
184
|
+
if (configContent.includes("@svgr/webpack")) {
|
|
185
|
+
spinner.warn("SVGR already configured in next.config");
|
|
186
|
+
} else {
|
|
187
|
+
const hasWebpack = configContent.includes("webpack(");
|
|
188
|
+
const hasTurbopack = configContent.includes("turbopack:");
|
|
189
|
+
if (hasWebpack || hasTurbopack) {
|
|
190
|
+
spinner.info("Manual update required for next.config");
|
|
191
|
+
logger.warn("\nYou need to manually add SVGR configuration to your next.config file.");
|
|
192
|
+
logger.info("See: https://react-svgr.com/docs/next/");
|
|
193
|
+
logger.info("\nAdd this to your next.config:\n");
|
|
194
|
+
console.log(chalk2.gray(`
|
|
195
|
+
webpack(config) {
|
|
196
|
+
const fileLoaderRule = config.module.rules
|
|
197
|
+
.find(rule => rule.oneOf)
|
|
198
|
+
?.oneOf.find(rule => rule.test?.test?.('.svg'));
|
|
199
|
+
|
|
200
|
+
if (fileLoaderRule) {
|
|
201
|
+
fileLoaderRule.exclude = /\\.svg$/i;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
config.module.rules.unshift({
|
|
205
|
+
test: /\\.svg$/i,
|
|
206
|
+
use: ['@svgr/webpack'],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return config;
|
|
210
|
+
},
|
|
211
|
+
turbopack: {
|
|
212
|
+
rules: {
|
|
213
|
+
'*.svg': {
|
|
214
|
+
loaders: ['@svgr/webpack'],
|
|
215
|
+
as: '*.js',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
`));
|
|
220
|
+
} else {
|
|
221
|
+
const webpackConfig = ` webpack(config) {
|
|
222
|
+
// SVGR: Import SVG as React components
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
+
const fileLoaderRule = (config.module.rules as any[])
|
|
225
|
+
.find((rule: any) => Array.isArray(rule.oneOf))
|
|
226
|
+
?.oneOf.find((rule: any) => rule.test?.test?.('.svg'));
|
|
227
|
+
|
|
228
|
+
if (fileLoaderRule) {
|
|
229
|
+
fileLoaderRule.exclude = /\\.svg$/i;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
config.module.rules.unshift({
|
|
233
|
+
test: /\\.svg$/i,
|
|
234
|
+
use: ['@svgr/webpack'],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return config;
|
|
238
|
+
},`;
|
|
239
|
+
const turbopackConfig = ` turbopack: {
|
|
240
|
+
rules: {
|
|
241
|
+
'*.svg': {
|
|
242
|
+
loaders: ['@svgr/webpack'],
|
|
243
|
+
as: '*.js',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},`;
|
|
247
|
+
const emptyConfigPattern = /const\s+\w+:\s*NextConfig\s*=\s*\{\s*\};/;
|
|
248
|
+
if (emptyConfigPattern.test(configContent)) {
|
|
249
|
+
configContent = configContent.replace(
|
|
250
|
+
emptyConfigPattern,
|
|
251
|
+
`const nextConfig: NextConfig = {
|
|
252
|
+
${webpackConfig}
|
|
253
|
+
${turbopackConfig}
|
|
254
|
+
};`
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
const configObjectPattern = /(const\s+\w+:\s*NextConfig\s*=\s*\{)([^}]*?)(\};)/s;
|
|
258
|
+
if (configObjectPattern.test(configContent)) {
|
|
259
|
+
configContent = configContent.replace(
|
|
260
|
+
configObjectPattern,
|
|
261
|
+
(_match, opening, content, closing) => {
|
|
262
|
+
const trimmedContent = content.trim();
|
|
263
|
+
if (trimmedContent) {
|
|
264
|
+
return `${opening}${content}
|
|
265
|
+
${webpackConfig}
|
|
266
|
+
${turbopackConfig}
|
|
267
|
+
${closing}`;
|
|
268
|
+
} else {
|
|
269
|
+
return `${opening}
|
|
270
|
+
${webpackConfig}
|
|
271
|
+
${turbopackConfig}
|
|
272
|
+
${closing}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
writeFileSync(configPath, configContent);
|
|
279
|
+
spinner.succeed("Added SVGR configuration to next.config");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
spinner.fail("Failed to update next.config");
|
|
285
|
+
logger.error(String(error));
|
|
286
|
+
}
|
|
287
|
+
const iconsSpinner = ora("Creating src/assets/icons/ directory...").start();
|
|
288
|
+
try {
|
|
289
|
+
const iconsDir = join2(cwd, "src", "assets", "icons");
|
|
290
|
+
ensureDirSync(iconsDir);
|
|
291
|
+
const readmePath = join2(iconsDir, "README.md");
|
|
292
|
+
const readmeContent = `# Icons
|
|
293
|
+
|
|
294
|
+
This directory manages SVG icons for the project.
|
|
295
|
+
|
|
296
|
+
## Usage
|
|
297
|
+
|
|
298
|
+
Import SVG files as React components using SVGR:
|
|
299
|
+
|
|
300
|
+
\`\`\`tsx
|
|
301
|
+
import Logo from '@/assets/icons/logo.svg';
|
|
302
|
+
|
|
303
|
+
function MyComponent() {
|
|
304
|
+
return (
|
|
305
|
+
<Logo className="size-8 text-gray-900 dark:text-white" />
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
## Color Control
|
|
311
|
+
|
|
312
|
+
Use \`fill="currentColor"\` in your SVG files to control colors via Tailwind CSS:
|
|
313
|
+
|
|
314
|
+
\`\`\`tsx
|
|
315
|
+
// Light mode: gray-900, Dark mode: white
|
|
316
|
+
<Logo className="size-8 text-gray-900 dark:text-white" />
|
|
317
|
+
|
|
318
|
+
// Custom colors
|
|
319
|
+
<Icon className="size-6 text-blue-600" />
|
|
320
|
+
\`\`\`
|
|
321
|
+
|
|
322
|
+
## Adding New Icons
|
|
323
|
+
|
|
324
|
+
1. Add SVG file to this directory
|
|
325
|
+
2. Set \`fill="currentColor"\` for color control (optional)
|
|
326
|
+
3. Remove \`width\` and \`height\` attributes for flexible sizing
|
|
327
|
+
4. Import and use as React component
|
|
328
|
+
|
|
329
|
+
\`\`\`tsx
|
|
330
|
+
import NewIcon from '@/assets/icons/new-icon.svg';
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
## Configuration
|
|
334
|
+
|
|
335
|
+
- **next.config.ts**: SVGR webpack loader configuration
|
|
336
|
+
- **Turbopack**: SVG loader rules for fast refresh
|
|
337
|
+
|
|
338
|
+
## Example SVG Structure
|
|
339
|
+
|
|
340
|
+
\`\`\`xml
|
|
341
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
342
|
+
<path d="..." fill="currentColor"/>
|
|
343
|
+
</svg>
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
Note: Remove \`width\` and \`height\` for flexible sizing with Tailwind utilities.
|
|
347
|
+
`;
|
|
348
|
+
writeFileSync(readmePath, readmeContent);
|
|
349
|
+
iconsSpinner.succeed("Created src/assets/icons/ directory with README.md");
|
|
350
|
+
} catch (error) {
|
|
351
|
+
iconsSpinner.fail("Failed to create directory structure");
|
|
352
|
+
logger.error(String(error));
|
|
353
|
+
}
|
|
354
|
+
console.log("\n" + chalk2.green.bold("\u2713 SVGR setup completed!\n"));
|
|
355
|
+
console.log("Next steps:");
|
|
356
|
+
console.log(" 1. Add SVG files to " + chalk2.cyan("src/assets/icons/"));
|
|
357
|
+
console.log(" 2. Import them as React components:");
|
|
358
|
+
console.log(" " + chalk2.gray("import Logo from '@/assets/icons/logo.svg';"));
|
|
359
|
+
console.log(" 3. Use with Tailwind classes:");
|
|
360
|
+
console.log(" " + chalk2.gray('<Logo className="size-8 text-gray-900 dark:text-white" />'));
|
|
361
|
+
console.log("\nDocumentation: " + chalk2.cyan("src/assets/icons/README.md"));
|
|
362
|
+
}
|
|
363
|
+
var ensureDirSync, writeFileSync, readFileSync, setupCommand;
|
|
364
|
+
var init_setup = __esm({
|
|
365
|
+
"src/commands/setup.ts"() {
|
|
366
|
+
"use strict";
|
|
367
|
+
init_logger();
|
|
368
|
+
init_package_manager();
|
|
369
|
+
({ ensureDirSync, writeFileSync, readFileSync } = fse);
|
|
370
|
+
setupCommand = new Command("setup").description("Setup additional features for your project");
|
|
371
|
+
setupCommand.command("icons").description("Setup SVGR for SVG icon management").action(setupIcons);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// src/commands/init/steps/validate.ts
|
|
376
|
+
import { existsSync as existsSync3 } from "fs";
|
|
377
|
+
import { join as join3 } from "path";
|
|
378
|
+
import prompts from "prompts";
|
|
379
|
+
async function validateProject(cwd, skipPrompts) {
|
|
380
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
381
|
+
if (!existsSync3(packageJsonPath)) {
|
|
382
|
+
logger.error("No package.json found. Please run this in a Next.js project.");
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
const packageJson = JSON.parse(await import("fs").then(
|
|
386
|
+
(fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
|
|
387
|
+
));
|
|
388
|
+
const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
|
|
389
|
+
if (!hasNext) {
|
|
390
|
+
logger.warn("Next.js not detected in dependencies.");
|
|
391
|
+
if (!skipPrompts) {
|
|
392
|
+
const { proceed } = await prompts(
|
|
393
|
+
{
|
|
394
|
+
type: "confirm",
|
|
395
|
+
name: "proceed",
|
|
396
|
+
message: "Continue anyway?",
|
|
397
|
+
initial: false
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
if (!proceed) {
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
logger.info("Initializing SPFN in your Next.js project...\n");
|
|
406
|
+
if (existsSync3(join3(cwd, "src", "server"))) {
|
|
407
|
+
logger.warn("src/server directory already exists.");
|
|
408
|
+
if (!skipPrompts) {
|
|
409
|
+
const { overwrite } = await prompts(
|
|
410
|
+
{
|
|
411
|
+
type: "confirm",
|
|
412
|
+
name: "overwrite",
|
|
413
|
+
message: "Overwrite existing files?",
|
|
414
|
+
initial: false
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
if (!overwrite) {
|
|
418
|
+
logger.info("Cancelled.");
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
let includeAuth = false;
|
|
424
|
+
if (!skipPrompts) {
|
|
425
|
+
const { auth } = await prompts(
|
|
426
|
+
{
|
|
427
|
+
type: "confirm",
|
|
428
|
+
name: "auth",
|
|
429
|
+
message: "Include authentication (@spfn/auth)?",
|
|
430
|
+
initial: true
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
includeAuth = auth;
|
|
434
|
+
}
|
|
435
|
+
return { packageJson, packageJsonPath, includeAuth };
|
|
436
|
+
}
|
|
437
|
+
var init_validate = __esm({
|
|
438
|
+
"src/commands/init/steps/validate.ts"() {
|
|
439
|
+
"use strict";
|
|
440
|
+
init_logger();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// src/commands/init/utils/templates.ts
|
|
445
|
+
import { existsSync as existsSync4 } from "fs";
|
|
446
|
+
import { join as join4, dirname } from "path";
|
|
447
|
+
import { fileURLToPath } from "url";
|
|
448
|
+
function findTemplatesPath() {
|
|
449
|
+
const bundledPath = join4(__dirname, "templates");
|
|
450
|
+
if (existsSync4(bundledPath)) {
|
|
451
|
+
return bundledPath;
|
|
452
|
+
}
|
|
453
|
+
const npmPath = join4(__dirname, "..", "..", "templates");
|
|
454
|
+
if (existsSync4(npmPath)) {
|
|
455
|
+
return npmPath;
|
|
456
|
+
}
|
|
457
|
+
const devPath = join4(__dirname, "..", "..", "..", "templates");
|
|
458
|
+
if (existsSync4(devPath)) {
|
|
459
|
+
return devPath;
|
|
460
|
+
}
|
|
461
|
+
throw new Error("Templates directory not found. Please rebuild the package.");
|
|
462
|
+
}
|
|
463
|
+
var __dirname;
|
|
464
|
+
var init_templates = __esm({
|
|
465
|
+
"src/commands/init/utils/templates.ts"() {
|
|
466
|
+
"use strict";
|
|
467
|
+
__dirname = dirname(fileURLToPath(import.meta.url));
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/commands/init/steps/server-structure.ts
|
|
472
|
+
import { existsSync as existsSync5 } from "fs";
|
|
473
|
+
import { join as join5 } from "path";
|
|
474
|
+
import ora2 from "ora";
|
|
475
|
+
import fse2 from "fs-extra";
|
|
476
|
+
async function setupServerStructure(cwd) {
|
|
477
|
+
const spinner = ora2("Setting up server structure...").start();
|
|
478
|
+
try {
|
|
479
|
+
const templatesDir = findTemplatesPath();
|
|
480
|
+
const serverTemplateDir = join5(templatesDir, "server");
|
|
481
|
+
const targetDir = join5(cwd, "src", "server");
|
|
482
|
+
if (!existsSync5(serverTemplateDir)) {
|
|
483
|
+
spinner.fail("Failed to create server structure");
|
|
484
|
+
logger.error(`Server templates not found at: ${serverTemplateDir}`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
ensureDirSync2(targetDir);
|
|
488
|
+
copySync(serverTemplateDir, targetDir);
|
|
489
|
+
const libTemplateDir = join5(templatesDir, "lib");
|
|
490
|
+
const libTargetDir = join5(cwd, "src", "lib");
|
|
491
|
+
if (existsSync5(libTemplateDir)) {
|
|
492
|
+
ensureDirSync2(libTargetDir);
|
|
493
|
+
copySync(libTemplateDir, libTargetDir);
|
|
494
|
+
}
|
|
495
|
+
const envConfigTemplate = join5(serverTemplateDir, "config", "env.config.ts");
|
|
496
|
+
const envConfigTarget = join5(targetDir, "config", "env.config.ts");
|
|
497
|
+
if (existsSync5(envConfigTemplate)) {
|
|
498
|
+
ensureDirSync2(join5(targetDir, "config"));
|
|
499
|
+
copySync(envConfigTemplate, envConfigTarget);
|
|
500
|
+
logger.success("Created src/server/config/env.config.ts (environment management)");
|
|
501
|
+
}
|
|
502
|
+
spinner.succeed("Server structure created");
|
|
503
|
+
} catch (error) {
|
|
504
|
+
spinner.fail("Failed to create server structure");
|
|
505
|
+
logger.error(String(error));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
var copySync, ensureDirSync2;
|
|
510
|
+
var init_server_structure = __esm({
|
|
511
|
+
"src/commands/init/steps/server-structure.ts"() {
|
|
512
|
+
"use strict";
|
|
513
|
+
init_logger();
|
|
514
|
+
init_templates();
|
|
515
|
+
({ copySync, ensureDirSync: ensureDirSync2 } = fse2);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// src/commands/init/steps/api-proxy.ts
|
|
520
|
+
import { existsSync as existsSync6 } from "fs";
|
|
521
|
+
import { join as join6 } from "path";
|
|
522
|
+
import fse3 from "fs-extra";
|
|
523
|
+
async function setupApiProxy(cwd, includeAuth) {
|
|
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
|
+
}
|
|
535
|
+
const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
|
|
536
|
+
const rpcRoutePath = join6(rpcDir, "route.ts");
|
|
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';
|
|
543
|
+
` : "";
|
|
544
|
+
const routeContent = `/**
|
|
545
|
+
* SPFN RPC Proxy
|
|
546
|
+
*
|
|
547
|
+
* Resolves routeName to actual HTTP method and path from routeMap,
|
|
548
|
+
* then forwards requests to SPFN API server with automatic:
|
|
549
|
+
* - Cookie forwarding
|
|
550
|
+
* - Interceptor execution
|
|
551
|
+
* - Header manipulation
|
|
552
|
+
*
|
|
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.
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
${authImport}import { routeMap } from '@/generated/route-map';
|
|
558
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
559
|
+
|
|
560
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
561
|
+
`;
|
|
562
|
+
writeFileSync2(rpcRoutePath, routeContent);
|
|
563
|
+
const relativePath = rpcRoutePath.replace(cwd + "/", "");
|
|
564
|
+
logger.success(`Created ${relativePath} (RPC proxy)`);
|
|
565
|
+
}
|
|
566
|
+
var ensureDirSync3, writeFileSync2;
|
|
567
|
+
var init_api_proxy = __esm({
|
|
568
|
+
"src/commands/init/steps/api-proxy.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
init_logger();
|
|
571
|
+
({ ensureDirSync: ensureDirSync3, writeFileSync: writeFileSync2 } = fse3);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// src/commands/init/steps/docker.ts
|
|
576
|
+
import { existsSync as existsSync7 } from "fs";
|
|
577
|
+
import { join as join7 } from "path";
|
|
578
|
+
import fse4 from "fs-extra";
|
|
579
|
+
async function setupDockerFiles(cwd) {
|
|
580
|
+
const templatesDir = findTemplatesPath();
|
|
581
|
+
const dockerComposePath = join7(cwd, "docker-compose.yml");
|
|
582
|
+
if (!existsSync7(dockerComposePath)) {
|
|
583
|
+
try {
|
|
584
|
+
const dockerComposeTemplate = join7(templatesDir, "docker-compose.yml");
|
|
585
|
+
if (existsSync7(dockerComposeTemplate)) {
|
|
586
|
+
copySync2(dockerComposeTemplate, dockerComposePath);
|
|
587
|
+
logger.success("Created docker-compose.yml (PostgreSQL + Redis)");
|
|
588
|
+
}
|
|
589
|
+
} catch (error) {
|
|
590
|
+
logger.warn("Could not copy docker-compose.yml");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const dockerfilePath = join7(cwd, "Dockerfile");
|
|
595
|
+
if (!existsSync7(dockerfilePath)) {
|
|
596
|
+
const dockerfileTemplate = join7(templatesDir, "Dockerfile");
|
|
597
|
+
if (existsSync7(dockerfileTemplate)) {
|
|
598
|
+
copySync2(dockerfileTemplate, dockerfilePath);
|
|
599
|
+
logger.success("Created Dockerfile");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const dockerignorePath = join7(cwd, ".dockerignore");
|
|
603
|
+
if (!existsSync7(dockerignorePath)) {
|
|
604
|
+
const dockerignoreTemplate = join7(templatesDir, ".dockerignore");
|
|
605
|
+
if (existsSync7(dockerignoreTemplate)) {
|
|
606
|
+
copySync2(dockerignoreTemplate, dockerignorePath);
|
|
607
|
+
logger.success("Created .dockerignore");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const dockerComposeProdPath = join7(cwd, "docker-compose.production.yml");
|
|
611
|
+
if (!existsSync7(dockerComposeProdPath)) {
|
|
612
|
+
const dockerComposeProdTemplate = join7(templatesDir, "docker-compose.production.yml");
|
|
613
|
+
if (existsSync7(dockerComposeProdTemplate)) {
|
|
614
|
+
copySync2(dockerComposeProdTemplate, dockerComposeProdPath);
|
|
615
|
+
logger.success("Created docker-compose.production.yml");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
logger.warn("Could not copy Docker files (you can create them manually)");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
var copySync2;
|
|
623
|
+
var init_docker = __esm({
|
|
624
|
+
"src/commands/init/steps/docker.ts"() {
|
|
625
|
+
"use strict";
|
|
626
|
+
init_logger();
|
|
627
|
+
init_templates();
|
|
628
|
+
({ copySync: copySync2 } = fse4);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/commands/init/steps/deployment-config.ts
|
|
633
|
+
import { existsSync as existsSync8 } from "fs";
|
|
634
|
+
import { join as join8 } from "path";
|
|
635
|
+
import fse5 from "fs-extra";
|
|
636
|
+
async function setupDeploymentConfig(cwd, packageJson, packageManager) {
|
|
637
|
+
const deploymentConfigPath = join8(cwd, "spfn.config.js");
|
|
638
|
+
if (!existsSync8(deploymentConfigPath)) {
|
|
639
|
+
try {
|
|
640
|
+
const projectName = packageJson.name?.replace(/[@\/]/g, "-").toLowerCase() || cwd.split("/").pop()?.toLowerCase() || "my-app";
|
|
641
|
+
const configContent = `/**
|
|
642
|
+
* SPFN Configuration
|
|
643
|
+
*
|
|
644
|
+
* This file configures your SPFN application deployment settings.
|
|
645
|
+
*
|
|
646
|
+
* @type {import('spfn').SpfnConfig}
|
|
647
|
+
*/
|
|
648
|
+
export default {
|
|
649
|
+
/**
|
|
650
|
+
* Package manager to use for dependency installation
|
|
651
|
+
* Options: 'npm' | 'yarn' | 'pnpm' | 'bun'
|
|
652
|
+
*/
|
|
653
|
+
packageManager: '${packageManager}',
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Deployment configuration for SPFN cloud platform
|
|
657
|
+
*/
|
|
658
|
+
deployment: {
|
|
659
|
+
/**
|
|
660
|
+
* Your app's subdomain on spfn.app
|
|
661
|
+
*
|
|
662
|
+
* This will automatically create region-specific domains:
|
|
663
|
+
* - {subdomain}.{region}.spfn.app \u2192 Next.js frontend (port 3790)
|
|
664
|
+
* - api-{subdomain}.{region}.spfn.app \u2192 SPFN backend (port 8790)
|
|
665
|
+
*
|
|
666
|
+
* Example: subdomain: '${projectName}', region: 'us' creates:
|
|
667
|
+
* - ${projectName}.us.spfn.app
|
|
668
|
+
* - api-${projectName}.us.spfn.app
|
|
669
|
+
*/
|
|
670
|
+
subdomain: '${projectName}',
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Deployment region (optional, defaults to 'us')
|
|
674
|
+
*
|
|
675
|
+
* Available regions:
|
|
676
|
+
* - 'us': Virginia, USA (default)
|
|
677
|
+
* - 'kr': Seoul, South Korea
|
|
678
|
+
* - 'jp': Tokyo, Japan [Coming soon]
|
|
679
|
+
* - 'sg': Singapore [Coming soon]
|
|
680
|
+
* - 'eu': Frankfurt, Germany [Coming soon]
|
|
681
|
+
*/
|
|
682
|
+
region: 'us',
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Custom domains (optional)
|
|
686
|
+
*
|
|
687
|
+
* Add your own custom domains here. Make sure to configure DNS:
|
|
688
|
+
* - CNAME record pointing to spfn.app
|
|
689
|
+
*
|
|
690
|
+
* Example:
|
|
691
|
+
* customDomains: {
|
|
692
|
+
* nextjs: ['www.example.com', 'example.com'],
|
|
693
|
+
* spfn: ['api.example.com']
|
|
694
|
+
* }
|
|
695
|
+
*/
|
|
696
|
+
customDomains: {
|
|
697
|
+
/**
|
|
698
|
+
* Custom domains for Next.js frontend
|
|
699
|
+
*/
|
|
700
|
+
nextjs: [],
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Custom domains for SPFN backend API
|
|
704
|
+
*/
|
|
705
|
+
spfn: []
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Environment variables (optional)
|
|
710
|
+
*
|
|
711
|
+
* Most environment variables are auto-generated by the CI/CD pipeline.
|
|
712
|
+
* Only add custom values if you need to override defaults.
|
|
713
|
+
*
|
|
714
|
+
* \u{1F527} Auto-generated variables (leave env empty for defaults):
|
|
715
|
+
* - NEXT_PUBLIC_API_URL: https://api-{subdomain}.{region}.spfn.app
|
|
716
|
+
* (Used by browser/client-side code)
|
|
717
|
+
* - API_URL: http://localhost:8790
|
|
718
|
+
* (Used by Next.js SSR/API Routes - same container, internal)
|
|
719
|
+
*
|
|
720
|
+
* \u{1F4CB} When to add custom env:
|
|
721
|
+
* - Using custom API domain (not *.spfn.app)
|
|
722
|
+
* - Additional environment variables for your app
|
|
723
|
+
*
|
|
724
|
+
* \u26A0\uFE0F SECURITY WARNING:
|
|
725
|
+
* - These values are committed to Git
|
|
726
|
+
* - Do NOT put sensitive credentials here (DB passwords, API keys, etc.)
|
|
727
|
+
* - For production secrets, use your CI/CD secrets management
|
|
728
|
+
*
|
|
729
|
+
* Example (custom API domain):
|
|
730
|
+
* env: {
|
|
731
|
+
* NEXT_PUBLIC_API_URL: 'https://api.custom.com',
|
|
732
|
+
* API_URL: 'https://api.custom.com',
|
|
733
|
+
* NEXT_PUBLIC_FEATURE_FLAG: 'true'
|
|
734
|
+
* }
|
|
735
|
+
*/
|
|
736
|
+
env: {}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
`;
|
|
740
|
+
writeFileSync3(deploymentConfigPath, configContent);
|
|
741
|
+
logger.success(`Created spfn.config.js (subdomain: ${projectName}.spfn.app)`);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
logger.warn("Could not create spfn.config.js");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
var writeFileSync3;
|
|
748
|
+
var init_deployment_config = __esm({
|
|
749
|
+
"src/commands/init/steps/deployment-config.ts"() {
|
|
750
|
+
"use strict";
|
|
751
|
+
init_logger();
|
|
752
|
+
({ writeFileSync: writeFileSync3 } = fse5);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// src/utils/version.ts
|
|
757
|
+
function getCliVersion() {
|
|
758
|
+
return "0.2.0-beta.10";
|
|
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
|
+
|
|
773
|
+
// src/commands/init/steps/package.ts
|
|
774
|
+
import ora3 from "ora";
|
|
775
|
+
import { execa as execa2 } from "execa";
|
|
776
|
+
import fse6 from "fs-extra";
|
|
777
|
+
async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManager, includeAuth) {
|
|
778
|
+
const spinner = ora3("Updating package.json...").start();
|
|
779
|
+
packageJson.dependencies = packageJson.dependencies || {};
|
|
780
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
781
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
782
|
+
const spfnTag = getSpfnTag();
|
|
783
|
+
packageJson.dependencies["@spfn/core"] = spfnTag;
|
|
784
|
+
packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
|
|
785
|
+
packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
|
|
786
|
+
packageJson.dependencies["spfn"] = spfnTag;
|
|
787
|
+
packageJson.dependencies["concurrently"] = "^9.2.1";
|
|
788
|
+
if (includeAuth) {
|
|
789
|
+
packageJson.dependencies["@spfn/auth"] = spfnTag;
|
|
790
|
+
}
|
|
791
|
+
packageJson.devDependencies["@types/node"] = "^20.11.0";
|
|
792
|
+
packageJson.devDependencies["tsx"] = "^4.20.6";
|
|
793
|
+
packageJson.devDependencies["tsup"] = "^8.5.0";
|
|
794
|
+
packageJson.devDependencies["drizzle-kit"] = "^0.31.5";
|
|
795
|
+
packageJson.devDependencies["dotenv"] = "^17.2.3";
|
|
796
|
+
if (!packageJson.scripts["build"]) {
|
|
797
|
+
packageJson.scripts["build"] = "next build --turbopack";
|
|
798
|
+
}
|
|
799
|
+
if (!packageJson.scripts["start"]) {
|
|
800
|
+
packageJson.scripts["start"] = "next start";
|
|
801
|
+
}
|
|
802
|
+
packageJson.scripts["spfn:dev"] = "spfn dev";
|
|
803
|
+
packageJson.scripts["spfn:server"] = "spfn dev --server-only";
|
|
804
|
+
packageJson.scripts["spfn:next"] = "next dev --turbo --port 3790";
|
|
805
|
+
packageJson.scripts["spfn:start"] = "spfn start";
|
|
806
|
+
packageJson.scripts["spfn:build"] = "spfn build";
|
|
807
|
+
packageJson.scripts["codegen"] = "spfn codegen run";
|
|
808
|
+
writeFileSync4(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
809
|
+
spinner.succeed("package.json updated");
|
|
810
|
+
spinner.start("Installing dependencies...");
|
|
811
|
+
try {
|
|
812
|
+
const installArgs = packageManager === "npm" ? ["install", "--legacy-peer-deps"] : ["install"];
|
|
813
|
+
await execa2(packageManager, installArgs, { cwd });
|
|
814
|
+
spinner.succeed("Dependencies installed");
|
|
815
|
+
} catch (error) {
|
|
816
|
+
spinner.fail("Failed to install dependencies");
|
|
817
|
+
logger.error(String(error));
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
var writeFileSync4;
|
|
822
|
+
var init_package = __esm({
|
|
823
|
+
"src/commands/init/steps/package.ts"() {
|
|
824
|
+
"use strict";
|
|
825
|
+
init_logger();
|
|
826
|
+
init_version();
|
|
827
|
+
({ writeFileSync: writeFileSync4 } = fse6);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// src/commands/init/steps/config-files.ts
|
|
832
|
+
import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
|
|
833
|
+
import { join as join9 } from "path";
|
|
834
|
+
import fse7 from "fs-extra";
|
|
835
|
+
async function setupConfigFiles(cwd) {
|
|
836
|
+
const envExamplePath = join9(cwd, ".env.local.example");
|
|
837
|
+
if (!existsSync9(envExamplePath)) {
|
|
838
|
+
const envExampleContent = `# Environment
|
|
839
|
+
NODE_ENV=local
|
|
840
|
+
|
|
841
|
+
# Logging
|
|
842
|
+
SPFN_LOG_LEVEL=info
|
|
843
|
+
|
|
844
|
+
# Database (matches docker-compose.yml)
|
|
845
|
+
DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
|
|
846
|
+
|
|
847
|
+
# Cache - Redis/Valkey (optional)
|
|
848
|
+
CACHE_URL=redis://localhost:6379
|
|
849
|
+
|
|
850
|
+
# SPFN API Server URL (for API Route Proxy and SSR)
|
|
851
|
+
SPFN_API_URL=http://localhost:8790
|
|
852
|
+
`;
|
|
853
|
+
writeFileSync5(envExamplePath, envExampleContent);
|
|
854
|
+
logger.success("Created .env.local.example");
|
|
855
|
+
}
|
|
856
|
+
const spfnrcPath = join9(cwd, ".spfnrc.ts");
|
|
857
|
+
if (!existsSync9(spfnrcPath)) {
|
|
858
|
+
const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* SPFN Codegen Configuration
|
|
862
|
+
*
|
|
863
|
+
* Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
export default defineConfig({
|
|
867
|
+
generators: [
|
|
868
|
+
// Route map generator - generates routeName \u2192 {method, path} mappings
|
|
869
|
+
// Used by RPC proxy to resolve routes without importing server code
|
|
870
|
+
defineGenerator({
|
|
871
|
+
name: '@spfn/core:route-map',
|
|
872
|
+
routerPath: './src/server/router.ts',
|
|
873
|
+
outputPath: './src/generated/route-map.ts',
|
|
874
|
+
}),
|
|
875
|
+
]
|
|
876
|
+
});
|
|
877
|
+
`;
|
|
878
|
+
writeFileSync5(spfnrcPath, spfnrcContent);
|
|
879
|
+
logger.success("Created .spfnrc.ts (codegen configuration)");
|
|
880
|
+
}
|
|
881
|
+
const gitignorePath = join9(cwd, ".gitignore");
|
|
882
|
+
if (existsSync9(gitignorePath)) {
|
|
883
|
+
try {
|
|
884
|
+
const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
|
|
885
|
+
if (!gitignoreContent.includes(".spfn")) {
|
|
886
|
+
const updatedContent = gitignoreContent.replace(
|
|
887
|
+
/# production\n\/build/,
|
|
888
|
+
"# production\n/build\n\n# spfn\n/.spfn/"
|
|
889
|
+
);
|
|
890
|
+
writeFileSync5(gitignorePath, updatedContent);
|
|
891
|
+
logger.success("Updated .gitignore with .spfn directory");
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
logger.warn("Could not update .gitignore (you can add .spfn manually)");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const tsconfigPath = join9(cwd, "tsconfig.json");
|
|
898
|
+
if (existsSync9(tsconfigPath)) {
|
|
899
|
+
try {
|
|
900
|
+
const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
|
|
901
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
902
|
+
if (!tsconfig.exclude) {
|
|
903
|
+
tsconfig.exclude = [];
|
|
904
|
+
}
|
|
905
|
+
if (!tsconfig.exclude.includes("src/server")) {
|
|
906
|
+
tsconfig.exclude.push("src/server");
|
|
907
|
+
writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
908
|
+
logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
|
|
909
|
+
}
|
|
910
|
+
} catch (error) {
|
|
911
|
+
logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
var writeFileSync5;
|
|
916
|
+
var init_config_files = __esm({
|
|
917
|
+
"src/commands/init/steps/config-files.ts"() {
|
|
918
|
+
"use strict";
|
|
919
|
+
init_logger();
|
|
920
|
+
({ writeFileSync: writeFileSync5 } = fse7);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// src/commands/init/index.ts
|
|
925
|
+
var init_exports = {};
|
|
926
|
+
__export(init_exports, {
|
|
927
|
+
initCommand: () => initCommand,
|
|
928
|
+
initializeSpfn: () => initializeSpfn
|
|
929
|
+
});
|
|
930
|
+
import { Command as Command2 } from "commander";
|
|
931
|
+
import chalk3 from "chalk";
|
|
932
|
+
async function initializeSpfn(options = {}) {
|
|
933
|
+
const cwd = process.cwd();
|
|
934
|
+
const { packageJson, packageJsonPath, includeAuth } = await validateProject(cwd, options.yes || false);
|
|
935
|
+
const pm = detectPackageManager(cwd);
|
|
936
|
+
logger.step(`Detected package manager: ${pm}`);
|
|
937
|
+
await setupServerStructure(cwd);
|
|
938
|
+
await setupApiProxy(cwd, includeAuth);
|
|
939
|
+
await setupDockerFiles(cwd);
|
|
940
|
+
await setupDeploymentConfig(cwd, packageJson, pm);
|
|
941
|
+
await setupPackageJson(cwd, packageJsonPath, packageJson, pm, includeAuth);
|
|
942
|
+
await setupConfigFiles(cwd);
|
|
943
|
+
console.log("\n" + chalk3.green.bold("\u2713 SPFN initialized successfully!\n"));
|
|
944
|
+
console.log("Next steps:");
|
|
945
|
+
console.log(" 1. Start PostgreSQL & Redis (if not installed locally):");
|
|
946
|
+
console.log(" " + chalk3.cyan("docker compose up -d"));
|
|
947
|
+
console.log(" 2. Copy .env.local.example to .env.local");
|
|
948
|
+
console.log(" " + chalk3.cyan("cp .env.local.example .env.local"));
|
|
949
|
+
console.log(" 3. Run: " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} run spfn:dev`));
|
|
950
|
+
console.log(" 4. Visit:");
|
|
951
|
+
console.log(" - Next.js: " + chalk3.cyan("http://localhost:3790"));
|
|
952
|
+
console.log(" - API: " + chalk3.cyan("http://localhost:8790/health"));
|
|
953
|
+
console.log("\nAvailable commands:");
|
|
954
|
+
console.log(" \u2022 " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} spfn:dev`) + " - Start SPFN + Next.js");
|
|
955
|
+
console.log(" \u2022 " + chalk3.cyan("spfn env:validate") + " - Validate environment variables");
|
|
956
|
+
console.log(" \u2022 " + chalk3.cyan("spfn env:docs") + " - Generate env documentation");
|
|
957
|
+
console.log(" \u2022 " + chalk3.cyan("spfn env:check") + " - Check environment status");
|
|
958
|
+
console.log("\n" + chalk3.blue("\u{1F4A1} Tip:") + " Edit " + chalk3.cyan("src/server/config/env.config.ts") + " to manage environment variables");
|
|
959
|
+
}
|
|
960
|
+
var initCommand;
|
|
961
|
+
var init_init = __esm({
|
|
962
|
+
"src/commands/init/index.ts"() {
|
|
963
|
+
"use strict";
|
|
964
|
+
init_package_manager();
|
|
965
|
+
init_logger();
|
|
966
|
+
init_validate();
|
|
967
|
+
init_server_structure();
|
|
968
|
+
init_api_proxy();
|
|
969
|
+
init_docker();
|
|
970
|
+
init_deployment_config();
|
|
971
|
+
init_package();
|
|
972
|
+
init_config_files();
|
|
973
|
+
initCommand = new Command2("init").description("Initialize SPFN in your Next.js project").option("-y, --yes", "Skip prompts and use defaults").action(initializeSpfn);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// src/utils/function-migrations.ts
|
|
978
|
+
var function_migrations_exports = {};
|
|
979
|
+
__export(function_migrations_exports, {
|
|
980
|
+
discoverFunctionMigrations: () => discoverFunctionMigrations,
|
|
981
|
+
executeFunctionMigrations: () => executeFunctionMigrations
|
|
982
|
+
});
|
|
983
|
+
import chalk11 from "chalk";
|
|
984
|
+
import { join as join15 } from "path";
|
|
985
|
+
import { env as env2 } from "@spfn/core/config";
|
|
986
|
+
import { loadEnvFiles as loadEnvFiles2 } from "@spfn/core/server";
|
|
987
|
+
import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
988
|
+
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
989
|
+
const nodeModulesPath = join15(cwd, "node_modules");
|
|
990
|
+
if (!existsSync16(nodeModulesPath)) {
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
const functions = [];
|
|
994
|
+
const spfnDir = join15(nodeModulesPath, "@spfn");
|
|
995
|
+
if (!existsSync16(spfnDir)) {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
const packages = readdirSync2(spfnDir);
|
|
999
|
+
for (const pkg of packages) {
|
|
1000
|
+
const packagePath = join15(spfnDir, pkg);
|
|
1001
|
+
const packageJsonPath = join15(packagePath, "package.json");
|
|
1002
|
+
if (!existsSync16(packageJsonPath)) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
1007
|
+
const spfnConfig = packageJson.spfn;
|
|
1008
|
+
if (!spfnConfig?.migrations) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
|
|
1012
|
+
if (!existsSync16(migrationsDir)) {
|
|
1013
|
+
console.warn(
|
|
1014
|
+
chalk11.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
|
|
1015
|
+
);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
functions.push({
|
|
1019
|
+
packageName: `@spfn/${pkg}`,
|
|
1020
|
+
migrationsDir,
|
|
1021
|
+
packagePath
|
|
1022
|
+
});
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
console.warn(chalk11.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return functions;
|
|
1028
|
+
}
|
|
1029
|
+
async function executeFunctionMigrations(functionMigrations) {
|
|
1030
|
+
let executedCount = 0;
|
|
1031
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
1032
|
+
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
1033
|
+
const postgres = await import("postgres");
|
|
1034
|
+
loadEnvFiles2();
|
|
1035
|
+
if (!env2.DATABASE_URL) {
|
|
1036
|
+
throw new Error("DATABASE_URL not found in environment");
|
|
1037
|
+
}
|
|
1038
|
+
const connection = postgres.default(env2.DATABASE_URL, { max: 1 });
|
|
1039
|
+
const db = drizzle(connection);
|
|
1040
|
+
try {
|
|
1041
|
+
for (const func of functionMigrations) {
|
|
1042
|
+
console.log(chalk11.blue(`
|
|
1043
|
+
\u{1F4E6} Running ${func.packageName} migrations...`));
|
|
1044
|
+
await migrate(db, { migrationsFolder: func.migrationsDir });
|
|
1045
|
+
console.log(chalk11.green(` \u2713 ${func.packageName} migrations applied`));
|
|
1046
|
+
executedCount++;
|
|
1047
|
+
}
|
|
1048
|
+
} finally {
|
|
1049
|
+
await connection.end();
|
|
1050
|
+
}
|
|
1051
|
+
return executedCount;
|
|
1052
|
+
}
|
|
1053
|
+
var init_function_migrations = __esm({
|
|
1054
|
+
"src/utils/function-migrations.ts"() {
|
|
1055
|
+
"use strict";
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// src/index.ts
|
|
1060
|
+
import { Command as Command13 } from "commander";
|
|
1061
|
+
|
|
1062
|
+
// src/commands/create.ts
|
|
1063
|
+
init_logger();
|
|
1064
|
+
init_package_manager();
|
|
1065
|
+
import { Command as Command3 } from "commander";
|
|
1066
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1067
|
+
import { join as join10 } from "path";
|
|
1068
|
+
import prompts2 from "prompts";
|
|
1069
|
+
import ora4 from "ora";
|
|
1070
|
+
import { execa as execa3 } from "execa";
|
|
1071
|
+
import chalk4 from "chalk";
|
|
23
1072
|
async function createProject(projectName, options) {
|
|
24
1073
|
const cwd = process.cwd();
|
|
25
|
-
const projectPath =
|
|
26
|
-
if (
|
|
1074
|
+
const projectPath = join10(cwd, projectName);
|
|
1075
|
+
if (existsSync10(projectPath)) {
|
|
27
1076
|
logger.error(`Directory ${projectName} already exists.`);
|
|
28
1077
|
process.exit(1);
|
|
29
1078
|
}
|
|
30
|
-
console.log(
|
|
1079
|
+
console.log(chalk4.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
|
|
31
1080
|
let pm = options.pm || detectPackageManager(cwd);
|
|
32
1081
|
if (!options.yes && !options.pm) {
|
|
33
|
-
const { selectedPm } = await
|
|
1082
|
+
const { selectedPm } = await prompts2({
|
|
34
1083
|
type: "select",
|
|
35
1084
|
name: "selectedPm",
|
|
36
1085
|
message: "Which package manager do you want to use?",
|
|
@@ -48,7 +1097,7 @@ async function createProject(projectName, options) {
|
|
|
48
1097
|
pm = selectedPm;
|
|
49
1098
|
}
|
|
50
1099
|
logger.step(`Using package manager: ${pm}`);
|
|
51
|
-
const spinner =
|
|
1100
|
+
const spinner = ora4("Creating Next.js project...").start();
|
|
52
1101
|
try {
|
|
53
1102
|
const createNextAppArgs = [
|
|
54
1103
|
"create-next-app@latest",
|
|
@@ -71,7 +1120,7 @@ async function createProject(projectName, options) {
|
|
|
71
1120
|
}
|
|
72
1121
|
const createCommand2 = pm === "npm" ? "npx" : pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "bunx";
|
|
73
1122
|
const createArgs = createCommand2 === "npx" ? createNextAppArgs : ["dlx", ...createNextAppArgs];
|
|
74
|
-
await
|
|
1123
|
+
await execa3(createCommand2, createArgs, {
|
|
75
1124
|
cwd,
|
|
76
1125
|
stdio: "inherit"
|
|
77
1126
|
});
|
|
@@ -85,22 +1134,22 @@ async function createProject(projectName, options) {
|
|
|
85
1134
|
logger.info(`
|
|
86
1135
|
\u{1F4C2} Changed directory to ${projectName}
|
|
87
1136
|
`);
|
|
88
|
-
const iconsSpinner =
|
|
1137
|
+
const iconsSpinner = ora4("Setting up SVGR for icon management...").start();
|
|
89
1138
|
try {
|
|
90
1139
|
const installArgs = pm === "npm" ? ["install", "--save-dev", "@svgr/webpack"] : pm === "yarn" ? ["add", "-D", "@svgr/webpack"] : pm === "pnpm" ? ["add", "-D", "@svgr/webpack"] : ["add", "-d", "@svgr/webpack"];
|
|
91
|
-
await
|
|
92
|
-
const { setupIcons } = await
|
|
93
|
-
await
|
|
1140
|
+
await execa3(pm, installArgs, { cwd: projectPath });
|
|
1141
|
+
const { setupIcons: setupIcons2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
1142
|
+
await setupIcons2();
|
|
94
1143
|
iconsSpinner.succeed("SVGR setup completed");
|
|
95
1144
|
} catch (error) {
|
|
96
1145
|
iconsSpinner.warn("Failed to setup SVGR (you can run `spfn setup icons` later)");
|
|
97
1146
|
}
|
|
98
1147
|
if (options.shadcn) {
|
|
99
|
-
const shadcnSpinner =
|
|
1148
|
+
const shadcnSpinner = ora4("Setting up shadcn/ui...").start();
|
|
100
1149
|
try {
|
|
101
1150
|
const shadcnCommand = pm === "npm" ? "npx" : pm === "pnpm" ? "pnpx" : pm === "yarn" ? "yarn dlx" : "bunx";
|
|
102
1151
|
const shadcnArgs = pm === "yarn" ? ["shadcn@latest", "init", "--yes", "--defaults"] : ["shadcn@latest", "init", "--yes", "--defaults"];
|
|
103
|
-
await
|
|
1152
|
+
await execa3(shadcnCommand, shadcnArgs, {
|
|
104
1153
|
cwd: projectPath,
|
|
105
1154
|
stdio: "inherit"
|
|
106
1155
|
});
|
|
@@ -109,95 +1158,103 @@ async function createProject(projectName, options) {
|
|
|
109
1158
|
shadcnSpinner.warn("Failed to initialize shadcn/ui (you can run `npx shadcn@latest init` later)");
|
|
110
1159
|
}
|
|
111
1160
|
}
|
|
112
|
-
const initSpinner =
|
|
1161
|
+
const initSpinner = ora4("Initializing SPFN...").start();
|
|
113
1162
|
try {
|
|
114
|
-
const { initializeSpfn } = await
|
|
115
|
-
await
|
|
1163
|
+
const { initializeSpfn: initializeSpfn2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1164
|
+
await initializeSpfn2({ yes: true });
|
|
116
1165
|
initSpinner.succeed("SPFN initialized");
|
|
117
1166
|
} catch (error) {
|
|
118
1167
|
initSpinner.fail("Failed to initialize SPFN");
|
|
119
1168
|
logger.error(String(error));
|
|
120
1169
|
process.exit(1);
|
|
121
1170
|
}
|
|
122
|
-
console.log("\n" +
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(` ${
|
|
125
|
-
console.log(` ${
|
|
126
|
-
console.log(` ${
|
|
127
|
-
console.log(` ${
|
|
1171
|
+
console.log("\n" + chalk4.green.bold("\u2713 Project created successfully!\n"));
|
|
1172
|
+
console.log(chalk4.bold("Next steps:\n"));
|
|
1173
|
+
console.log(` ${chalk4.cyan("cd")} ${projectName}`);
|
|
1174
|
+
console.log(` ${chalk4.cyan("docker compose up -d")} ${chalk4.gray("# Start PostgreSQL & Redis")}`);
|
|
1175
|
+
console.log(` ${chalk4.cyan("cp .env.local.example .env.local")} ${chalk4.gray("# Configure environment")}`);
|
|
1176
|
+
console.log(` ${chalk4.cyan(`${pm === "npm" ? "npm run" : pm + " run"} spfn:dev`)} ${chalk4.gray("# Start dev server")}
|
|
128
1177
|
`);
|
|
129
|
-
console.log(
|
|
130
|
-
console.log(` ${
|
|
131
|
-
console.log(` ${
|
|
1178
|
+
console.log(chalk4.bold("Your app will be available at:\n"));
|
|
1179
|
+
console.log(` ${chalk4.cyan("http://localhost:3790")} ${chalk4.gray("(Next.js)")}`);
|
|
1180
|
+
console.log(` ${chalk4.cyan("http://localhost:8790")} ${chalk4.gray("(SPFN API)")}
|
|
132
1181
|
`);
|
|
133
|
-
console.log(
|
|
134
|
-
console.log(" " +
|
|
135
|
-
console.log(` ${
|
|
136
|
-
console.log(` ${
|
|
1182
|
+
console.log(chalk4.bold("\u{1F680} Ready for production?\n"));
|
|
1183
|
+
console.log(" " + chalk4.cyan("Build for production:"));
|
|
1184
|
+
console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:build`);
|
|
1185
|
+
console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start
|
|
137
1186
|
`);
|
|
138
|
-
console.log(" " +
|
|
139
|
-
console.log(` ${
|
|
1187
|
+
console.log(" " + chalk4.cyan("Or deploy with Docker:"));
|
|
1188
|
+
console.log(` ${chalk4.cyan("docker compose -f docker-compose.production.yml up --build -d")}
|
|
140
1189
|
`);
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
1190
|
+
console.log(chalk4.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide"));
|
|
1191
|
+
console.log(chalk4.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
|
|
143
1192
|
}
|
|
144
|
-
var createCommand = new
|
|
1193
|
+
var createCommand = new Command3("create").description("Create a new Next.js project with SPFN").argument("<project-name>", "Name of the project directory").option("--skip-install", "Skip installing dependencies").option("--skip-git", "Skip initializing a git repository").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--shadcn", "Setup shadcn/ui (component library)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
|
|
145
1194
|
await createProject(projectName, options);
|
|
146
1195
|
});
|
|
147
1196
|
|
|
1197
|
+
// src/index.ts
|
|
1198
|
+
init_init();
|
|
1199
|
+
|
|
148
1200
|
// src/commands/dev.ts
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
import {
|
|
152
|
-
import {
|
|
1201
|
+
init_logger();
|
|
1202
|
+
init_package_manager();
|
|
1203
|
+
import { Command as Command4 } from "commander";
|
|
1204
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
|
|
1205
|
+
import { join as join11 } from "path";
|
|
1206
|
+
import { execa as execa4 } from "execa";
|
|
153
1207
|
import chokidar from "chokidar";
|
|
154
|
-
var devCommand = new
|
|
1208
|
+
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) => {
|
|
155
1209
|
process.setMaxListeners(20);
|
|
156
1210
|
if (!process.env.NODE_ENV) {
|
|
157
1211
|
process.env.NODE_ENV = "development";
|
|
158
1212
|
}
|
|
159
1213
|
const cwd = process.cwd();
|
|
160
|
-
const serverDir =
|
|
161
|
-
if (!
|
|
1214
|
+
const serverDir = join11(cwd, "src", "server");
|
|
1215
|
+
if (!existsSync11(serverDir)) {
|
|
162
1216
|
logger.error("src/server directory not found.");
|
|
163
1217
|
logger.info('Run "spfn init" first to initialize SPFN in your project.');
|
|
164
1218
|
process.exit(1);
|
|
165
1219
|
}
|
|
166
|
-
const packageJsonPath =
|
|
1220
|
+
const packageJsonPath = join11(cwd, "package.json");
|
|
167
1221
|
let hasNext = false;
|
|
168
|
-
if (
|
|
169
|
-
const packageJson = JSON.parse(
|
|
1222
|
+
if (existsSync11(packageJsonPath)) {
|
|
1223
|
+
const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
170
1224
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
171
1225
|
}
|
|
172
|
-
const tempDir =
|
|
173
|
-
const serverEntry =
|
|
174
|
-
const watcherEntry =
|
|
1226
|
+
const tempDir = join11(cwd, ".spfn");
|
|
1227
|
+
const serverEntry = join11(tempDir, "server.mjs");
|
|
1228
|
+
const watcherEntry = join11(tempDir, "watcher.mjs");
|
|
175
1229
|
mkdirSync(tempDir, { recursive: true });
|
|
176
|
-
|
|
1230
|
+
const configParts = [];
|
|
1231
|
+
if (options.port) configParts.push(`port: ${options.port}`);
|
|
1232
|
+
if (options.host) configParts.push(`host: '${options.host}'`);
|
|
1233
|
+
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
1234
|
+
configParts.push("debug: true");
|
|
1235
|
+
writeFileSync6(serverEntry, `
|
|
177
1236
|
// Load environment variables FIRST (before any imports that depend on them)
|
|
178
1237
|
// Use centralized environment loader for standard dotenv priority
|
|
179
|
-
|
|
180
|
-
loadEnvironment({ debug: true });
|
|
1238
|
+
await import('@spfn/core/config');
|
|
181
1239
|
|
|
182
1240
|
// Import and start server
|
|
183
1241
|
const { startServer } = await import('@spfn/core/server');
|
|
184
1242
|
|
|
185
1243
|
await startServer({
|
|
186
|
-
|
|
187
|
-
host: '${options.host}',
|
|
188
|
-
${options.routes ? `routesPath: '${options.routes}',` : ""}debug: true
|
|
1244
|
+
${configParts.join(",\n ")}
|
|
189
1245
|
});
|
|
190
1246
|
`);
|
|
191
|
-
|
|
1247
|
+
writeFileSync6(watcherEntry, `
|
|
192
1248
|
// Load environment variables
|
|
193
|
-
const {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
}
|
|
1249
|
+
// const { loadEnvFiles } = await import('@spfn/core/server');
|
|
1250
|
+
// loadEnvFiles();
|
|
1251
|
+
// await import('@spfn/core/config');
|
|
1252
|
+
//
|
|
1253
|
+
// // Initialize database for generators that need it
|
|
1254
|
+
// const { initDatabase, closeDatabase } = await import('@spfn/core/db');
|
|
1255
|
+
// await initDatabase({
|
|
1256
|
+
// pool: { max: 3 } // Watcher needs fewer connections than server
|
|
1257
|
+
// });
|
|
201
1258
|
|
|
202
1259
|
import { CodegenOrchestrator, loadCodegenConfig, createGeneratorsFromConfig } from '@spfn/core/codegen';
|
|
203
1260
|
|
|
@@ -215,7 +1272,6 @@ const orchestrator = new CodegenOrchestrator({
|
|
|
215
1272
|
const cleanup = async () =>
|
|
216
1273
|
{
|
|
217
1274
|
await orchestrator.close();
|
|
218
|
-
await closeDatabase();
|
|
219
1275
|
};
|
|
220
1276
|
|
|
221
1277
|
process.on('SIGTERM', async () =>
|
|
@@ -229,15 +1285,23 @@ process.on('SIGINT', async () =>
|
|
|
229
1285
|
process.exit(0);
|
|
230
1286
|
});
|
|
231
1287
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await
|
|
1288
|
+
// Start watching - this will run indefinitely until the watcher is closed
|
|
1289
|
+
try
|
|
1290
|
+
{
|
|
1291
|
+
await orchestrator.watch();
|
|
1292
|
+
}
|
|
1293
|
+
catch (error)
|
|
1294
|
+
{
|
|
1295
|
+
console.error('[SPFN] Codegen watcher error:', error);
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
236
1298
|
`);
|
|
237
1299
|
const pm = detectPackageManager(cwd);
|
|
238
1300
|
if (options.serverOnly || !hasNext) {
|
|
239
|
-
const watchMode2 = options.watch
|
|
240
|
-
|
|
1301
|
+
const watchMode2 = options.watch === true;
|
|
1302
|
+
const host = options.host ?? process.env.HOST ?? "localhost";
|
|
1303
|
+
const port = options.port ?? process.env.PORT ?? "4000";
|
|
1304
|
+
logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
|
|
241
1305
|
`);
|
|
242
1306
|
let serverProcess2 = null;
|
|
243
1307
|
let watcherProcess2 = null;
|
|
@@ -245,7 +1309,7 @@ await new Promise(() => {});
|
|
|
245
1309
|
const startWatcher2 = () => {
|
|
246
1310
|
const watcherCmd = pm === "npm" ? "npx" : pm;
|
|
247
1311
|
const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
|
|
248
|
-
watcherProcess2 =
|
|
1312
|
+
watcherProcess2 = execa4(watcherCmd, watcherArgs, {
|
|
249
1313
|
cwd,
|
|
250
1314
|
stdio: "inherit",
|
|
251
1315
|
reject: false
|
|
@@ -260,7 +1324,7 @@ await new Promise(() => {});
|
|
|
260
1324
|
const startServer2 = () => {
|
|
261
1325
|
const serverCmd = pm === "npm" ? "npx" : pm;
|
|
262
1326
|
const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
|
|
263
|
-
serverProcess2 =
|
|
1327
|
+
serverProcess2 = execa4(serverCmd, serverArgs, {
|
|
264
1328
|
cwd,
|
|
265
1329
|
stdio: "inherit",
|
|
266
1330
|
reject: false
|
|
@@ -275,7 +1339,7 @@ await new Promise(() => {});
|
|
|
275
1339
|
serverProcess2.kill("SIGTERM");
|
|
276
1340
|
await serverProcess2.catch(() => {
|
|
277
1341
|
});
|
|
278
|
-
await new Promise((
|
|
1342
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
279
1343
|
} catch (error) {
|
|
280
1344
|
}
|
|
281
1345
|
}
|
|
@@ -293,16 +1357,16 @@ await new Promise(() => {});
|
|
|
293
1357
|
pollInterval: 50
|
|
294
1358
|
}
|
|
295
1359
|
});
|
|
296
|
-
watcher.on("change", (
|
|
297
|
-
logger.info(`[SPFN] Changed: ${
|
|
1360
|
+
watcher.on("change", (path5) => {
|
|
1361
|
+
logger.info(`[SPFN] Changed: ${path5.replace(cwd + "/", "")}`);
|
|
298
1362
|
restartServer2();
|
|
299
1363
|
});
|
|
300
|
-
watcher.on("add", (
|
|
301
|
-
logger.info(`[SPFN] Added: ${
|
|
1364
|
+
watcher.on("add", (path5) => {
|
|
1365
|
+
logger.info(`[SPFN] Added: ${path5.replace(cwd + "/", "")}`);
|
|
302
1366
|
restartServer2();
|
|
303
1367
|
});
|
|
304
|
-
watcher.on("unlink", (
|
|
305
|
-
logger.info(`[SPFN] Removed: ${
|
|
1368
|
+
watcher.on("unlink", (path5) => {
|
|
1369
|
+
logger.info(`[SPFN] Removed: ${path5.replace(cwd + "/", "")}`);
|
|
306
1370
|
restartServer2();
|
|
307
1371
|
});
|
|
308
1372
|
}
|
|
@@ -319,11 +1383,17 @@ await new Promise(() => {});
|
|
|
319
1383
|
process.on("SIGTERM", cleanup2);
|
|
320
1384
|
startWatcher2();
|
|
321
1385
|
startServer2();
|
|
322
|
-
await new Promise(() => {
|
|
1386
|
+
await new Promise((resolve2) => {
|
|
1387
|
+
const keepAlive = setInterval(() => {
|
|
1388
|
+
}, 1e6);
|
|
1389
|
+
process.once("beforeExit", () => {
|
|
1390
|
+
clearInterval(keepAlive);
|
|
1391
|
+
resolve2();
|
|
1392
|
+
});
|
|
323
1393
|
});
|
|
324
1394
|
return;
|
|
325
1395
|
}
|
|
326
|
-
const watchMode = options.watch
|
|
1396
|
+
const watchMode = options.watch === true;
|
|
327
1397
|
logger.info(`Starting SPFN server + Next.js (Turbopack)${watchMode ? " (watch mode)" : ""}...
|
|
328
1398
|
`);
|
|
329
1399
|
let serverProcess = null;
|
|
@@ -333,7 +1403,7 @@ await new Promise(() => {});
|
|
|
333
1403
|
const startWatcher = () => {
|
|
334
1404
|
const watcherCmd = pm === "npm" ? "npx" : pm;
|
|
335
1405
|
const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
|
|
336
|
-
watcherProcess =
|
|
1406
|
+
watcherProcess = execa4(watcherCmd, watcherArgs, {
|
|
337
1407
|
cwd,
|
|
338
1408
|
stdio: "inherit",
|
|
339
1409
|
reject: false
|
|
@@ -348,7 +1418,7 @@ await new Promise(() => {});
|
|
|
348
1418
|
const startNext = () => {
|
|
349
1419
|
const nextCmd = pm === "npm" ? "npm" : pm;
|
|
350
1420
|
const nextArgs = pm === "npm" ? ["run", "spfn:next"] : ["run", "spfn:next"];
|
|
351
|
-
nextProcess =
|
|
1421
|
+
nextProcess = execa4(nextCmd, nextArgs, {
|
|
352
1422
|
cwd,
|
|
353
1423
|
stdio: "inherit",
|
|
354
1424
|
reject: false
|
|
@@ -363,7 +1433,7 @@ await new Promise(() => {});
|
|
|
363
1433
|
const startServer = () => {
|
|
364
1434
|
const serverCmd = pm === "npm" ? "npx" : pm;
|
|
365
1435
|
const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
|
|
366
|
-
serverProcess =
|
|
1436
|
+
serverProcess = execa4(serverCmd, serverArgs, {
|
|
367
1437
|
cwd,
|
|
368
1438
|
stdio: "inherit",
|
|
369
1439
|
reject: false
|
|
@@ -378,7 +1448,7 @@ await new Promise(() => {});
|
|
|
378
1448
|
serverProcess.kill("SIGTERM");
|
|
379
1449
|
await serverProcess.catch(() => {
|
|
380
1450
|
});
|
|
381
|
-
await new Promise((
|
|
1451
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
382
1452
|
} catch (error) {
|
|
383
1453
|
}
|
|
384
1454
|
}
|
|
@@ -396,16 +1466,16 @@ await new Promise(() => {});
|
|
|
396
1466
|
pollInterval: 50
|
|
397
1467
|
}
|
|
398
1468
|
});
|
|
399
|
-
watcher.on("change", (
|
|
400
|
-
logger.info(`[SPFN] Changed: ${
|
|
1469
|
+
watcher.on("change", (path5) => {
|
|
1470
|
+
logger.info(`[SPFN] Changed: ${path5.replace(cwd + "/", "")}`);
|
|
401
1471
|
restartServer();
|
|
402
1472
|
});
|
|
403
|
-
watcher.on("add", (
|
|
404
|
-
logger.info(`[SPFN] Added: ${
|
|
1473
|
+
watcher.on("add", (path5) => {
|
|
1474
|
+
logger.info(`[SPFN] Added: ${path5.replace(cwd + "/", "")}`);
|
|
405
1475
|
restartServer();
|
|
406
1476
|
});
|
|
407
|
-
watcher.on("unlink", (
|
|
408
|
-
logger.info(`[SPFN] Removed: ${
|
|
1477
|
+
watcher.on("unlink", (path5) => {
|
|
1478
|
+
logger.info(`[SPFN] Removed: ${path5.replace(cwd + "/", "")}`);
|
|
409
1479
|
restartServer();
|
|
410
1480
|
});
|
|
411
1481
|
}
|
|
@@ -425,39 +1495,50 @@ await new Promise(() => {});
|
|
|
425
1495
|
process.on("SIGTERM", cleanup);
|
|
426
1496
|
startWatcher();
|
|
427
1497
|
startServer();
|
|
428
|
-
await new Promise((
|
|
1498
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2e3));
|
|
429
1499
|
startNext();
|
|
430
|
-
await new Promise(() => {
|
|
1500
|
+
await new Promise((resolve2) => {
|
|
1501
|
+
const keepAlive = setInterval(() => {
|
|
1502
|
+
}, 1e6);
|
|
1503
|
+
process.once("beforeExit", () => {
|
|
1504
|
+
clearInterval(keepAlive);
|
|
1505
|
+
resolve2();
|
|
1506
|
+
});
|
|
431
1507
|
});
|
|
432
1508
|
});
|
|
433
1509
|
|
|
434
1510
|
// src/commands/build.ts
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
import {
|
|
438
|
-
import {
|
|
439
|
-
import
|
|
440
|
-
import
|
|
1511
|
+
init_logger();
|
|
1512
|
+
init_package_manager();
|
|
1513
|
+
import { Command as Command5 } from "commander";
|
|
1514
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
1515
|
+
import { join as join12 } from "path";
|
|
1516
|
+
import { execa as execa5 } from "execa";
|
|
1517
|
+
import ora5 from "ora";
|
|
1518
|
+
import chalk5 from "chalk";
|
|
441
1519
|
import { build } from "tsup";
|
|
442
1520
|
async function buildProject(options) {
|
|
443
1521
|
if (!process.env.NODE_ENV) {
|
|
444
1522
|
process.env.NODE_ENV = "production";
|
|
445
1523
|
}
|
|
1524
|
+
if (!process.env.LOG_LEVEL) {
|
|
1525
|
+
process.env.LOG_LEVEL = "warn";
|
|
1526
|
+
}
|
|
446
1527
|
const cwd = process.cwd();
|
|
447
1528
|
const pm = detectPackageManager(cwd);
|
|
448
|
-
const packageJsonPath =
|
|
1529
|
+
const packageJsonPath = join12(cwd, "package.json");
|
|
449
1530
|
let hasNext = false;
|
|
450
|
-
if (
|
|
1531
|
+
if (existsSync12(packageJsonPath)) {
|
|
451
1532
|
const packageJson = JSON.parse(await import("fs").then(
|
|
452
|
-
(
|
|
1533
|
+
(fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
|
|
453
1534
|
));
|
|
454
1535
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
455
1536
|
}
|
|
456
|
-
const serverDir =
|
|
457
|
-
const hasServer =
|
|
458
|
-
console.log(
|
|
1537
|
+
const serverDir = join12(cwd, "src", "server");
|
|
1538
|
+
const hasServer = existsSync12(serverDir);
|
|
1539
|
+
console.log(chalk5.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
|
|
459
1540
|
if (hasServer) {
|
|
460
|
-
const spinner =
|
|
1541
|
+
const spinner = ora5("Generating API client...").start();
|
|
461
1542
|
try {
|
|
462
1543
|
const { CodegenOrchestrator, loadCodegenConfig, createGeneratorsFromConfig } = await import("@spfn/core/codegen");
|
|
463
1544
|
const config = loadCodegenConfig(cwd);
|
|
@@ -465,7 +1546,7 @@ async function buildProject(options) {
|
|
|
465
1546
|
const orchestrator = new CodegenOrchestrator({
|
|
466
1547
|
generators,
|
|
467
1548
|
cwd,
|
|
468
|
-
debug:
|
|
1549
|
+
debug: true
|
|
469
1550
|
});
|
|
470
1551
|
await orchestrator.generateAll();
|
|
471
1552
|
spinner.succeed("API client generated");
|
|
@@ -475,9 +1556,9 @@ async function buildProject(options) {
|
|
|
475
1556
|
}
|
|
476
1557
|
}
|
|
477
1558
|
if (hasNext && !options.serverOnly) {
|
|
478
|
-
const spinner =
|
|
1559
|
+
const spinner = ora5("Building Next.js...").start();
|
|
479
1560
|
try {
|
|
480
|
-
await
|
|
1561
|
+
await execa5(pm, ["run", "build"], {
|
|
481
1562
|
cwd,
|
|
482
1563
|
stdio: "inherit"
|
|
483
1564
|
});
|
|
@@ -489,12 +1570,12 @@ async function buildProject(options) {
|
|
|
489
1570
|
}
|
|
490
1571
|
}
|
|
491
1572
|
if (hasServer && !options.nextOnly) {
|
|
492
|
-
const spinner =
|
|
1573
|
+
const spinner = ora5("Building SPFN server...").start();
|
|
493
1574
|
try {
|
|
494
|
-
const outputDir =
|
|
1575
|
+
const outputDir = join12(cwd, ".spfn", "server");
|
|
495
1576
|
mkdirSync2(outputDir, { recursive: true });
|
|
496
|
-
const serverDir2 =
|
|
497
|
-
if (!
|
|
1577
|
+
const serverDir2 = join12(cwd, "src", "server");
|
|
1578
|
+
if (!existsSync12(serverDir2)) {
|
|
498
1579
|
spinner.fail("SPFN server build failed");
|
|
499
1580
|
logger.error("src/server/ directory not found");
|
|
500
1581
|
logger.error('Please run "spfn init" to initialize the project.');
|
|
@@ -518,15 +1599,14 @@ async function buildProject(options) {
|
|
|
518
1599
|
"@sinclair/typebox",
|
|
519
1600
|
"@spfn/core"
|
|
520
1601
|
],
|
|
521
|
-
silent:
|
|
1602
|
+
silent: true,
|
|
522
1603
|
onSuccess: async () => {
|
|
523
1604
|
}
|
|
524
1605
|
});
|
|
525
|
-
const prodServerPath =
|
|
1606
|
+
const prodServerPath = join12(cwd, ".spfn", "prod-server.mjs");
|
|
526
1607
|
const prodServerContent = `// Load environment variables FIRST (before any imports that depend on them)
|
|
527
1608
|
// Use centralized environment loader for standard dotenv priority
|
|
528
|
-
const {
|
|
529
|
-
loadEnvironment({ debug: false });
|
|
1609
|
+
const { env } = await import('@spfn/core/config');
|
|
530
1610
|
|
|
531
1611
|
// Now import server (logger singleton will be created with correct NODE_ENV)
|
|
532
1612
|
const { startServer } = await import('@spfn/core/server');
|
|
@@ -537,8 +1617,8 @@ import { dirname } from 'path';
|
|
|
537
1617
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
538
1618
|
|
|
539
1619
|
// Environment variables: from .env files OR injected by container/kubernetes
|
|
540
|
-
const port =
|
|
541
|
-
const host =
|
|
1620
|
+
const port = env.SPFN_PORT || '8790';
|
|
1621
|
+
const host = env.SPFN_HOST || '0.0.0.0';
|
|
542
1622
|
|
|
543
1623
|
await startServer({
|
|
544
1624
|
port: Number(port),
|
|
@@ -547,11 +1627,41 @@ await startServer({
|
|
|
547
1627
|
debug: false
|
|
548
1628
|
});
|
|
549
1629
|
`;
|
|
550
|
-
|
|
1630
|
+
writeFileSync7(prodServerPath, prodServerContent);
|
|
551
1631
|
spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
|
|
1632
|
+
const routesDir = join12(cwd, ".spfn", "server", "routes");
|
|
1633
|
+
if (existsSync12(routesDir)) {
|
|
1634
|
+
console.log();
|
|
1635
|
+
console.log(chalk5.bold("Route (api)"));
|
|
1636
|
+
try {
|
|
1637
|
+
const routes = readdirSync(routesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
|
|
1638
|
+
if (routes.length > 0) {
|
|
1639
|
+
routes.forEach((route, index) => {
|
|
1640
|
+
const isLast = index === routes.length - 1;
|
|
1641
|
+
const prefix = isLast ? "\u2514" : "\u251C";
|
|
1642
|
+
const routePath = `/api/${route}`;
|
|
1643
|
+
if (route === "health") {
|
|
1644
|
+
console.log(`${prefix} ${chalk5.green("GET")} ${routePath}`);
|
|
1645
|
+
} else {
|
|
1646
|
+
console.log(`${prefix} ${chalk5.cyan("*")} ${routePath}`);
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
console.log();
|
|
1650
|
+
console.log(chalk5.cyan("*") + " (Hono) multiple methods supported");
|
|
1651
|
+
}
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
552
1655
|
} catch (error) {
|
|
553
1656
|
spinner.fail("SPFN server build failed");
|
|
554
|
-
|
|
1657
|
+
if (error instanceof Error) {
|
|
1658
|
+
console.error("\n" + chalk5.red(error.message));
|
|
1659
|
+
if (error.stack) {
|
|
1660
|
+
console.error(chalk5.dim("\n" + error.stack));
|
|
1661
|
+
}
|
|
1662
|
+
} else {
|
|
1663
|
+
logger.error(String(error));
|
|
1664
|
+
}
|
|
555
1665
|
process.exit(1);
|
|
556
1666
|
}
|
|
557
1667
|
}
|
|
@@ -559,41 +1669,42 @@ await startServer({
|
|
|
559
1669
|
logger.error("No Next.js or SPFN server found in this project.");
|
|
560
1670
|
process.exit(1);
|
|
561
1671
|
}
|
|
562
|
-
console.log("\n" +
|
|
563
|
-
console.log(
|
|
564
|
-
console.log(" " +
|
|
565
|
-
console.log(` ${
|
|
1672
|
+
console.log("\n" + chalk5.green.bold("\u2713 Build completed successfully!\n"));
|
|
1673
|
+
console.log(chalk5.bold("Next steps:\n"));
|
|
1674
|
+
console.log(" " + chalk5.cyan("Start production server:"));
|
|
1675
|
+
console.log(` ${chalk5.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start ${chalk5.gray("# Start SPFN + Next.js")}
|
|
566
1676
|
`);
|
|
567
|
-
console.log(" " +
|
|
568
|
-
console.log(` ${
|
|
1677
|
+
console.log(" " + chalk5.cyan("Or deploy with Docker:"));
|
|
1678
|
+
console.log(` ${chalk5.cyan("docker compose -f docker-compose.production.yml up --build -d")}
|
|
569
1679
|
`);
|
|
570
|
-
console.log(chalk2.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide\n"));
|
|
571
1680
|
}
|
|
572
|
-
var buildCommand = new
|
|
1681
|
+
var buildCommand = new Command5("build").description("Build SPFN project for production (Next.js + Server)").option("--server-only", "Build only SPFN server (skip Next.js)").option("--next-only", "Build only Next.js (skip SPFN server)").option("--turbo", "Use Turbopack for Next.js build").action(async (options) => {
|
|
573
1682
|
await buildProject(options);
|
|
574
1683
|
});
|
|
575
1684
|
|
|
576
1685
|
// src/commands/start.ts
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
import {
|
|
580
|
-
import {
|
|
581
|
-
import
|
|
582
|
-
|
|
1686
|
+
init_logger();
|
|
1687
|
+
init_package_manager();
|
|
1688
|
+
import { Command as Command6 } from "commander";
|
|
1689
|
+
import { existsSync as existsSync13, readFileSync as readFileSync4 } from "fs";
|
|
1690
|
+
import { join as join13 } from "path";
|
|
1691
|
+
import { execa as execa6 } from "execa";
|
|
1692
|
+
import chalk6 from "chalk";
|
|
1693
|
+
var startCommand = new Command6("start").description("Start SPFN production server (Next.js + Hono)").option("--server-only", "Run only SPFN server (skip Next.js)").option("--next-only", "Run only Next.js (skip SPFN server)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "0.0.0.0").action(async (options) => {
|
|
583
1694
|
if (!process.env.NODE_ENV) {
|
|
584
1695
|
process.env.NODE_ENV = "production";
|
|
585
1696
|
}
|
|
586
1697
|
const cwd = process.cwd();
|
|
587
|
-
const packageJsonPath =
|
|
1698
|
+
const packageJsonPath = join13(cwd, "package.json");
|
|
588
1699
|
let hasNext = false;
|
|
589
|
-
if (
|
|
590
|
-
const packageJson = JSON.parse(
|
|
1700
|
+
if (existsSync13(packageJsonPath)) {
|
|
1701
|
+
const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
591
1702
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
592
1703
|
}
|
|
593
|
-
const builtServerDir =
|
|
594
|
-
const hasBuiltServer =
|
|
595
|
-
const nextBuildDir =
|
|
596
|
-
const hasNextBuild =
|
|
1704
|
+
const builtServerDir = join13(cwd, ".spfn", "server");
|
|
1705
|
+
const hasBuiltServer = existsSync13(builtServerDir);
|
|
1706
|
+
const nextBuildDir = join13(cwd, ".next");
|
|
1707
|
+
const hasNextBuild = existsSync13(nextBuildDir);
|
|
597
1708
|
if (!options.nextOnly && !hasBuiltServer) {
|
|
598
1709
|
logger.error('.spfn/server directory not found. Please run "spfn build" first.');
|
|
599
1710
|
process.exit(1);
|
|
@@ -603,8 +1714,8 @@ var startCommand = new Command4("start").description("Start SPFN production serv
|
|
|
603
1714
|
process.exit(1);
|
|
604
1715
|
}
|
|
605
1716
|
const pm = detectPackageManager(cwd);
|
|
606
|
-
const serverEntry =
|
|
607
|
-
if (!
|
|
1717
|
+
const serverEntry = join13(cwd, ".spfn", "prod-server.mjs");
|
|
1718
|
+
if (!existsSync13(serverEntry)) {
|
|
608
1719
|
logger.error('.spfn/prod-server.mjs not found. Please run "spfn build" first.');
|
|
609
1720
|
process.exit(1);
|
|
610
1721
|
}
|
|
@@ -614,7 +1725,7 @@ var startCommand = new Command4("start").description("Start SPFN production serv
|
|
|
614
1725
|
logger.info(`Starting SPFN Server (production) on http://${options.host}:${options.port}
|
|
615
1726
|
`);
|
|
616
1727
|
try {
|
|
617
|
-
await
|
|
1728
|
+
await execa6("node", [serverEntry], {
|
|
618
1729
|
stdio: "inherit",
|
|
619
1730
|
cwd,
|
|
620
1731
|
env: { ...process.env }
|
|
@@ -628,7 +1739,7 @@ var startCommand = new Command4("start").description("Start SPFN production serv
|
|
|
628
1739
|
if (options.nextOnly) {
|
|
629
1740
|
logger.info("Starting Next.js (production) on http://0.0.0.0:3790\n");
|
|
630
1741
|
try {
|
|
631
|
-
await
|
|
1742
|
+
await execa6("npx", ["next", "start", "-H", "0.0.0.0", "-p", "3790"], {
|
|
632
1743
|
stdio: "inherit",
|
|
633
1744
|
cwd
|
|
634
1745
|
});
|
|
@@ -640,12 +1751,12 @@ var startCommand = new Command4("start").description("Start SPFN production serv
|
|
|
640
1751
|
}
|
|
641
1752
|
const nextCmd = "next start -H 0.0.0.0 -p 3790";
|
|
642
1753
|
const serverCmd = `node ${serverEntry}`;
|
|
643
|
-
console.log(
|
|
1754
|
+
console.log(chalk6.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
|
|
644
1755
|
logger.info("Next.js: http://0.0.0.0:3790");
|
|
645
1756
|
logger.info(`SPFN API: http://${options.host}:${options.port}
|
|
646
1757
|
`);
|
|
647
1758
|
try {
|
|
648
|
-
await
|
|
1759
|
+
await execa6(
|
|
649
1760
|
pm === "npm" ? "npx" : pm,
|
|
650
1761
|
pm === "npm" ? ["concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`] : ["exec", "concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`],
|
|
651
1762
|
{
|
|
@@ -666,15 +1777,16 @@ var startCommand = new Command4("start").description("Start SPFN production serv
|
|
|
666
1777
|
});
|
|
667
1778
|
|
|
668
1779
|
// src/commands/codegen.ts
|
|
669
|
-
|
|
670
|
-
import {
|
|
671
|
-
import {
|
|
672
|
-
import
|
|
1780
|
+
init_logger();
|
|
1781
|
+
import { Command as Command7 } from "commander";
|
|
1782
|
+
import { existsSync as existsSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
1783
|
+
import { join as join14 } from "path";
|
|
1784
|
+
import chalk7 from "chalk";
|
|
673
1785
|
async function initCodegen(options) {
|
|
674
1786
|
const cwd = process.cwd();
|
|
675
|
-
const rcPath =
|
|
676
|
-
if (
|
|
677
|
-
logger.warn(".spfnrc.
|
|
1787
|
+
const rcPath = join14(cwd, ".spfnrc.ts");
|
|
1788
|
+
if (existsSync14(rcPath)) {
|
|
1789
|
+
logger.warn(".spfnrc.ts already exists");
|
|
678
1790
|
logger.info("Edit manually to add custom generators");
|
|
679
1791
|
process.exit(0);
|
|
680
1792
|
}
|
|
@@ -688,15 +1800,15 @@ async function initCodegen(options) {
|
|
|
688
1800
|
]
|
|
689
1801
|
}
|
|
690
1802
|
};
|
|
691
|
-
|
|
692
|
-
console.log("\n" +
|
|
1803
|
+
writeFileSync8(rcPath, JSON.stringify(config, null, 2) + "\n");
|
|
1804
|
+
console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
693
1805
|
console.log("Configuration:");
|
|
694
|
-
console.log(
|
|
1806
|
+
console.log(chalk7.gray(JSON.stringify(config, null, 2)));
|
|
695
1807
|
if (options.withExample) {
|
|
696
|
-
console.log("\n" +
|
|
1808
|
+
console.log("\n" + chalk7.yellow("\u{1F4DD} To add custom generators:"));
|
|
697
1809
|
console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
|
|
698
|
-
console.log(" 2. Add to .spfnrc.
|
|
699
|
-
console.log(
|
|
1810
|
+
console.log(" 2. Add to .spfnrc.ts:");
|
|
1811
|
+
console.log(chalk7.gray(`
|
|
700
1812
|
{
|
|
701
1813
|
"codegen": {
|
|
702
1814
|
"generators": [
|
|
@@ -706,11 +1818,11 @@ async function initCodegen(options) {
|
|
|
706
1818
|
}
|
|
707
1819
|
}
|
|
708
1820
|
`));
|
|
709
|
-
console.log(" 3. Run: " +
|
|
1821
|
+
console.log(" 3. Run: " + chalk7.cyan("spfn dev") + " (generators run automatically)");
|
|
710
1822
|
} else {
|
|
711
|
-
console.log("\n" +
|
|
712
|
-
console.log(" \u2022 Add custom generators to .spfnrc.
|
|
713
|
-
console.log(" \u2022 Run: " +
|
|
1823
|
+
console.log("\n" + chalk7.yellow("\u{1F4DD} Next steps:"));
|
|
1824
|
+
console.log(" \u2022 Add custom generators to .spfnrc.ts");
|
|
1825
|
+
console.log(" \u2022 Run: " + chalk7.cyan("spfn dev") + " to start development with code generation");
|
|
714
1826
|
}
|
|
715
1827
|
}
|
|
716
1828
|
async function listGenerators() {
|
|
@@ -723,10 +1835,10 @@ async function listGenerators() {
|
|
|
723
1835
|
logger.info('Run "spfn codegen init" to initialize configuration');
|
|
724
1836
|
return;
|
|
725
1837
|
}
|
|
726
|
-
console.log("\n" +
|
|
1838
|
+
console.log("\n" + chalk7.bold("Registered Generators:"));
|
|
727
1839
|
generators.forEach((gen, index) => {
|
|
728
|
-
console.log(` ${index + 1}. ${
|
|
729
|
-
console.log(` Patterns: ${
|
|
1840
|
+
console.log(` ${index + 1}. ${chalk7.cyan(gen.name)}`);
|
|
1841
|
+
console.log(` Patterns: ${chalk7.gray(gen.watchPatterns.join(", "))}`);
|
|
730
1842
|
});
|
|
731
1843
|
console.log("");
|
|
732
1844
|
}
|
|
@@ -744,21 +1856,22 @@ async function runGenerators() {
|
|
|
744
1856
|
const orchestrator = new CodegenOrchestrator({
|
|
745
1857
|
generators,
|
|
746
1858
|
cwd,
|
|
747
|
-
debug:
|
|
1859
|
+
debug: true
|
|
748
1860
|
});
|
|
749
1861
|
await orchestrator.generateAll();
|
|
750
|
-
console.log("\n" +
|
|
1862
|
+
console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
|
|
751
1863
|
}
|
|
752
|
-
var codegenCommand = new
|
|
753
|
-
codegenCommand.command("init").description("Initialize .spfnrc.
|
|
1864
|
+
var codegenCommand = new Command7("codegen").description("Code generation management");
|
|
1865
|
+
codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
|
|
754
1866
|
codegenCommand.command("list").alias("ls").description("List registered code generators").action(listGenerators);
|
|
755
1867
|
codegenCommand.command("run").description("Run code generators once (no watch mode)").action(runGenerators);
|
|
756
1868
|
|
|
757
1869
|
// src/commands/key.ts
|
|
758
|
-
|
|
1870
|
+
init_logger();
|
|
1871
|
+
import { Command as Command8 } from "commander";
|
|
759
1872
|
import { randomBytes } from "crypto";
|
|
760
1873
|
import { execSync } from "child_process";
|
|
761
|
-
import
|
|
1874
|
+
import chalk8 from "chalk";
|
|
762
1875
|
var PRESETS = {
|
|
763
1876
|
"auth-encryption": {
|
|
764
1877
|
bytes: 32,
|
|
@@ -809,62 +1922,62 @@ function copyToClipboard(text) {
|
|
|
809
1922
|
}
|
|
810
1923
|
}
|
|
811
1924
|
function generateSecret(bytes, preset, envVarName, copy) {
|
|
812
|
-
const key = randomBytes(bytes).toString("
|
|
1925
|
+
const key = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
813
1926
|
const config = preset ? PRESETS[preset] : null;
|
|
814
|
-
console.log("\n" +
|
|
1927
|
+
console.log("\n" + chalk8.green.bold("\u2713 Generated secret key:"));
|
|
815
1928
|
if (config) {
|
|
816
|
-
console.log(
|
|
1929
|
+
console.log(chalk8.dim(` ${config.description} (${bytes * 8} bits)`));
|
|
817
1930
|
} else {
|
|
818
|
-
console.log(
|
|
1931
|
+
console.log(chalk8.dim(` ${bytes * 8}-bit secret`));
|
|
819
1932
|
}
|
|
820
|
-
console.log("\n" +
|
|
1933
|
+
console.log("\n" + chalk8.cyan(key) + "\n");
|
|
821
1934
|
const varName = envVarName || config?.envVar || "SECRET_KEY";
|
|
822
|
-
console.log(
|
|
823
|
-
console.log(
|
|
1935
|
+
console.log(chalk8.dim("Add to your .env file:"));
|
|
1936
|
+
console.log(chalk8.yellow(`${varName}=${key}
|
|
824
1937
|
`));
|
|
825
1938
|
if (config?.usage) {
|
|
826
|
-
console.log(
|
|
827
|
-
console.log(
|
|
1939
|
+
console.log(chalk8.dim("Usage:"));
|
|
1940
|
+
console.log(chalk8.gray(` ${config.usage}
|
|
828
1941
|
`));
|
|
829
1942
|
}
|
|
830
1943
|
if (copy) {
|
|
831
1944
|
if (copyToClipboard(key)) {
|
|
832
|
-
console.log(
|
|
1945
|
+
console.log(chalk8.green("\u2713 Copied to clipboard!\n"));
|
|
833
1946
|
} else {
|
|
834
1947
|
logger.warn("Could not copy to clipboard");
|
|
835
1948
|
}
|
|
836
1949
|
}
|
|
837
1950
|
}
|
|
838
1951
|
function listPresets() {
|
|
839
|
-
console.log("\n" +
|
|
1952
|
+
console.log("\n" + chalk8.bold("Available presets:"));
|
|
840
1953
|
console.log();
|
|
841
1954
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
842
|
-
console.log(` ${
|
|
843
|
-
console.log(` ${" ".repeat(20)} ${
|
|
1955
|
+
console.log(` ${chalk8.cyan(name.padEnd(20))} ${chalk8.dim(config.description)}`);
|
|
1956
|
+
console.log(` ${" ".repeat(20)} ${chalk8.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
|
|
844
1957
|
console.log();
|
|
845
1958
|
});
|
|
846
|
-
console.log(
|
|
847
|
-
console.log(
|
|
848
|
-
console.log(
|
|
1959
|
+
console.log(chalk8.dim("Usage:"));
|
|
1960
|
+
console.log(chalk8.gray(" spfn key <preset>"));
|
|
1961
|
+
console.log(chalk8.gray(" spfn key auth-encryption --copy"));
|
|
849
1962
|
console.log();
|
|
850
1963
|
}
|
|
851
|
-
var generateValueCommand = new
|
|
1964
|
+
var generateValueCommand = new Command8("generate").alias("gen").description("Generate random value (simple output, no metadata)").option("-b, --bytes <number>", "Number of random bytes", "32").option("-c, --copy", "Copy to clipboard").action((options) => {
|
|
852
1965
|
const bytes = parseInt(options.bytes, 10);
|
|
853
1966
|
if (isNaN(bytes) || bytes < 1 || bytes > 128) {
|
|
854
1967
|
logger.error("Invalid bytes value. Must be between 1 and 128.");
|
|
855
1968
|
process.exit(1);
|
|
856
1969
|
}
|
|
857
|
-
const value = randomBytes(bytes).toString("
|
|
1970
|
+
const value = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
858
1971
|
console.log(value);
|
|
859
1972
|
if (options.copy) {
|
|
860
1973
|
if (copyToClipboard(value)) {
|
|
861
|
-
console.error(
|
|
1974
|
+
console.error(chalk8.green("\u2713 Copied to clipboard"));
|
|
862
1975
|
} else {
|
|
863
|
-
console.error(
|
|
1976
|
+
console.error(chalk8.yellow("\u26A0 Could not copy to clipboard"));
|
|
864
1977
|
}
|
|
865
1978
|
}
|
|
866
1979
|
});
|
|
867
|
-
var keyCommand = new
|
|
1980
|
+
var keyCommand = new Command8("key").alias("k").description("Generate secure random keys and secrets").argument("[preset]", `Preset type (use --list to see all)`).option("-l, --list", "List all available presets").option("-b, --bytes <number>", "Number of random bytes to generate", "32").option("-e, --env <name>", "Environment variable name").option("-c, --copy", "Copy to clipboard").action((preset, options) => {
|
|
868
1981
|
if (options.list) {
|
|
869
1982
|
listPresets();
|
|
870
1983
|
return;
|
|
@@ -878,9 +1991,9 @@ var keyCommand = new Command6("key").alias("k").description("Generate secure ran
|
|
|
878
1991
|
logger.error(`Unknown preset: ${preset}`);
|
|
879
1992
|
console.log("\nAvailable presets:");
|
|
880
1993
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
881
|
-
console.log(` ${
|
|
1994
|
+
console.log(` ${chalk8.cyan(name)}: ${config.description}`);
|
|
882
1995
|
});
|
|
883
|
-
console.log("\nUse " +
|
|
1996
|
+
console.log("\nUse " + chalk8.cyan("--list") + " to see detailed information");
|
|
884
1997
|
process.exit(1);
|
|
885
1998
|
}
|
|
886
1999
|
generateSecret(
|
|
@@ -892,41 +2005,145 @@ var keyCommand = new Command6("key").alias("k").description("Generate secure ran
|
|
|
892
2005
|
});
|
|
893
2006
|
keyCommand.addCommand(generateValueCommand);
|
|
894
2007
|
|
|
895
|
-
// src/
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
import
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
import
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
import
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
2008
|
+
// src/index.ts
|
|
2009
|
+
init_setup();
|
|
2010
|
+
|
|
2011
|
+
// src/commands/db/index.ts
|
|
2012
|
+
import { Command as Command9 } from "commander";
|
|
2013
|
+
|
|
2014
|
+
// src/commands/db/generate.ts
|
|
2015
|
+
import chalk10 from "chalk";
|
|
2016
|
+
|
|
2017
|
+
// src/commands/db/utils/drizzle.ts
|
|
2018
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
|
|
2019
|
+
import { spawn } from "child_process";
|
|
2020
|
+
import chalk9 from "chalk";
|
|
2021
|
+
import ora6 from "ora";
|
|
2022
|
+
import { env } from "@spfn/core/config";
|
|
2023
|
+
import { loadEnvFiles } from "@spfn/core/server";
|
|
2024
|
+
function validateDatabasePrerequisites() {
|
|
2025
|
+
loadEnvFiles();
|
|
2026
|
+
if (!env.DATABASE_URL) {
|
|
2027
|
+
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2028
|
+
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2029
|
+
throw new Error("DATABASE_URL is required for database operations");
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async function runDrizzleCommand(command) {
|
|
2033
|
+
const hasUserConfig = existsSync15("./drizzle.config.ts");
|
|
2034
|
+
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2035
|
+
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2036
|
+
if (!hasUserConfig) {
|
|
2037
|
+
loadEnvFiles();
|
|
2038
|
+
if (!env.DATABASE_URL) {
|
|
2039
|
+
console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
|
|
2040
|
+
console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2041
|
+
process.exit(1);
|
|
2042
|
+
}
|
|
2043
|
+
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
2044
|
+
const configContent = generateDrizzleConfigFile({
|
|
2045
|
+
cwd: process.cwd(),
|
|
2046
|
+
// Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
|
|
2047
|
+
disablePackageDiscovery: true,
|
|
2048
|
+
// Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
|
|
2049
|
+
expandGlobs: true,
|
|
2050
|
+
autoDetectSchemas: true
|
|
913
2051
|
});
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
2052
|
+
writeFileSync9(tempConfigPath, configContent);
|
|
2053
|
+
console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
|
|
2054
|
+
}
|
|
2055
|
+
const args = command.split(" ");
|
|
2056
|
+
args.push(`--config=${configPath}`);
|
|
2057
|
+
return new Promise((resolve2, reject) => {
|
|
2058
|
+
const drizzleProcess = spawn("drizzle-kit", args, {
|
|
2059
|
+
stdio: "inherit",
|
|
2060
|
+
// Allow interactive input
|
|
2061
|
+
shell: true
|
|
2062
|
+
});
|
|
2063
|
+
const cleanup = () => {
|
|
2064
|
+
if (!hasUserConfig && existsSync15(tempConfigPath)) {
|
|
2065
|
+
unlinkSync(tempConfigPath);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
drizzleProcess.on("close", (code) => {
|
|
2069
|
+
cleanup();
|
|
2070
|
+
if (code === 0) {
|
|
2071
|
+
resolve2();
|
|
2072
|
+
} else {
|
|
2073
|
+
reject(new Error(`drizzle-kit ${command} exited with code ${code}`));
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
drizzleProcess.on("error", (error) => {
|
|
2077
|
+
cleanup();
|
|
2078
|
+
reject(error);
|
|
917
2079
|
});
|
|
918
|
-
server.listen(port, "127.0.0.1");
|
|
919
2080
|
});
|
|
920
2081
|
}
|
|
921
|
-
async function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
2082
|
+
async function runWithSpinner(spinnerText, command, successMessage, failMessage) {
|
|
2083
|
+
const spinner = ora6(spinnerText).start();
|
|
2084
|
+
try {
|
|
2085
|
+
spinner.stop();
|
|
2086
|
+
await runDrizzleCommand(command);
|
|
2087
|
+
console.log(chalk9.green(`\u2705 ${successMessage}`));
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
spinner.fail(failMessage);
|
|
2090
|
+
console.error(chalk9.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2091
|
+
process.exit(1);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// src/commands/db/generate.ts
|
|
2096
|
+
async function dbGenerate() {
|
|
2097
|
+
try {
|
|
2098
|
+
await runDrizzleCommand("generate");
|
|
2099
|
+
console.log(chalk10.green("\n\u2705 Migrations generated successfully"));
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
console.error(chalk10.red("\n\u274C Failed to generate migrations"));
|
|
2102
|
+
console.error(chalk10.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2103
|
+
process.exit(1);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/commands/db/push.ts
|
|
2108
|
+
import chalk12 from "chalk";
|
|
2109
|
+
import "@spfn/core/config";
|
|
2110
|
+
async function dbPush() {
|
|
2111
|
+
await runWithSpinner(
|
|
2112
|
+
"Pushing schema changes to database...",
|
|
2113
|
+
"push",
|
|
2114
|
+
"Schema pushed successfully",
|
|
2115
|
+
"Failed to push schema"
|
|
2116
|
+
);
|
|
2117
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2118
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2119
|
+
if (functions.length > 0) {
|
|
2120
|
+
console.log(chalk12.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
2121
|
+
functions.forEach((func) => {
|
|
2122
|
+
console.log(chalk12.dim(` - ${func.packageName}`));
|
|
2123
|
+
});
|
|
2124
|
+
try {
|
|
2125
|
+
await executeFunctionMigrations2(functions);
|
|
2126
|
+
console.log(chalk12.green("\n\u2705 All function migrations applied\n"));
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
console.error(chalk12.red("\n\u274C Failed to apply function migrations"));
|
|
2129
|
+
console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2130
|
+
process.exit(1);
|
|
926
2131
|
}
|
|
927
2132
|
}
|
|
928
|
-
throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
929
2133
|
}
|
|
2134
|
+
|
|
2135
|
+
// src/commands/db/migrate.ts
|
|
2136
|
+
import chalk16 from "chalk";
|
|
2137
|
+
|
|
2138
|
+
// src/commands/db/backup.ts
|
|
2139
|
+
import { promises as fs3 } from "fs";
|
|
2140
|
+
import path3 from "path";
|
|
2141
|
+
import { spawn as spawn2 } from "child_process";
|
|
2142
|
+
import chalk15 from "chalk";
|
|
2143
|
+
import ora7 from "ora";
|
|
2144
|
+
|
|
2145
|
+
// src/commands/db/utils/database.ts
|
|
2146
|
+
import net from "net";
|
|
930
2147
|
function parseDatabaseUrl(dbUrl) {
|
|
931
2148
|
try {
|
|
932
2149
|
const url = new URL(dbUrl);
|
|
@@ -942,61 +2159,64 @@ function parseDatabaseUrl(dbUrl) {
|
|
|
942
2159
|
throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
943
2160
|
}
|
|
944
2161
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
959
|
-
const hours = String(now.getHours()).padStart(2, "0");
|
|
960
|
-
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
961
|
-
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
962
|
-
return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
|
|
2162
|
+
async function isPortAvailable(port) {
|
|
2163
|
+
return new Promise((resolve2) => {
|
|
2164
|
+
const server = net.createServer();
|
|
2165
|
+
server.once("error", () => {
|
|
2166
|
+
server.close();
|
|
2167
|
+
resolve2(false);
|
|
2168
|
+
});
|
|
2169
|
+
server.once("listening", () => {
|
|
2170
|
+
server.close();
|
|
2171
|
+
resolve2(true);
|
|
2172
|
+
});
|
|
2173
|
+
server.listen(port, "127.0.0.1");
|
|
2174
|
+
});
|
|
963
2175
|
}
|
|
964
|
-
async function
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (exists) {
|
|
970
|
-
content = await fs.readFile(gitignorePath, "utf-8");
|
|
971
|
-
}
|
|
972
|
-
const lines = content.split("\n");
|
|
973
|
-
const hasBackupsIgnore = lines.some(
|
|
974
|
-
(line) => line.trim() === "backups/" || line.trim() === "/backups/" || line.trim() === "backups"
|
|
975
|
-
);
|
|
976
|
-
if (!hasBackupsIgnore) {
|
|
977
|
-
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
978
|
-
await fs.appendFile(gitignorePath, entry);
|
|
979
|
-
console.log(chalk6.dim("\u2713 Added backups/ to .gitignore"));
|
|
2176
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
2177
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
2178
|
+
const port = startPort + i;
|
|
2179
|
+
if (await isPortAvailable(port)) {
|
|
2180
|
+
return port;
|
|
980
2181
|
}
|
|
981
|
-
} catch (error) {
|
|
982
|
-
console.log(chalk6.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
983
2182
|
}
|
|
2183
|
+
throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/commands/db/utils/formatters.ts
|
|
2187
|
+
function formatBytes(bytes) {
|
|
2188
|
+
if (bytes === 0) {
|
|
2189
|
+
return "0 B";
|
|
2190
|
+
}
|
|
2191
|
+
const k = 1024;
|
|
2192
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
2193
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2194
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
984
2195
|
}
|
|
985
|
-
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
await ensureBackupInGitignore();
|
|
995
|
-
return backupDir;
|
|
996
|
-
} catch (error) {
|
|
997
|
-
throw new Error(`Failed to create backup directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
998
|
-
}
|
|
2196
|
+
function formatTimestamp() {
|
|
2197
|
+
const now = /* @__PURE__ */ new Date();
|
|
2198
|
+
const year = now.getFullYear();
|
|
2199
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2200
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
2201
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
2202
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
2203
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
2204
|
+
return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
|
|
999
2205
|
}
|
|
2206
|
+
|
|
2207
|
+
// src/commands/db/utils/backup-files.ts
|
|
2208
|
+
import { promises as fs2 } from "fs";
|
|
2209
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2210
|
+
import path2 from "path";
|
|
2211
|
+
import chalk14 from "chalk";
|
|
2212
|
+
|
|
2213
|
+
// src/commands/db/utils/metadata.ts
|
|
2214
|
+
import { promises as fs } from "fs";
|
|
2215
|
+
import path from "path";
|
|
2216
|
+
import { promisify } from "util";
|
|
2217
|
+
import { exec } from "child_process";
|
|
2218
|
+
import chalk13 from "chalk";
|
|
2219
|
+
var execAsync = promisify(exec);
|
|
1000
2220
|
async function collectGitInfo() {
|
|
1001
2221
|
try {
|
|
1002
2222
|
const { stdout: isRepo } = await execAsync('git rev-parse --is-inside-work-tree 2>/dev/null || echo "false"');
|
|
@@ -1058,7 +2278,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
1058
2278
|
await pool.end();
|
|
1059
2279
|
}
|
|
1060
2280
|
} catch (error) {
|
|
1061
|
-
console.log(
|
|
2281
|
+
console.log(chalk13.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
1062
2282
|
return void 0;
|
|
1063
2283
|
}
|
|
1064
2284
|
}
|
|
@@ -1066,9 +2286,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
1066
2286
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
1067
2287
|
try {
|
|
1068
2288
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
1069
|
-
console.log(
|
|
2289
|
+
console.log(chalk13.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
1070
2290
|
} catch (error) {
|
|
1071
|
-
console.log(
|
|
2291
|
+
console.log(chalk13.dim("\u26A0\uFE0F Could not save metadata"));
|
|
1072
2292
|
}
|
|
1073
2293
|
}
|
|
1074
2294
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -1080,14 +2300,52 @@ async function loadBackupMetadata(backupFilename) {
|
|
|
1080
2300
|
return void 0;
|
|
1081
2301
|
}
|
|
1082
2302
|
}
|
|
2303
|
+
|
|
2304
|
+
// src/commands/db/utils/backup-files.ts
|
|
2305
|
+
async function ensureBackupInGitignore() {
|
|
2306
|
+
const gitignorePath = path2.join(process.cwd(), ".gitignore");
|
|
2307
|
+
try {
|
|
2308
|
+
let content = "";
|
|
2309
|
+
let exists = existsSync17(gitignorePath);
|
|
2310
|
+
if (exists) {
|
|
2311
|
+
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
2312
|
+
}
|
|
2313
|
+
const lines = content.split("\n");
|
|
2314
|
+
const hasBackupsIgnore = lines.some(
|
|
2315
|
+
(line) => line.trim() === "backups/" || line.trim() === "/backups/" || line.trim() === "backups"
|
|
2316
|
+
);
|
|
2317
|
+
if (!hasBackupsIgnore) {
|
|
2318
|
+
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
2319
|
+
await fs2.appendFile(gitignorePath, entry);
|
|
2320
|
+
console.log(chalk14.dim("\u2713 Added backups/ to .gitignore"));
|
|
2321
|
+
}
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
console.log(chalk14.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
async function ensureBackupDir() {
|
|
2327
|
+
const backupDir = path2.join(process.cwd(), "backups");
|
|
2328
|
+
try {
|
|
2329
|
+
await fs2.mkdir(backupDir, { recursive: true });
|
|
2330
|
+
const gitignorePath = path2.join(backupDir, ".gitignore");
|
|
2331
|
+
const gitignoreExists = existsSync17(gitignorePath);
|
|
2332
|
+
if (!gitignoreExists) {
|
|
2333
|
+
await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
|
|
2334
|
+
}
|
|
2335
|
+
await ensureBackupInGitignore();
|
|
2336
|
+
return backupDir;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
throw new Error(`Failed to create backup directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
1083
2341
|
async function listBackupFiles() {
|
|
1084
|
-
const backupDir =
|
|
2342
|
+
const backupDir = path2.join(process.cwd(), "backups");
|
|
1085
2343
|
try {
|
|
1086
|
-
const files = await
|
|
2344
|
+
const files = await fs2.readdir(backupDir);
|
|
1087
2345
|
const backups = await Promise.all(
|
|
1088
2346
|
files.filter((f) => f.endsWith(".sql") || f.endsWith(".dump")).map(async (f) => {
|
|
1089
|
-
const filepath =
|
|
1090
|
-
const stats = await
|
|
2347
|
+
const filepath = path2.join(backupDir, f);
|
|
2348
|
+
const stats = await fs2.stat(filepath);
|
|
1091
2349
|
const metadata = await loadBackupMetadata(filepath);
|
|
1092
2350
|
return {
|
|
1093
2351
|
name: f,
|
|
@@ -1107,93 +2365,136 @@ async function listBackupFiles() {
|
|
|
1107
2365
|
throw error;
|
|
1108
2366
|
}
|
|
1109
2367
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
1124
|
-
const configContent = generateDrizzleConfigFile({
|
|
1125
|
-
cwd: process.cwd(),
|
|
1126
|
-
// Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
|
|
1127
|
-
disablePackageDiscovery: true
|
|
1128
|
-
});
|
|
1129
|
-
writeFileSync4(tempConfigPath, configContent);
|
|
1130
|
-
console.log(chalk6.dim("Using auto-generated Drizzle config\n"));
|
|
1131
|
-
}
|
|
1132
|
-
const fullCommand = `drizzle-kit ${command} --config=${configPath}`;
|
|
1133
|
-
const { stdout, stderr } = await execAsync(fullCommand);
|
|
1134
|
-
if (stdout) {
|
|
1135
|
-
console.log(stdout);
|
|
1136
|
-
}
|
|
1137
|
-
if (stderr) {
|
|
1138
|
-
console.error(stderr);
|
|
1139
|
-
}
|
|
1140
|
-
} finally {
|
|
1141
|
-
if (!hasUserConfig && existsSync6(tempConfigPath)) {
|
|
1142
|
-
unlinkSync(tempConfigPath);
|
|
1143
|
-
}
|
|
2368
|
+
|
|
2369
|
+
// src/commands/db/backup.ts
|
|
2370
|
+
import { env as env3 } from "@spfn/core/config";
|
|
2371
|
+
import { loadEnvFiles as loadEnvFiles3 } from "@spfn/core/server";
|
|
2372
|
+
async function dbBackup(options) {
|
|
2373
|
+
console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2374
|
+
loadEnvFiles3();
|
|
2375
|
+
const dbUrl = env3.DATABASE_URL;
|
|
2376
|
+
if (!dbUrl) {
|
|
2377
|
+
console.error(chalk15.red("\u274C DATABASE_URL not found in environment"));
|
|
2378
|
+
console.log(chalk15.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2379
|
+
process.exit(1);
|
|
1144
2380
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
spinner.fail(failMessage);
|
|
1154
|
-
console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2381
|
+
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
2382
|
+
const backupDir = await ensureBackupDir();
|
|
2383
|
+
const timestamp = formatTimestamp();
|
|
2384
|
+
const format = options.format || "sql";
|
|
2385
|
+
const ext = format === "sql" ? "sql" : "dump";
|
|
2386
|
+
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
2387
|
+
if (options.dataOnly && options.schemaOnly) {
|
|
2388
|
+
console.error(chalk15.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
1155
2389
|
process.exit(1);
|
|
1156
2390
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
"
|
|
1161
|
-
|
|
1162
|
-
"
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
"
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
"
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2391
|
+
const args = [
|
|
2392
|
+
"-h",
|
|
2393
|
+
dbInfo.host,
|
|
2394
|
+
"-p",
|
|
2395
|
+
dbInfo.port,
|
|
2396
|
+
"-U",
|
|
2397
|
+
dbInfo.user,
|
|
2398
|
+
"-d",
|
|
2399
|
+
dbInfo.database,
|
|
2400
|
+
"-f",
|
|
2401
|
+
filename
|
|
2402
|
+
];
|
|
2403
|
+
if (format === "custom") {
|
|
2404
|
+
args.push("-Fc");
|
|
2405
|
+
}
|
|
2406
|
+
if (options.schema) {
|
|
2407
|
+
args.push("-n", options.schema);
|
|
2408
|
+
}
|
|
2409
|
+
if (options.dataOnly) {
|
|
2410
|
+
args.push("--data-only");
|
|
2411
|
+
}
|
|
2412
|
+
if (options.schemaOnly) {
|
|
2413
|
+
args.push("--schema-only");
|
|
2414
|
+
}
|
|
2415
|
+
const spinner = ora7("Creating backup...").start();
|
|
2416
|
+
const pgDump = spawn2("pg_dump", args, {
|
|
2417
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2418
|
+
env: {
|
|
2419
|
+
...process.env,
|
|
2420
|
+
PGPASSWORD: dbInfo.password
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
let errorOutput = "";
|
|
2424
|
+
pgDump.stderr?.on("data", (data) => {
|
|
2425
|
+
errorOutput += data.toString();
|
|
2426
|
+
});
|
|
2427
|
+
await new Promise((resolve2, reject) => {
|
|
2428
|
+
pgDump.on("close", async (code) => {
|
|
2429
|
+
if (code === 0) {
|
|
2430
|
+
try {
|
|
2431
|
+
const stats = await fs3.stat(filename);
|
|
2432
|
+
const size = formatBytes(stats.size);
|
|
2433
|
+
spinner.succeed("Backup created");
|
|
2434
|
+
console.log(chalk15.green(`
|
|
2435
|
+
\u2705 Backup created successfully`));
|
|
2436
|
+
console.log(chalk15.gray(` File: ${filename}`));
|
|
2437
|
+
console.log(chalk15.gray(` Size: ${size}`));
|
|
2438
|
+
console.log(chalk15.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
2439
|
+
const [gitInfo, migrationInfo] = await Promise.all([
|
|
2440
|
+
collectGitInfo(),
|
|
2441
|
+
collectMigrationInfo(dbUrl)
|
|
2442
|
+
]);
|
|
2443
|
+
const tags = [];
|
|
2444
|
+
if (options.tag) {
|
|
2445
|
+
tags.push(...options.tag.split(",").map((t) => t.trim()));
|
|
2446
|
+
}
|
|
2447
|
+
const metadata = {
|
|
2448
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2449
|
+
database: dbInfo.database,
|
|
2450
|
+
environment: options.env || process.env.NODE_ENV,
|
|
2451
|
+
git: gitInfo,
|
|
2452
|
+
migrations: migrationInfo,
|
|
2453
|
+
backup: {
|
|
2454
|
+
filename: path3.basename(filename),
|
|
2455
|
+
format,
|
|
2456
|
+
sizeBytes: stats.size,
|
|
2457
|
+
schema: options.schema,
|
|
2458
|
+
dataOnly: options.dataOnly,
|
|
2459
|
+
schemaOnly: options.schemaOnly
|
|
2460
|
+
},
|
|
2461
|
+
tags: tags.length > 0 ? tags : void 0
|
|
2462
|
+
};
|
|
2463
|
+
await saveBackupMetadata(metadata, filename);
|
|
2464
|
+
resolve2();
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
reject(error);
|
|
2467
|
+
}
|
|
2468
|
+
} else {
|
|
2469
|
+
spinner.fail("Backup failed");
|
|
2470
|
+
reject(new Error(errorOutput || "pg_dump failed"));
|
|
2471
|
+
}
|
|
1181
2472
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
2473
|
+
pgDump.on("error", (error) => {
|
|
2474
|
+
spinner.fail("Backup failed");
|
|
2475
|
+
reject(error);
|
|
2476
|
+
});
|
|
2477
|
+
}).catch((error) => {
|
|
2478
|
+
console.error(chalk15.red("\n\u274C Failed to create backup"));
|
|
2479
|
+
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
2480
|
+
console.error(chalk15.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
2481
|
+
} else {
|
|
2482
|
+
console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1189
2483
|
}
|
|
1190
|
-
|
|
2484
|
+
process.exit(1);
|
|
2485
|
+
});
|
|
1191
2486
|
}
|
|
2487
|
+
|
|
2488
|
+
// src/commands/db/migrate.ts
|
|
2489
|
+
import "@spfn/core/config";
|
|
1192
2490
|
async function dbMigrate(options = {}) {
|
|
1193
|
-
|
|
1194
|
-
|
|
2491
|
+
try {
|
|
2492
|
+
validateDatabasePrerequisites();
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
1195
2496
|
if (options.withBackup) {
|
|
1196
|
-
console.log(
|
|
2497
|
+
console.log(chalk16.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
1197
2498
|
await dbBackup({
|
|
1198
2499
|
format: "custom",
|
|
1199
2500
|
tag: "pre-migration",
|
|
@@ -1201,19 +2502,19 @@ async function dbMigrate(options = {}) {
|
|
|
1201
2502
|
});
|
|
1202
2503
|
console.log("");
|
|
1203
2504
|
}
|
|
1204
|
-
const { discoverFunctionMigrations, executeFunctionMigrations } = await
|
|
1205
|
-
const functions =
|
|
2505
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2506
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
1206
2507
|
if (functions.length > 0) {
|
|
1207
|
-
console.log(
|
|
2508
|
+
console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
|
|
1208
2509
|
functions.forEach((func) => {
|
|
1209
|
-
console.log(
|
|
2510
|
+
console.log(chalk16.dim(` - ${func.packageName}`));
|
|
1210
2511
|
});
|
|
1211
2512
|
try {
|
|
1212
|
-
await
|
|
1213
|
-
console.log(
|
|
2513
|
+
await executeFunctionMigrations2(functions);
|
|
2514
|
+
console.log(chalk16.green("\u2705 Function migrations applied\n"));
|
|
1214
2515
|
} catch (error) {
|
|
1215
|
-
console.error(
|
|
1216
|
-
console.error(
|
|
2516
|
+
console.error(chalk16.red("\n\u274C Failed to apply function migrations"));
|
|
2517
|
+
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1217
2518
|
process.exit(1);
|
|
1218
2519
|
}
|
|
1219
2520
|
}
|
|
@@ -1224,66 +2525,73 @@ async function dbMigrate(options = {}) {
|
|
|
1224
2525
|
"Failed to run project migrations"
|
|
1225
2526
|
);
|
|
1226
2527
|
}
|
|
2528
|
+
|
|
2529
|
+
// src/commands/db/studio.ts
|
|
2530
|
+
import chalk17 from "chalk";
|
|
2531
|
+
import { existsSync as existsSync18, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
|
|
2532
|
+
import { spawn as spawn3 } from "child_process";
|
|
2533
|
+
import { env as env4 } from "@spfn/core/config";
|
|
2534
|
+
import "@spfn/core/config";
|
|
1227
2535
|
async function dbStudio(requestedPort) {
|
|
1228
|
-
console.log(
|
|
1229
|
-
const { loadEnvironment } = await import("@spfn/core/env");
|
|
1230
|
-
loadEnvironment({ debug: false });
|
|
2536
|
+
console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
1231
2537
|
const defaultPort = 4983;
|
|
1232
2538
|
const startPort = requestedPort || defaultPort;
|
|
1233
2539
|
let port;
|
|
1234
2540
|
try {
|
|
1235
2541
|
port = await findAvailablePort(startPort);
|
|
1236
2542
|
if (port !== startPort) {
|
|
1237
|
-
console.log(
|
|
2543
|
+
console.log(chalk17.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
1238
2544
|
`));
|
|
1239
2545
|
}
|
|
1240
2546
|
} catch (error) {
|
|
1241
|
-
console.error(
|
|
2547
|
+
console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
1242
2548
|
process.exit(1);
|
|
1243
2549
|
}
|
|
1244
|
-
const hasUserConfig =
|
|
2550
|
+
const hasUserConfig = existsSync18("./drizzle.config.ts");
|
|
1245
2551
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
1246
2552
|
try {
|
|
1247
2553
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
1248
2554
|
if (!hasUserConfig) {
|
|
1249
|
-
if (!
|
|
1250
|
-
console.error(
|
|
1251
|
-
console.log(
|
|
2555
|
+
if (!env4.DATABASE_URL) {
|
|
2556
|
+
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2557
|
+
console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
1252
2558
|
process.exit(1);
|
|
1253
2559
|
}
|
|
1254
2560
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
1255
2561
|
const configContent = generateDrizzleConfigFile({
|
|
1256
2562
|
cwd: process.cwd(),
|
|
1257
|
-
disablePackageDiscovery: true
|
|
2563
|
+
disablePackageDiscovery: true,
|
|
2564
|
+
expandGlobs: true
|
|
2565
|
+
// Expand glob patterns for Studio compatibility
|
|
1258
2566
|
});
|
|
1259
|
-
|
|
1260
|
-
console.log(
|
|
2567
|
+
writeFileSync10(tempConfigPath, configContent);
|
|
2568
|
+
console.log(chalk17.dim("Using auto-generated Drizzle config\n"));
|
|
1261
2569
|
}
|
|
1262
|
-
const studioProcess =
|
|
2570
|
+
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
1263
2571
|
stdio: "inherit",
|
|
1264
2572
|
shell: true
|
|
1265
2573
|
});
|
|
1266
2574
|
const cleanup = () => {
|
|
1267
|
-
if (!hasUserConfig &&
|
|
1268
|
-
|
|
2575
|
+
if (!hasUserConfig && existsSync18(tempConfigPath)) {
|
|
2576
|
+
unlinkSync2(tempConfigPath);
|
|
1269
2577
|
}
|
|
1270
2578
|
};
|
|
1271
2579
|
studioProcess.on("exit", (code) => {
|
|
1272
2580
|
cleanup();
|
|
1273
2581
|
if (code !== 0 && code !== null) {
|
|
1274
|
-
console.error(
|
|
2582
|
+
console.error(chalk17.red(`
|
|
1275
2583
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
1276
2584
|
process.exit(code);
|
|
1277
2585
|
}
|
|
1278
2586
|
});
|
|
1279
2587
|
studioProcess.on("error", (error) => {
|
|
1280
2588
|
cleanup();
|
|
1281
|
-
console.error(
|
|
1282
|
-
console.error(
|
|
2589
|
+
console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
|
|
2590
|
+
console.error(chalk17.red(error.message));
|
|
1283
2591
|
process.exit(1);
|
|
1284
2592
|
});
|
|
1285
2593
|
process.on("SIGINT", () => {
|
|
1286
|
-
console.log(
|
|
2594
|
+
console.log(chalk17.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
1287
2595
|
studioProcess.kill("SIGTERM");
|
|
1288
2596
|
cleanup();
|
|
1289
2597
|
process.exit(0);
|
|
@@ -1294,24 +2602,28 @@ async function dbStudio(requestedPort) {
|
|
|
1294
2602
|
process.exit(0);
|
|
1295
2603
|
});
|
|
1296
2604
|
} catch (error) {
|
|
1297
|
-
if (!hasUserConfig &&
|
|
1298
|
-
|
|
2605
|
+
if (!hasUserConfig && existsSync18(tempConfigPath)) {
|
|
2606
|
+
unlinkSync2(tempConfigPath);
|
|
1299
2607
|
}
|
|
1300
|
-
console.error(
|
|
1301
|
-
console.error(
|
|
2608
|
+
console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
|
|
2609
|
+
console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1302
2610
|
process.exit(1);
|
|
1303
2611
|
}
|
|
1304
2612
|
}
|
|
2613
|
+
|
|
2614
|
+
// src/commands/db/drop.ts
|
|
2615
|
+
import chalk18 from "chalk";
|
|
2616
|
+
import prompts3 from "prompts";
|
|
1305
2617
|
async function dbDrop() {
|
|
1306
|
-
console.log(
|
|
1307
|
-
const { confirm } = await
|
|
2618
|
+
console.log(chalk18.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
2619
|
+
const { confirm } = await prompts3({
|
|
1308
2620
|
type: "confirm",
|
|
1309
2621
|
name: "confirm",
|
|
1310
2622
|
message: "Are you sure you want to drop all tables?",
|
|
1311
2623
|
initial: false
|
|
1312
2624
|
});
|
|
1313
2625
|
if (!confirm) {
|
|
1314
|
-
console.log(
|
|
2626
|
+
console.log(chalk18.gray("Cancelled."));
|
|
1315
2627
|
process.exit(0);
|
|
1316
2628
|
}
|
|
1317
2629
|
await runWithSpinner(
|
|
@@ -1321,144 +2633,47 @@ async function dbDrop() {
|
|
|
1321
2633
|
"Failed to drop tables"
|
|
1322
2634
|
);
|
|
1323
2635
|
}
|
|
2636
|
+
|
|
2637
|
+
// src/commands/db/check.ts
|
|
2638
|
+
import chalk19 from "chalk";
|
|
2639
|
+
import ora8 from "ora";
|
|
1324
2640
|
async function dbCheck() {
|
|
1325
|
-
const spinner =
|
|
2641
|
+
const spinner = ora8("Checking database connection...").start();
|
|
1326
2642
|
try {
|
|
1327
2643
|
await runDrizzleCommand("check");
|
|
1328
2644
|
spinner.succeed("Database connection OK");
|
|
1329
2645
|
} catch (error) {
|
|
1330
2646
|
spinner.fail("Database connection failed");
|
|
1331
|
-
console.error(
|
|
1332
|
-
process.exit(1);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
async function dbBackup(options) {
|
|
1336
|
-
console.log(chalk6.blue("\u{1F4BE} Creating database backup...\n"));
|
|
1337
|
-
const { loadEnvironment } = await import("@spfn/core/env");
|
|
1338
|
-
loadEnvironment({ debug: false });
|
|
1339
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
1340
|
-
if (!dbUrl) {
|
|
1341
|
-
console.error(chalk6.red("\u274C DATABASE_URL not found in environment"));
|
|
1342
|
-
console.log(chalk6.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
1343
|
-
process.exit(1);
|
|
1344
|
-
}
|
|
1345
|
-
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
1346
|
-
const backupDir = await ensureBackupDir();
|
|
1347
|
-
const timestamp = formatTimestamp();
|
|
1348
|
-
const format = options.format || "sql";
|
|
1349
|
-
const ext = format === "sql" ? "sql" : "dump";
|
|
1350
|
-
const filename = options.output || path.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
1351
|
-
if (options.dataOnly && options.schemaOnly) {
|
|
1352
|
-
console.error(chalk6.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2647
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1353
2648
|
process.exit(1);
|
|
1354
2649
|
}
|
|
1355
|
-
const args = [
|
|
1356
|
-
"-h",
|
|
1357
|
-
dbInfo.host,
|
|
1358
|
-
"-p",
|
|
1359
|
-
dbInfo.port,
|
|
1360
|
-
"-U",
|
|
1361
|
-
dbInfo.user,
|
|
1362
|
-
"-d",
|
|
1363
|
-
dbInfo.database,
|
|
1364
|
-
"-f",
|
|
1365
|
-
filename
|
|
1366
|
-
];
|
|
1367
|
-
if (format === "custom") {
|
|
1368
|
-
args.push("-Fc");
|
|
1369
|
-
}
|
|
1370
|
-
if (options.schema) {
|
|
1371
|
-
args.push("-n", options.schema);
|
|
1372
|
-
}
|
|
1373
|
-
if (options.dataOnly) {
|
|
1374
|
-
args.push("--data-only");
|
|
1375
|
-
}
|
|
1376
|
-
if (options.schemaOnly) {
|
|
1377
|
-
args.push("--schema-only");
|
|
1378
|
-
}
|
|
1379
|
-
const spinner = ora3("Creating backup...").start();
|
|
1380
|
-
const pgDump = spawn("pg_dump", args, {
|
|
1381
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1382
|
-
env: {
|
|
1383
|
-
...process.env,
|
|
1384
|
-
PGPASSWORD: dbInfo.password
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1387
|
-
let errorOutput = "";
|
|
1388
|
-
pgDump.stderr?.on("data", (data) => {
|
|
1389
|
-
errorOutput += data.toString();
|
|
1390
|
-
});
|
|
1391
|
-
pgDump.on("close", async (code) => {
|
|
1392
|
-
if (code === 0) {
|
|
1393
|
-
const stats = await fs.stat(filename);
|
|
1394
|
-
const size = formatBytes(stats.size);
|
|
1395
|
-
spinner.succeed("Backup created");
|
|
1396
|
-
console.log(chalk6.green(`
|
|
1397
|
-
\u2705 Backup created successfully`));
|
|
1398
|
-
console.log(chalk6.gray(` File: ${filename}`));
|
|
1399
|
-
console.log(chalk6.gray(` Size: ${size}`));
|
|
1400
|
-
console.log(chalk6.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
1401
|
-
const [gitInfo, migrationInfo] = await Promise.all([
|
|
1402
|
-
collectGitInfo(),
|
|
1403
|
-
collectMigrationInfo(dbUrl)
|
|
1404
|
-
]);
|
|
1405
|
-
const tags = [];
|
|
1406
|
-
if (options.tag) {
|
|
1407
|
-
tags.push(...options.tag.split(",").map((t) => t.trim()));
|
|
1408
|
-
}
|
|
1409
|
-
const metadata = {
|
|
1410
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1411
|
-
database: dbInfo.database,
|
|
1412
|
-
environment: options.env || process.env.NODE_ENV,
|
|
1413
|
-
git: gitInfo,
|
|
1414
|
-
migrations: migrationInfo,
|
|
1415
|
-
backup: {
|
|
1416
|
-
filename: path.basename(filename),
|
|
1417
|
-
format,
|
|
1418
|
-
sizeBytes: stats.size,
|
|
1419
|
-
schema: options.schema,
|
|
1420
|
-
dataOnly: options.dataOnly,
|
|
1421
|
-
schemaOnly: options.schemaOnly
|
|
1422
|
-
},
|
|
1423
|
-
tags: tags.length > 0 ? tags : void 0
|
|
1424
|
-
};
|
|
1425
|
-
await saveBackupMetadata(metadata, filename);
|
|
1426
|
-
} else {
|
|
1427
|
-
spinner.fail("Backup failed");
|
|
1428
|
-
console.error(chalk6.red("\n\u274C Failed to create backup"));
|
|
1429
|
-
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
1430
|
-
console.error(chalk6.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
1431
|
-
} else if (errorOutput) {
|
|
1432
|
-
console.error(chalk6.red(errorOutput));
|
|
1433
|
-
}
|
|
1434
|
-
process.exit(1);
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
pgDump.on("error", (error) => {
|
|
1438
|
-
spinner.fail("Backup failed");
|
|
1439
|
-
console.error(chalk6.red("\n\u274C Failed to start pg_dump"));
|
|
1440
|
-
console.error(chalk6.red(error.message));
|
|
1441
|
-
process.exit(1);
|
|
1442
|
-
});
|
|
1443
2650
|
}
|
|
2651
|
+
|
|
2652
|
+
// src/commands/db/restore.ts
|
|
2653
|
+
import path4 from "path";
|
|
2654
|
+
import { spawn as spawn4 } from "child_process";
|
|
2655
|
+
import chalk20 from "chalk";
|
|
2656
|
+
import ora9 from "ora";
|
|
2657
|
+
import prompts4 from "prompts";
|
|
2658
|
+
import { env as env5 } from "@spfn/core/config";
|
|
2659
|
+
import { loadEnvFiles as loadEnvFiles4 } from "@spfn/core/server";
|
|
1444
2660
|
async function dbRestore(backupFile, options = {}) {
|
|
1445
|
-
console.log(
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
2661
|
+
console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
2662
|
+
loadEnvFiles4();
|
|
2663
|
+
const dbUrl = env5.DATABASE_URL;
|
|
1449
2664
|
if (!dbUrl) {
|
|
1450
|
-
console.error(
|
|
1451
|
-
console.log(
|
|
2665
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
2666
|
+
console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
1452
2667
|
process.exit(1);
|
|
1453
2668
|
}
|
|
1454
2669
|
let file = backupFile;
|
|
1455
2670
|
if (!file) {
|
|
1456
2671
|
const backups = await listBackupFiles();
|
|
1457
2672
|
if (backups.length === 0) {
|
|
1458
|
-
console.log(
|
|
2673
|
+
console.log(chalk20.yellow("No backups found in ./backups directory"));
|
|
1459
2674
|
process.exit(0);
|
|
1460
2675
|
}
|
|
1461
|
-
const { selected } = await
|
|
2676
|
+
const { selected } = await prompts4({
|
|
1462
2677
|
type: "select",
|
|
1463
2678
|
name: "selected",
|
|
1464
2679
|
message: "Select backup to restore:",
|
|
@@ -1468,74 +2683,74 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
1468
2683
|
}))
|
|
1469
2684
|
});
|
|
1470
2685
|
if (!selected) {
|
|
1471
|
-
console.log(
|
|
2686
|
+
console.log(chalk20.gray("Cancelled"));
|
|
1472
2687
|
process.exit(0);
|
|
1473
2688
|
}
|
|
1474
2689
|
file = selected;
|
|
1475
2690
|
}
|
|
1476
2691
|
if (!file) {
|
|
1477
|
-
console.error(
|
|
2692
|
+
console.error(chalk20.red("\u274C No backup file selected"));
|
|
1478
2693
|
process.exit(1);
|
|
1479
2694
|
}
|
|
1480
2695
|
const metadata = await loadBackupMetadata(file);
|
|
1481
2696
|
if (metadata) {
|
|
1482
|
-
console.log(
|
|
1483
|
-
console.log(
|
|
1484
|
-
console.log(
|
|
2697
|
+
console.log(chalk20.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
2698
|
+
console.log(chalk20.dim(` Database: ${metadata.database}`));
|
|
2699
|
+
console.log(chalk20.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
1485
2700
|
if (metadata.environment) {
|
|
1486
|
-
console.log(
|
|
2701
|
+
console.log(chalk20.dim(` Environment: ${metadata.environment}`));
|
|
1487
2702
|
}
|
|
1488
2703
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
1489
|
-
console.log(
|
|
2704
|
+
console.log(chalk20.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
1490
2705
|
}
|
|
1491
2706
|
if (metadata.backup.dataOnly) {
|
|
1492
|
-
console.log(
|
|
2707
|
+
console.log(chalk20.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
1493
2708
|
}
|
|
1494
2709
|
if (metadata.backup.schemaOnly) {
|
|
1495
|
-
console.log(
|
|
2710
|
+
console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
1496
2711
|
}
|
|
1497
|
-
const
|
|
2712
|
+
const warnings2 = [];
|
|
1498
2713
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
1499
2714
|
collectGitInfo(),
|
|
1500
2715
|
collectMigrationInfo(dbUrl)
|
|
1501
2716
|
]);
|
|
1502
2717
|
if (metadata.git && currentGitInfo) {
|
|
1503
2718
|
if (metadata.git.commit !== currentGitInfo.commit) {
|
|
1504
|
-
|
|
2719
|
+
warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
|
|
1505
2720
|
}
|
|
1506
2721
|
if (metadata.git.branch !== currentGitInfo.branch) {
|
|
1507
|
-
|
|
2722
|
+
warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
|
|
1508
2723
|
}
|
|
1509
2724
|
}
|
|
1510
2725
|
if (metadata.migrations && currentMigrationInfo) {
|
|
1511
2726
|
if (metadata.migrations.hash !== currentMigrationInfo.hash) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2727
|
+
warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
|
|
2728
|
+
warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
|
|
2729
|
+
warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
|
|
1515
2730
|
}
|
|
1516
2731
|
}
|
|
1517
|
-
if (
|
|
1518
|
-
console.log(
|
|
1519
|
-
|
|
2732
|
+
if (warnings2.length > 0) {
|
|
2733
|
+
console.log(chalk20.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
2734
|
+
warnings2.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
|
|
1520
2735
|
console.log("");
|
|
1521
2736
|
}
|
|
1522
2737
|
}
|
|
1523
|
-
const { confirm } = await
|
|
2738
|
+
const { confirm } = await prompts4({
|
|
1524
2739
|
type: "confirm",
|
|
1525
2740
|
name: "confirm",
|
|
1526
|
-
message:
|
|
2741
|
+
message: chalk20.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
|
|
1527
2742
|
initial: false
|
|
1528
2743
|
});
|
|
1529
2744
|
if (!confirm) {
|
|
1530
|
-
console.log(
|
|
2745
|
+
console.log(chalk20.gray("Cancelled"));
|
|
1531
2746
|
process.exit(0);
|
|
1532
2747
|
}
|
|
1533
2748
|
if (options.dataOnly && options.schemaOnly) {
|
|
1534
|
-
console.error(
|
|
2749
|
+
console.error(chalk20.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
1535
2750
|
process.exit(1);
|
|
1536
2751
|
}
|
|
1537
2752
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
1538
|
-
const ext =
|
|
2753
|
+
const ext = path4.extname(file);
|
|
1539
2754
|
const isCustomFormat = ext === ".dump";
|
|
1540
2755
|
const command = isCustomFormat ? "pg_restore" : "psql";
|
|
1541
2756
|
const args = [];
|
|
@@ -1544,6 +2759,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
1544
2759
|
args.push("-p", dbInfo.port);
|
|
1545
2760
|
args.push("-U", dbInfo.user);
|
|
1546
2761
|
args.push("-d", dbInfo.database);
|
|
2762
|
+
args.push("--verbose");
|
|
1547
2763
|
if (options.drop) {
|
|
1548
2764
|
args.push("--clean");
|
|
1549
2765
|
}
|
|
@@ -1559,58 +2775,128 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
1559
2775
|
args.push(file);
|
|
1560
2776
|
} else {
|
|
1561
2777
|
if (options.dataOnly || options.schemaOnly) {
|
|
1562
|
-
console.log(
|
|
1563
|
-
console.log(
|
|
2778
|
+
console.log(chalk20.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
2779
|
+
console.log(chalk20.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
1564
2780
|
}
|
|
1565
2781
|
args.push("-h", dbInfo.host);
|
|
1566
2782
|
args.push("-p", dbInfo.port);
|
|
1567
2783
|
args.push("-U", dbInfo.user);
|
|
1568
2784
|
args.push("-d", dbInfo.database);
|
|
2785
|
+
args.push("-v", "ON_ERROR_STOP=1");
|
|
1569
2786
|
args.push("-f", file);
|
|
1570
2787
|
}
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
2788
|
+
const verbose = options.verbose ?? false;
|
|
2789
|
+
const spinner = ora9("Restoring backup...").start();
|
|
2790
|
+
const restoreProcess = spawn4(command, args, {
|
|
1573
2791
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1574
2792
|
env: {
|
|
1575
2793
|
...process.env,
|
|
1576
2794
|
PGPASSWORD: dbInfo.password
|
|
1577
2795
|
}
|
|
1578
2796
|
});
|
|
1579
|
-
|
|
2797
|
+
const warnings = [];
|
|
2798
|
+
const errors = [];
|
|
2799
|
+
let objectCount = 0;
|
|
2800
|
+
let lastObject = "";
|
|
1580
2801
|
restoreProcess.stderr?.on("data", (data) => {
|
|
1581
|
-
|
|
2802
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
2803
|
+
for (const line of lines) {
|
|
2804
|
+
if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
|
|
2805
|
+
warnings.push(line.trim());
|
|
2806
|
+
} else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
|
|
2807
|
+
errors.push(line.trim());
|
|
2808
|
+
}
|
|
2809
|
+
const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
|
|
2810
|
+
if (objectMatch) {
|
|
2811
|
+
objectCount = Number(objectMatch[2]);
|
|
2812
|
+
const current = Number(objectMatch[1]);
|
|
2813
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
2814
|
+
lastObject = desc;
|
|
2815
|
+
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
2816
|
+
} else if (isCustomFormat) {
|
|
2817
|
+
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
2818
|
+
if (desc && !/warning:|error:/i.test(desc)) {
|
|
2819
|
+
lastObject = desc;
|
|
2820
|
+
spinner.text = `Restoring backup... ${desc}`;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (verbose) {
|
|
2824
|
+
spinner.stop();
|
|
2825
|
+
console.log(chalk20.dim(` ${line.trim()}`));
|
|
2826
|
+
spinner.start();
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
1582
2829
|
});
|
|
1583
|
-
restoreProcess.on("
|
|
1584
|
-
if (
|
|
1585
|
-
spinner.
|
|
1586
|
-
console.log(
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2830
|
+
restoreProcess.stdout?.on("data", (data) => {
|
|
2831
|
+
if (verbose) {
|
|
2832
|
+
spinner.stop();
|
|
2833
|
+
console.log(chalk20.dim(` ${data.toString().trim()}`));
|
|
2834
|
+
spinner.start();
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
await new Promise((resolve2, reject) => {
|
|
2838
|
+
restoreProcess.on("close", (code) => {
|
|
2839
|
+
if (code === 0) {
|
|
2840
|
+
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
2841
|
+
spinner.succeed(`Restore completed${summary}`);
|
|
2842
|
+
if (warnings.length > 0) {
|
|
2843
|
+
console.log(chalk20.yellow(`
|
|
2844
|
+
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
2845
|
+
`));
|
|
2846
|
+
for (const w of warnings) {
|
|
2847
|
+
console.log(chalk20.yellow(` - ${w}`));
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
console.log(chalk20.green("\n\u2705 Database restored successfully"));
|
|
2851
|
+
resolve2();
|
|
2852
|
+
} else {
|
|
2853
|
+
spinner.fail("Restore failed");
|
|
2854
|
+
if (errors.length > 0) {
|
|
2855
|
+
console.error(chalk20.red(`
|
|
2856
|
+
\u274C Errors (${errors.length}):
|
|
2857
|
+
`));
|
|
2858
|
+
for (const e of errors) {
|
|
2859
|
+
console.error(chalk20.red(` - ${e}`));
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (warnings.length > 0) {
|
|
2863
|
+
console.log(chalk20.yellow(`
|
|
2864
|
+
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
2865
|
+
`));
|
|
2866
|
+
for (const w of warnings) {
|
|
2867
|
+
console.log(chalk20.yellow(` - ${w}`));
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
2871
|
+
reject(new Error(fallback));
|
|
1592
2872
|
}
|
|
1593
|
-
|
|
2873
|
+
});
|
|
2874
|
+
restoreProcess.on("error", (error) => {
|
|
2875
|
+
spinner.fail("Restore failed");
|
|
2876
|
+
reject(error);
|
|
2877
|
+
});
|
|
2878
|
+
}).catch((error) => {
|
|
2879
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
2880
|
+
if (msg) {
|
|
2881
|
+
console.error(chalk20.red(`
|
|
2882
|
+
\u274C ${msg}`));
|
|
1594
2883
|
}
|
|
1595
|
-
});
|
|
1596
|
-
restoreProcess.on("error", (error) => {
|
|
1597
|
-
spinner.fail("Restore failed");
|
|
1598
|
-
console.error(chalk6.red(`
|
|
1599
|
-
\u274C Failed to start ${command}`));
|
|
1600
|
-
console.error(chalk6.red(error.message));
|
|
1601
2884
|
process.exit(1);
|
|
1602
2885
|
});
|
|
1603
2886
|
}
|
|
2887
|
+
|
|
2888
|
+
// src/commands/db/list.ts
|
|
2889
|
+
import chalk21 from "chalk";
|
|
1604
2890
|
async function dbBackupList() {
|
|
1605
|
-
console.log(
|
|
2891
|
+
console.log(chalk21.blue("\u{1F4CB} Database backups:\n"));
|
|
1606
2892
|
const backups = await listBackupFiles();
|
|
1607
2893
|
if (backups.length === 0) {
|
|
1608
|
-
console.log(
|
|
1609
|
-
console.log(
|
|
2894
|
+
console.log(chalk21.yellow("No backups found in ./backups directory"));
|
|
2895
|
+
console.log(chalk21.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
1610
2896
|
return;
|
|
1611
2897
|
}
|
|
1612
|
-
console.log(
|
|
1613
|
-
console.log(
|
|
2898
|
+
console.log(chalk21.bold(" Date Size File"));
|
|
2899
|
+
console.log(chalk21.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"));
|
|
1614
2900
|
backups.forEach((backup) => {
|
|
1615
2901
|
const date = backup.date.toLocaleString("en-US", {
|
|
1616
2902
|
year: "numeric",
|
|
@@ -1621,17 +2907,23 @@ async function dbBackupList() {
|
|
|
1621
2907
|
second: "2-digit"
|
|
1622
2908
|
});
|
|
1623
2909
|
const sizeStr = backup.size.padEnd(10);
|
|
1624
|
-
console.log(
|
|
2910
|
+
console.log(chalk21.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
1625
2911
|
});
|
|
1626
|
-
console.log(
|
|
2912
|
+
console.log(chalk21.gray(`
|
|
1627
2913
|
Total: ${backups.length} backup(s)
|
|
1628
2914
|
`));
|
|
1629
2915
|
}
|
|
2916
|
+
|
|
2917
|
+
// src/commands/db/clean.ts
|
|
2918
|
+
import { promises as fs4 } from "fs";
|
|
2919
|
+
import chalk22 from "chalk";
|
|
2920
|
+
import ora10 from "ora";
|
|
2921
|
+
import prompts5 from "prompts";
|
|
1630
2922
|
async function dbBackupClean(options) {
|
|
1631
|
-
console.log(
|
|
2923
|
+
console.log(chalk22.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
1632
2924
|
const backups = await listBackupFiles();
|
|
1633
2925
|
if (backups.length === 0) {
|
|
1634
|
-
console.log(
|
|
2926
|
+
console.log(chalk22.yellow("No backups found"));
|
|
1635
2927
|
return;
|
|
1636
2928
|
}
|
|
1637
2929
|
let toDelete = [];
|
|
@@ -1648,277 +2940,39 @@ async function dbBackupClean(options) {
|
|
|
1648
2940
|
toDelete = backups.slice(defaultKeep);
|
|
1649
2941
|
}
|
|
1650
2942
|
if (toDelete.length === 0) {
|
|
1651
|
-
console.log(
|
|
2943
|
+
console.log(chalk22.green("\u2705 No backups to clean"));
|
|
1652
2944
|
return;
|
|
1653
2945
|
}
|
|
1654
|
-
console.log(
|
|
2946
|
+
console.log(chalk22.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
1655
2947
|
`));
|
|
1656
2948
|
toDelete.forEach((backup) => {
|
|
1657
|
-
console.log(
|
|
2949
|
+
console.log(chalk22.gray(` - ${backup.name} (${backup.size})`));
|
|
1658
2950
|
});
|
|
1659
|
-
const { confirm } = await
|
|
2951
|
+
const { confirm } = await prompts5({
|
|
1660
2952
|
type: "confirm",
|
|
1661
2953
|
name: "confirm",
|
|
1662
2954
|
message: "\nProceed with deletion?",
|
|
1663
2955
|
initial: false
|
|
1664
2956
|
});
|
|
1665
2957
|
if (!confirm) {
|
|
1666
|
-
console.log(
|
|
2958
|
+
console.log(chalk22.gray("Cancelled"));
|
|
1667
2959
|
return;
|
|
1668
2960
|
}
|
|
1669
|
-
const spinner =
|
|
2961
|
+
const spinner = ora10("Deleting backups...").start();
|
|
1670
2962
|
try {
|
|
1671
|
-
await Promise.all(toDelete.map((backup) =>
|
|
2963
|
+
await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
|
|
1672
2964
|
spinner.succeed("Backups deleted");
|
|
1673
|
-
console.log(
|
|
2965
|
+
console.log(chalk22.green(`
|
|
1674
2966
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
1675
2967
|
} catch (error) {
|
|
1676
2968
|
spinner.fail("Failed to delete backups");
|
|
1677
|
-
console.error(
|
|
1678
|
-
process.exit(1);
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
async function dbSync(target, options) {
|
|
1682
|
-
console.log(chalk6.blue("\u{1F504} Database sync\n"));
|
|
1683
|
-
const { loadEnvironment } = await import("@spfn/core/env");
|
|
1684
|
-
loadEnvironment({ debug: false });
|
|
1685
|
-
const {
|
|
1686
|
-
validateSyncEnvironments,
|
|
1687
|
-
testDatabaseConnection,
|
|
1688
|
-
getDatabaseInfo,
|
|
1689
|
-
isProductionLike,
|
|
1690
|
-
getAvailableSyncTargets
|
|
1691
|
-
} = await import("./db-sync-NBLWUZMH.js");
|
|
1692
|
-
const sourceName = options.pull ? target : "local";
|
|
1693
|
-
const targetName = options.pull ? "local" : target;
|
|
1694
|
-
let source, targetEnv;
|
|
1695
|
-
try {
|
|
1696
|
-
const envs = await validateSyncEnvironments(sourceName, targetName);
|
|
1697
|
-
source = envs.source;
|
|
1698
|
-
targetEnv = envs.target;
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
console.error(chalk6.red(`\u274C ${error instanceof Error ? error.message : "Environment validation failed"}`));
|
|
1701
|
-
const available = getAvailableSyncTargets();
|
|
1702
|
-
if (available.length > 0) {
|
|
1703
|
-
console.log(chalk6.yellow(`
|
|
1704
|
-
\u{1F4A1} Available sync targets: ${available.join(", ")}`));
|
|
1705
|
-
console.log(chalk6.dim(` Configure in .env: SPFN_DB_${target.toUpperCase()}=postgresql://...`));
|
|
1706
|
-
} else {
|
|
1707
|
-
console.log(chalk6.yellow("\n\u{1F4A1} No sync targets configured"));
|
|
1708
|
-
console.log(chalk6.dim(" Add to .env: SPFN_DB_DEV=postgresql://..."));
|
|
1709
|
-
}
|
|
1710
|
-
process.exit(1);
|
|
1711
|
-
}
|
|
1712
|
-
if (isProductionLike(targetEnv.name) && !options.force) {
|
|
1713
|
-
console.error(chalk6.red(`\u274C Cannot sync to production-like environment '${targetEnv.name}' without --force flag`));
|
|
1714
|
-
console.log(chalk6.yellow(" This is a safety measure to prevent accidental data loss"));
|
|
1715
|
-
console.log(chalk6.dim(" Use --force if you really want to do this"));
|
|
1716
|
-
process.exit(1);
|
|
1717
|
-
}
|
|
1718
|
-
console.log(chalk6.dim("Testing database connections..."));
|
|
1719
|
-
const spinner = ora3("Connecting to source database...").start();
|
|
1720
|
-
const sourceConnected = await testDatabaseConnection(source);
|
|
1721
|
-
if (!sourceConnected) {
|
|
1722
|
-
spinner.fail("Failed to connect to source database");
|
|
1723
|
-
console.error(chalk6.red(`\u274C Cannot connect to ${source.name} database`));
|
|
1724
|
-
process.exit(1);
|
|
1725
|
-
}
|
|
1726
|
-
spinner.text = "Connecting to target database...";
|
|
1727
|
-
const targetConnected = await testDatabaseConnection(targetEnv);
|
|
1728
|
-
if (!targetConnected) {
|
|
1729
|
-
spinner.fail("Failed to connect to target database");
|
|
1730
|
-
console.error(chalk6.red(`\u274C Cannot connect to ${targetEnv.name} database`));
|
|
1731
|
-
process.exit(1);
|
|
1732
|
-
}
|
|
1733
|
-
spinner.succeed("Database connections OK");
|
|
1734
|
-
console.log(chalk6.dim("\nCollecting database information..."));
|
|
1735
|
-
const [sourceInfo, targetInfo] = await Promise.all([
|
|
1736
|
-
getDatabaseInfo(source),
|
|
1737
|
-
getDatabaseInfo(targetEnv)
|
|
1738
|
-
]);
|
|
1739
|
-
console.log(chalk6.blue("\n\u{1F4CB} Sync Plan:\n"));
|
|
1740
|
-
console.log(chalk6.white(` Source: ${chalk6.cyan(source.name)} (${source.connection.database})`));
|
|
1741
|
-
console.log(chalk6.dim(` ${sourceInfo.tableCount} tables, ${sourceInfo.size}`));
|
|
1742
|
-
console.log("");
|
|
1743
|
-
console.log(chalk6.white(` Target: ${chalk6.yellow(targetEnv.name)} (${targetEnv.connection.database})`));
|
|
1744
|
-
console.log(chalk6.dim(` ${targetInfo.tableCount} tables, ${targetInfo.size}`));
|
|
1745
|
-
console.log("");
|
|
1746
|
-
if (options.tables) {
|
|
1747
|
-
console.log(chalk6.dim(` Tables: ${options.tables}`));
|
|
1748
|
-
}
|
|
1749
|
-
if (options.excludeTables) {
|
|
1750
|
-
console.log(chalk6.dim(` Exclude: ${options.excludeTables}`));
|
|
1751
|
-
}
|
|
1752
|
-
if (options.dataOnly) {
|
|
1753
|
-
console.log(chalk6.yellow(" \u26A0\uFE0F Data only (schema will not be changed)"));
|
|
1754
|
-
}
|
|
1755
|
-
if (options.schemaOnly) {
|
|
1756
|
-
console.log(chalk6.yellow(" \u26A0\uFE0F Schema only (data will not be changed)"));
|
|
1757
|
-
}
|
|
1758
|
-
console.log(chalk6.yellow("\n \u26A0\uFE0F Target database will be completely replaced!"));
|
|
1759
|
-
console.log(chalk6.dim(" \u2139\uFE0F Target will be backed up before sync\n"));
|
|
1760
|
-
if (options.dryRun) {
|
|
1761
|
-
console.log(chalk6.green("\u2705 Dry run complete (no changes made)"));
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
if (!options.yes) {
|
|
1765
|
-
const { confirm } = await prompts2({
|
|
1766
|
-
type: "confirm",
|
|
1767
|
-
name: "confirm",
|
|
1768
|
-
message: chalk6.yellow(`Sync ${source.name} \u2192 ${targetEnv.name}?`),
|
|
1769
|
-
initial: false
|
|
1770
|
-
});
|
|
1771
|
-
if (!confirm) {
|
|
1772
|
-
console.log(chalk6.gray("Cancelled"));
|
|
1773
|
-
process.exit(0);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
console.log(chalk6.blue("\n\u{1F4E6} Step 1/4: Creating target backup...\n"));
|
|
1777
|
-
const originalDatabaseUrl = process.env.DATABASE_URL;
|
|
1778
|
-
process.env.DATABASE_URL = targetEnv.url;
|
|
1779
|
-
try {
|
|
1780
|
-
await dbBackup({
|
|
1781
|
-
format: "custom",
|
|
1782
|
-
tag: `pre-sync-from-${source.name}`,
|
|
1783
|
-
env: targetEnv.name
|
|
1784
|
-
});
|
|
1785
|
-
} finally {
|
|
1786
|
-
process.env.DATABASE_URL = originalDatabaseUrl;
|
|
1787
|
-
}
|
|
1788
|
-
console.log(chalk6.blue("\n\u{1F4E4} Step 2/4: Dumping source database..."));
|
|
1789
|
-
const tempDir = path.join(process.cwd(), "backups");
|
|
1790
|
-
const timestamp = formatTimestamp();
|
|
1791
|
-
const tempDumpFile = path.join(tempDir, `_temp_sync_${timestamp}.dump`);
|
|
1792
|
-
const dumpSpinner = ora3("Creating source dump...").start();
|
|
1793
|
-
const dumpArgs = [
|
|
1794
|
-
"-h",
|
|
1795
|
-
source.connection.host,
|
|
1796
|
-
"-p",
|
|
1797
|
-
source.connection.port,
|
|
1798
|
-
"-U",
|
|
1799
|
-
source.connection.user,
|
|
1800
|
-
"-d",
|
|
1801
|
-
source.connection.database,
|
|
1802
|
-
"-f",
|
|
1803
|
-
tempDumpFile,
|
|
1804
|
-
"-Fc"
|
|
1805
|
-
// Custom format
|
|
1806
|
-
];
|
|
1807
|
-
if (options.tables) {
|
|
1808
|
-
const tables = options.tables.split(",").map((t) => t.trim());
|
|
1809
|
-
tables.forEach((table) => {
|
|
1810
|
-
dumpArgs.push("-t", table);
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
if (options.excludeTables) {
|
|
1814
|
-
const tables = options.excludeTables.split(",").map((t) => t.trim());
|
|
1815
|
-
tables.forEach((table) => {
|
|
1816
|
-
dumpArgs.push("-T", table);
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
if (options.dataOnly) {
|
|
1820
|
-
dumpArgs.push("--data-only");
|
|
1821
|
-
}
|
|
1822
|
-
if (options.schemaOnly) {
|
|
1823
|
-
dumpArgs.push("--schema-only");
|
|
1824
|
-
}
|
|
1825
|
-
const dumpProcess = spawn("pg_dump", dumpArgs, {
|
|
1826
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1827
|
-
env: {
|
|
1828
|
-
...process.env,
|
|
1829
|
-
PGPASSWORD: source.connection.password
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
let dumpError = "";
|
|
1833
|
-
dumpProcess.stderr?.on("data", (data) => {
|
|
1834
|
-
dumpError += data.toString();
|
|
1835
|
-
});
|
|
1836
|
-
await new Promise((resolve, reject) => {
|
|
1837
|
-
dumpProcess.on("close", (code) => {
|
|
1838
|
-
if (code === 0) {
|
|
1839
|
-
dumpSpinner.succeed("Source dump created");
|
|
1840
|
-
resolve();
|
|
1841
|
-
} else {
|
|
1842
|
-
dumpSpinner.fail("Source dump failed");
|
|
1843
|
-
reject(new Error(dumpError || "pg_dump failed"));
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
dumpProcess.on("error", (error) => {
|
|
1847
|
-
dumpSpinner.fail("Source dump failed");
|
|
1848
|
-
reject(error);
|
|
1849
|
-
});
|
|
1850
|
-
}).catch((error) => {
|
|
1851
|
-
console.error(chalk6.red(`
|
|
1852
|
-
\u274C ${error instanceof Error ? error.message : "Failed to dump source"}`));
|
|
1853
|
-
process.exit(1);
|
|
1854
|
-
});
|
|
1855
|
-
console.log(chalk6.blue("\n\u{1F4E5} Step 3/4: Restoring to target database..."));
|
|
1856
|
-
const restoreSpinner = ora3("Restoring to target...").start();
|
|
1857
|
-
const restoreArgs = [
|
|
1858
|
-
"-h",
|
|
1859
|
-
targetEnv.connection.host,
|
|
1860
|
-
"-p",
|
|
1861
|
-
targetEnv.connection.port,
|
|
1862
|
-
"-U",
|
|
1863
|
-
targetEnv.connection.user,
|
|
1864
|
-
"-d",
|
|
1865
|
-
targetEnv.connection.database,
|
|
1866
|
-
"--clean",
|
|
1867
|
-
// Drop existing objects
|
|
1868
|
-
"--if-exists"
|
|
1869
|
-
// Don't error if objects don't exist
|
|
1870
|
-
];
|
|
1871
|
-
if (options.dataOnly) {
|
|
1872
|
-
restoreArgs.push("--data-only");
|
|
1873
|
-
}
|
|
1874
|
-
if (options.schemaOnly) {
|
|
1875
|
-
restoreArgs.push("--schema-only");
|
|
1876
|
-
}
|
|
1877
|
-
restoreArgs.push(tempDumpFile);
|
|
1878
|
-
const restoreProcess = spawn("pg_restore", restoreArgs, {
|
|
1879
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1880
|
-
env: {
|
|
1881
|
-
...process.env,
|
|
1882
|
-
PGPASSWORD: targetEnv.connection.password
|
|
1883
|
-
}
|
|
1884
|
-
});
|
|
1885
|
-
let restoreError = "";
|
|
1886
|
-
restoreProcess.stderr?.on("data", (data) => {
|
|
1887
|
-
restoreError += data.toString();
|
|
1888
|
-
});
|
|
1889
|
-
await new Promise((resolve, reject) => {
|
|
1890
|
-
restoreProcess.on("close", (code) => {
|
|
1891
|
-
if (code === 0) {
|
|
1892
|
-
restoreSpinner.succeed("Target restored");
|
|
1893
|
-
resolve();
|
|
1894
|
-
} else {
|
|
1895
|
-
restoreSpinner.fail("Target restore failed");
|
|
1896
|
-
reject(new Error(restoreError || "pg_restore failed"));
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
restoreProcess.on("error", (error) => {
|
|
1900
|
-
restoreSpinner.fail("Target restore failed");
|
|
1901
|
-
reject(error);
|
|
1902
|
-
});
|
|
1903
|
-
}).catch((error) => {
|
|
1904
|
-
console.error(chalk6.red(`
|
|
1905
|
-
\u274C ${error instanceof Error ? error.message : "Failed to restore target"}`));
|
|
1906
|
-
console.log(chalk6.yellow("\n\u{1F4A1} You can restore the target from the backup created in Step 1"));
|
|
2969
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1907
2970
|
process.exit(1);
|
|
1908
|
-
});
|
|
1909
|
-
console.log(chalk6.blue("\n\u{1F9F9} Step 4/4: Cleaning up..."));
|
|
1910
|
-
try {
|
|
1911
|
-
await fs.unlink(tempDumpFile);
|
|
1912
|
-
console.log(chalk6.dim("\u2713 Temporary files deleted"));
|
|
1913
|
-
} catch (error) {
|
|
1914
|
-
console.log(chalk6.dim("\u26A0\uFE0F Could not delete temporary dump file"));
|
|
1915
2971
|
}
|
|
1916
|
-
console.log(chalk6.green(`
|
|
1917
|
-
\u2705 Sync completed successfully!`));
|
|
1918
|
-
console.log(chalk6.dim(` ${source.name} \u2192 ${targetEnv.name}`));
|
|
1919
|
-
console.log("");
|
|
1920
2972
|
}
|
|
1921
|
-
|
|
2973
|
+
|
|
2974
|
+
// src/commands/db/index.ts
|
|
2975
|
+
var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
|
|
1922
2976
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
1923
2977
|
dbCommand.command("push").description("Push schema changes directly to database (no migrations)").action(dbPush);
|
|
1924
2978
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
@@ -1926,36 +2980,35 @@ dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").op
|
|
|
1926
2980
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
1927
2981
|
dbCommand.command("check").description("Check database connection").action(dbCheck);
|
|
1928
2982
|
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));
|
|
1929
|
-
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));
|
|
2983
|
+
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));
|
|
1930
2984
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
1931
2985
|
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));
|
|
1932
|
-
dbCommand.command("sync <target>").description("Sync database between environments").option("--pull", "Pull from target to local (reverse direction)").option("--tables <tables>", "Sync only specific tables (comma-separated)").option("--exclude-tables <tables>", "Exclude specific tables (comma-separated)").option("--data-only", "Sync data only (schema unchanged)").option("--schema-only", "Sync schema only (data unchanged)").option("--force", "Allow syncing to production-like environments").option("--dry-run", "Show sync plan without making changes").option("-y, --yes", "Skip confirmation prompt").action((target, options) => dbSync(target, options));
|
|
1933
2986
|
|
|
1934
2987
|
// src/commands/add.ts
|
|
1935
|
-
import { Command as
|
|
1936
|
-
import { existsSync as
|
|
1937
|
-
import { join as
|
|
2988
|
+
import { Command as Command10 } from "commander";
|
|
2989
|
+
import { existsSync as existsSync19, readFileSync as readFileSync6 } from "fs";
|
|
2990
|
+
import { join as join16 } from "path";
|
|
1938
2991
|
import { exec as exec2 } from "child_process";
|
|
1939
2992
|
import { promisify as promisify2 } from "util";
|
|
1940
|
-
import
|
|
1941
|
-
import
|
|
2993
|
+
import chalk23 from "chalk";
|
|
2994
|
+
import ora11 from "ora";
|
|
1942
2995
|
var execAsync2 = promisify2(exec2);
|
|
1943
2996
|
async function addPackage(packageName) {
|
|
1944
2997
|
if (!packageName.includes("/")) {
|
|
1945
|
-
console.error(
|
|
1946
|
-
console.log(
|
|
1947
|
-
console.log(
|
|
1948
|
-
console.log(
|
|
2998
|
+
console.error(chalk23.red("\u274C Please specify full package name"));
|
|
2999
|
+
console.log(chalk23.yellow("\n\u{1F4A1} Examples:"));
|
|
3000
|
+
console.log(chalk23.gray(" pnpm spfn add @spfn/cms"));
|
|
3001
|
+
console.log(chalk23.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
1949
3002
|
process.exit(1);
|
|
1950
3003
|
}
|
|
1951
|
-
console.log(
|
|
3004
|
+
console.log(chalk23.blue(`
|
|
1952
3005
|
\u{1F4E6} Setting up ${packageName}...
|
|
1953
3006
|
`));
|
|
1954
3007
|
try {
|
|
1955
|
-
const pkgPath =
|
|
1956
|
-
const pkgJsonPath =
|
|
1957
|
-
if (!
|
|
1958
|
-
const installSpinner =
|
|
3008
|
+
const pkgPath = join16(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3009
|
+
const pkgJsonPath = join16(pkgPath, "package.json");
|
|
3010
|
+
if (!existsSync19(pkgJsonPath)) {
|
|
3011
|
+
const installSpinner = ora11("Installing package...").start();
|
|
1959
3012
|
try {
|
|
1960
3013
|
await execAsync2(`pnpm add ${packageName}`);
|
|
1961
3014
|
installSpinner.succeed("Package installed");
|
|
@@ -1964,73 +3017,74 @@ async function addPackage(packageName) {
|
|
|
1964
3017
|
throw error;
|
|
1965
3018
|
}
|
|
1966
3019
|
} else {
|
|
1967
|
-
console.log(
|
|
3020
|
+
console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
|
|
1968
3021
|
}
|
|
1969
|
-
if (!
|
|
3022
|
+
if (!existsSync19(pkgJsonPath)) {
|
|
1970
3023
|
throw new Error(`Package ${packageName} not found after installation`);
|
|
1971
3024
|
}
|
|
1972
|
-
const pkgJson = JSON.parse(
|
|
3025
|
+
const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
|
|
1973
3026
|
if (pkgJson.spfn?.migrations) {
|
|
1974
|
-
console.log(
|
|
3027
|
+
console.log(chalk23.blue(`
|
|
1975
3028
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
1976
3029
|
`));
|
|
1977
|
-
const {
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
console.log(
|
|
1981
|
-
console.log(
|
|
1982
|
-
console.log(chalk7.gray(` pnpm spfn db push
|
|
3030
|
+
const { env: env6 } = await import("@spfn/core/config");
|
|
3031
|
+
if (!env6.DATABASE_URL) {
|
|
3032
|
+
console.log(chalk23.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
3033
|
+
console.log(chalk23.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
3034
|
+
console.log(chalk23.gray(` pnpm spfn db push
|
|
1983
3035
|
`));
|
|
1984
3036
|
} else {
|
|
1985
|
-
const { discoverFunctionMigrations, executeFunctionMigrations } = await
|
|
1986
|
-
const functions =
|
|
3037
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
3038
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
1987
3039
|
const targetFunction = functions.find((f) => f.packageName === packageName);
|
|
1988
3040
|
if (targetFunction) {
|
|
1989
|
-
const spinner =
|
|
3041
|
+
const spinner = ora11("Applying migrations...").start();
|
|
1990
3042
|
try {
|
|
1991
|
-
await
|
|
3043
|
+
await executeFunctionMigrations2([targetFunction]);
|
|
1992
3044
|
spinner.succeed("Migrations applied");
|
|
1993
3045
|
} catch (error) {
|
|
1994
3046
|
spinner.fail("Failed to apply migrations");
|
|
1995
3047
|
throw error;
|
|
1996
3048
|
}
|
|
1997
3049
|
} else {
|
|
1998
|
-
console.log(
|
|
3050
|
+
console.log(chalk23.gray("\u2139\uFE0F No migrations found for this package"));
|
|
1999
3051
|
}
|
|
2000
3052
|
}
|
|
2001
3053
|
} else {
|
|
2002
|
-
console.log(
|
|
3054
|
+
console.log(chalk23.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
2003
3055
|
}
|
|
2004
|
-
console.log(
|
|
3056
|
+
console.log(chalk23.green(`
|
|
2005
3057
|
\u2705 ${packageName} installed successfully!
|
|
2006
3058
|
`));
|
|
2007
3059
|
if (pkgJson.spfn?.setupMessage) {
|
|
2008
|
-
console.log(
|
|
3060
|
+
console.log(chalk23.cyan("\u{1F4DA} Setup Guide:"));
|
|
2009
3061
|
console.log(pkgJson.spfn.setupMessage);
|
|
2010
3062
|
console.log();
|
|
2011
3063
|
}
|
|
2012
3064
|
} catch (error) {
|
|
2013
|
-
console.error(
|
|
3065
|
+
console.error(chalk23.red(`
|
|
2014
3066
|
\u274C Failed to install ${packageName}
|
|
2015
3067
|
`));
|
|
2016
|
-
console.error(
|
|
3068
|
+
console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2017
3069
|
process.exit(1);
|
|
2018
3070
|
}
|
|
2019
3071
|
}
|
|
2020
|
-
var addCommand = new
|
|
3072
|
+
var addCommand = new Command10("add").description("Install and set up SPFN ecosystem packages").argument("<package>", "Package name (e.g., @spfn/cms, @mycompany/spfn-analytics)").action(addPackage);
|
|
2021
3073
|
|
|
2022
3074
|
// src/commands/generate.ts
|
|
2023
|
-
|
|
2024
|
-
import
|
|
2025
|
-
import
|
|
2026
|
-
import {
|
|
2027
|
-
import
|
|
3075
|
+
init_logger();
|
|
3076
|
+
import { Command as Command11 } from "commander";
|
|
3077
|
+
import ora12 from "ora";
|
|
3078
|
+
import { join as join25 } from "path";
|
|
3079
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3080
|
+
import chalk25 from "chalk";
|
|
2028
3081
|
|
|
2029
3082
|
// src/commands/generate/prompts.ts
|
|
2030
|
-
|
|
2031
|
-
import
|
|
3083
|
+
init_logger();
|
|
3084
|
+
import prompts6 from "prompts";
|
|
3085
|
+
import chalk24 from "chalk";
|
|
2032
3086
|
async function promptScope() {
|
|
2033
|
-
const response = await
|
|
3087
|
+
const response = await prompts6({
|
|
2034
3088
|
type: "text",
|
|
2035
3089
|
name: "scope",
|
|
2036
3090
|
message: "NPM scope (e.g., @mycompany, @username):",
|
|
@@ -2052,7 +3106,7 @@ async function promptScope() {
|
|
|
2052
3106
|
return response.scope;
|
|
2053
3107
|
}
|
|
2054
3108
|
async function promptFunctionName() {
|
|
2055
|
-
const response = await
|
|
3109
|
+
const response = await prompts6({
|
|
2056
3110
|
type: "text",
|
|
2057
3111
|
name: "fnName",
|
|
2058
3112
|
message: "Function name:",
|
|
@@ -2073,7 +3127,7 @@ async function promptFunctionName() {
|
|
|
2073
3127
|
return response.fnName;
|
|
2074
3128
|
}
|
|
2075
3129
|
async function promptDescription(fnName) {
|
|
2076
|
-
const response = await
|
|
3130
|
+
const response = await prompts6({
|
|
2077
3131
|
type: "text",
|
|
2078
3132
|
name: "description",
|
|
2079
3133
|
message: "Function description:",
|
|
@@ -2082,7 +3136,7 @@ async function promptDescription(fnName) {
|
|
|
2082
3136
|
return response.description || "A description of what this module does";
|
|
2083
3137
|
}
|
|
2084
3138
|
async function promptEntities() {
|
|
2085
|
-
const response = await
|
|
3139
|
+
const response = await prompts6({
|
|
2086
3140
|
type: "list",
|
|
2087
3141
|
name: "entities",
|
|
2088
3142
|
message: "Entity names (comma-separated, press enter to skip):",
|
|
@@ -2094,14 +3148,14 @@ async function promptEntities() {
|
|
|
2094
3148
|
async function confirmConfiguration(config) {
|
|
2095
3149
|
const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
|
|
2096
3150
|
console.log("");
|
|
2097
|
-
logger.info(
|
|
2098
|
-
console.log(` ${
|
|
2099
|
-
console.log(` ${
|
|
2100
|
-
console.log(` ${
|
|
2101
|
-
console.log(` ${
|
|
2102
|
-
console.log(` ${
|
|
3151
|
+
logger.info(chalk24.bold("\u26A1 Function Configuration:"));
|
|
3152
|
+
console.log(` ${chalk24.gray("Package:")} ${chalk24.cyan(`${scope}/${fnName}`)}`);
|
|
3153
|
+
console.log(` ${chalk24.gray("Description:")} ${description}`);
|
|
3154
|
+
console.log(` ${chalk24.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk24.gray("none")}`);
|
|
3155
|
+
console.log(` ${chalk24.gray("Cache:")} ${enableCache ? chalk24.green("yes") : chalk24.gray("no")}`);
|
|
3156
|
+
console.log(` ${chalk24.gray("Routes:")} ${enableRoutes ? chalk24.green("yes") : chalk24.gray("no")}`);
|
|
2103
3157
|
console.log("");
|
|
2104
|
-
const { confirmed } = await
|
|
3158
|
+
const { confirmed } = await prompts6({
|
|
2105
3159
|
type: "confirm",
|
|
2106
3160
|
name: "confirmed",
|
|
2107
3161
|
message: "Create function?",
|
|
@@ -2115,12 +3169,12 @@ async function confirmConfiguration(config) {
|
|
|
2115
3169
|
}
|
|
2116
3170
|
|
|
2117
3171
|
// src/commands/generate/generators/structure.ts
|
|
2118
|
-
import { join as
|
|
2119
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
3172
|
+
import { join as join24 } from "path";
|
|
3173
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
2120
3174
|
|
|
2121
3175
|
// src/commands/generate/generators/config.ts
|
|
2122
|
-
import { join as
|
|
2123
|
-
import { writeFileSync as
|
|
3176
|
+
import { join as join18 } from "path";
|
|
3177
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
|
|
2124
3178
|
|
|
2125
3179
|
// src/commands/generate/string-utils.ts
|
|
2126
3180
|
function toPascalCase(str) {
|
|
@@ -2149,30 +3203,30 @@ function toSafeSchemaName(str) {
|
|
|
2149
3203
|
}
|
|
2150
3204
|
|
|
2151
3205
|
// src/commands/generate/template-loader.ts
|
|
2152
|
-
import { readFileSync as
|
|
2153
|
-
import { join as
|
|
2154
|
-
import { fileURLToPath } from "url";
|
|
2155
|
-
function
|
|
2156
|
-
const __filename =
|
|
2157
|
-
const
|
|
2158
|
-
const distPath =
|
|
2159
|
-
if (
|
|
3206
|
+
import { readFileSync as readFileSync7, existsSync as existsSync20 } from "fs";
|
|
3207
|
+
import { join as join17, dirname as dirname2 } from "path";
|
|
3208
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3209
|
+
function findTemplatesPath2() {
|
|
3210
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
3211
|
+
const __dirname2 = dirname2(__filename);
|
|
3212
|
+
const distPath = join17(__dirname2, "commands", "generate", "templates");
|
|
3213
|
+
if (existsSync20(distPath)) {
|
|
2160
3214
|
return distPath;
|
|
2161
3215
|
}
|
|
2162
|
-
const sameDirPath =
|
|
2163
|
-
if (
|
|
3216
|
+
const sameDirPath = join17(__dirname2, "templates");
|
|
3217
|
+
if (existsSync20(sameDirPath)) {
|
|
2164
3218
|
return sameDirPath;
|
|
2165
3219
|
}
|
|
2166
|
-
const srcPath =
|
|
2167
|
-
if (
|
|
3220
|
+
const srcPath = join17(__dirname2, "..", "..", "src", "commands", "generate", "templates");
|
|
3221
|
+
if (existsSync20(srcPath)) {
|
|
2168
3222
|
return srcPath;
|
|
2169
3223
|
}
|
|
2170
3224
|
throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
|
|
2171
3225
|
}
|
|
2172
3226
|
function loadTemplate(templateName, variables) {
|
|
2173
|
-
const templatesDir =
|
|
2174
|
-
const templatePath =
|
|
2175
|
-
let content =
|
|
3227
|
+
const templatesDir = findTemplatesPath2();
|
|
3228
|
+
const templatePath = join17(templatesDir, `${templateName}.template`);
|
|
3229
|
+
let content = readFileSync7(templatePath, "utf-8");
|
|
2176
3230
|
for (const [key, value] of Object.entries(variables)) {
|
|
2177
3231
|
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
2178
3232
|
content = content.replace(regex, value);
|
|
@@ -2274,8 +3328,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
|
|
|
2274
3328
|
vitest: "^4.0.6"
|
|
2275
3329
|
}
|
|
2276
3330
|
};
|
|
2277
|
-
|
|
2278
|
-
|
|
3331
|
+
writeFileSync11(
|
|
3332
|
+
join18(fnDir, "package.json"),
|
|
2279
3333
|
JSON.stringify(content, null, 4) + "\n"
|
|
2280
3334
|
);
|
|
2281
3335
|
}
|
|
@@ -2308,8 +3362,8 @@ function generateTsConfig(fnDir) {
|
|
|
2308
3362
|
include: ["src/**/*"],
|
|
2309
3363
|
exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
|
|
2310
3364
|
};
|
|
2311
|
-
|
|
2312
|
-
|
|
3365
|
+
writeFileSync11(
|
|
3366
|
+
join18(fnDir, "tsconfig.json"),
|
|
2313
3367
|
JSON.stringify(content, null, 4) + "\n"
|
|
2314
3368
|
);
|
|
2315
3369
|
}
|
|
@@ -2385,7 +3439,7 @@ export default defineConfig({
|
|
|
2385
3439
|
],
|
|
2386
3440
|
});
|
|
2387
3441
|
`;
|
|
2388
|
-
|
|
3442
|
+
writeFileSync11(join18(fnDir, "tsup.config.ts"), content);
|
|
2389
3443
|
}
|
|
2390
3444
|
function generateDrizzleConfig(fnDir, scope, fnName) {
|
|
2391
3445
|
const schemaName = `spfn_${toSnakeCase(fnName)}`;
|
|
@@ -2405,7 +3459,7 @@ export default defineConfig({
|
|
|
2405
3459
|
schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
|
|
2406
3460
|
});
|
|
2407
3461
|
`;
|
|
2408
|
-
|
|
3462
|
+
writeFileSync11(join18(fnDir, "drizzle.config.ts"), content);
|
|
2409
3463
|
}
|
|
2410
3464
|
function generateExampleGenerator(fnDir, scope, fnName) {
|
|
2411
3465
|
const pascalName = toPascalCase(fnName);
|
|
@@ -2474,10 +3528,10 @@ export const moduleName = '${fnName}';
|
|
|
2474
3528
|
};
|
|
2475
3529
|
}
|
|
2476
3530
|
`;
|
|
2477
|
-
const generatorsDir =
|
|
3531
|
+
const generatorsDir = join18(fnDir, "src/server/generators");
|
|
2478
3532
|
mkdirSync3(generatorsDir, { recursive: true });
|
|
2479
|
-
|
|
2480
|
-
|
|
3533
|
+
writeFileSync11(
|
|
3534
|
+
join18(generatorsDir, "example-generator.ts"),
|
|
2481
3535
|
content
|
|
2482
3536
|
);
|
|
2483
3537
|
const indexContent = `/**
|
|
@@ -2491,8 +3545,8 @@ export const moduleName = '${fnName}';
|
|
|
2491
3545
|
|
|
2492
3546
|
export { create${pascalName}ExampleGenerator } from './example-generator.js';
|
|
2493
3547
|
`;
|
|
2494
|
-
|
|
2495
|
-
|
|
3548
|
+
writeFileSync11(
|
|
3549
|
+
join18(generatorsDir, "index.ts"),
|
|
2496
3550
|
indexContent
|
|
2497
3551
|
);
|
|
2498
3552
|
}
|
|
@@ -2981,15 +4035,15 @@ Contributions are welcome! Please follow the development workflow above.
|
|
|
2981
4035
|
|
|
2982
4036
|
MIT
|
|
2983
4037
|
`;
|
|
2984
|
-
|
|
4038
|
+
writeFileSync11(join18(fnDir, "README.md"), content);
|
|
2985
4039
|
}
|
|
2986
4040
|
|
|
2987
4041
|
// src/commands/generate/generators/entity.ts
|
|
2988
|
-
import { join as
|
|
2989
|
-
import { writeFileSync as
|
|
4042
|
+
import { join as join19 } from "path";
|
|
4043
|
+
import { writeFileSync as writeFileSync12, existsSync as existsSync21 } from "fs";
|
|
2990
4044
|
function generateSchema(fnDir, scope, fnName) {
|
|
2991
|
-
const schemaFilePath =
|
|
2992
|
-
if (
|
|
4045
|
+
const schemaFilePath = join19(fnDir, "src/server/entities/schema.ts");
|
|
4046
|
+
if (existsSync21(schemaFilePath)) {
|
|
2993
4047
|
return;
|
|
2994
4048
|
}
|
|
2995
4049
|
const packageName = `${scope}/${fnName}`;
|
|
@@ -3000,7 +4054,7 @@ function generateSchema(fnDir, scope, fnName) {
|
|
|
3000
4054
|
PACKAGE_NAME: packageName,
|
|
3001
4055
|
SCHEMA_VAR_NAME: schemaVarName
|
|
3002
4056
|
});
|
|
3003
|
-
|
|
4057
|
+
writeFileSync12(schemaFilePath, content);
|
|
3004
4058
|
}
|
|
3005
4059
|
function generateEntity(fnDir, scope, fnName, entityName) {
|
|
3006
4060
|
const safeScope = toSafeSchemaName(scope);
|
|
@@ -3019,8 +4073,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
|
|
|
3019
4073
|
SCHEMA_VAR_NAME: schemaVarName,
|
|
3020
4074
|
SCHEMA_FILE_NAME: schemaFileName
|
|
3021
4075
|
});
|
|
3022
|
-
|
|
3023
|
-
|
|
4076
|
+
writeFileSync12(
|
|
4077
|
+
join19(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
|
|
3024
4078
|
content
|
|
3025
4079
|
);
|
|
3026
4080
|
}
|
|
@@ -3028,12 +4082,12 @@ function generateEntitiesIndex(fnDir, entities) {
|
|
|
3028
4082
|
const schemaExport = `export * from './schema';`;
|
|
3029
4083
|
const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
|
|
3030
4084
|
const content = [schemaExport, entityExports].filter(Boolean).join("\n");
|
|
3031
|
-
|
|
4085
|
+
writeFileSync12(join19(fnDir, "src/server/entities/index.ts"), content + "\n");
|
|
3032
4086
|
}
|
|
3033
4087
|
|
|
3034
4088
|
// src/commands/generate/generators/repository.ts
|
|
3035
|
-
import { join as
|
|
3036
|
-
import { writeFileSync as
|
|
4089
|
+
import { join as join20 } from "path";
|
|
4090
|
+
import { writeFileSync as writeFileSync13 } from "fs";
|
|
3037
4091
|
function generateRepository(fnDir, entityName) {
|
|
3038
4092
|
const pascalName = toPascalCase(entityName);
|
|
3039
4093
|
const repoName = `${entityName}Repository`;
|
|
@@ -3042,19 +4096,19 @@ function generateRepository(fnDir, entityName) {
|
|
|
3042
4096
|
ENTITY_NAME: entityName,
|
|
3043
4097
|
REPO_NAME: repoName
|
|
3044
4098
|
});
|
|
3045
|
-
|
|
3046
|
-
|
|
4099
|
+
writeFileSync13(
|
|
4100
|
+
join20(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
|
|
3047
4101
|
content
|
|
3048
4102
|
);
|
|
3049
4103
|
}
|
|
3050
4104
|
function generateRepositoriesIndex(fnDir, entities) {
|
|
3051
4105
|
const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
|
|
3052
|
-
|
|
4106
|
+
writeFileSync13(join20(fnDir, "src/server/repositories/index.ts"), exports + "\n");
|
|
3053
4107
|
}
|
|
3054
4108
|
|
|
3055
4109
|
// src/commands/generate/generators/route.ts
|
|
3056
|
-
import { join as
|
|
3057
|
-
import { mkdirSync as mkdirSync4, writeFileSync as
|
|
4110
|
+
import { join as join21 } from "path";
|
|
4111
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
|
|
3058
4112
|
function generateRoute(fnDir, entityName) {
|
|
3059
4113
|
const pascalName = toPascalCase(entityName);
|
|
3060
4114
|
const repoName = `${entityName}Repository`;
|
|
@@ -3065,29 +4119,29 @@ function generateRoute(fnDir, entityName) {
|
|
|
3065
4119
|
REPO_NAME: repoName,
|
|
3066
4120
|
KEBAB_NAME: kebabName
|
|
3067
4121
|
});
|
|
3068
|
-
const routeDir =
|
|
4122
|
+
const routeDir = join21(fnDir, `src/server/routes/${kebabName}`);
|
|
3069
4123
|
mkdirSync4(routeDir, { recursive: true });
|
|
3070
|
-
|
|
4124
|
+
writeFileSync14(join21(routeDir, "index.ts"), content);
|
|
3071
4125
|
}
|
|
3072
4126
|
|
|
3073
4127
|
// src/commands/generate/generators/contract.ts
|
|
3074
|
-
import { join as
|
|
3075
|
-
import { writeFileSync as
|
|
4128
|
+
import { join as join22 } from "path";
|
|
4129
|
+
import { writeFileSync as writeFileSync15 } from "fs";
|
|
3076
4130
|
function generateContract(fnDir, entityName) {
|
|
3077
4131
|
const pascalName = toPascalCase(entityName);
|
|
3078
4132
|
const content = loadTemplate("contract", {
|
|
3079
4133
|
PASCAL_NAME: pascalName,
|
|
3080
4134
|
ENTITY_NAME: entityName
|
|
3081
4135
|
});
|
|
3082
|
-
|
|
3083
|
-
|
|
4136
|
+
writeFileSync15(
|
|
4137
|
+
join22(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
|
|
3084
4138
|
content
|
|
3085
4139
|
);
|
|
3086
4140
|
}
|
|
3087
4141
|
|
|
3088
4142
|
// src/commands/generate/generators/index-files.ts
|
|
3089
|
-
import { join as
|
|
3090
|
-
import { writeFileSync as
|
|
4143
|
+
import { join as join23 } from "path";
|
|
4144
|
+
import { writeFileSync as writeFileSync16 } from "fs";
|
|
3091
4145
|
function generateMainIndex(fnDir, fnName) {
|
|
3092
4146
|
const content = `/**
|
|
3093
4147
|
* @spfn/${fnName}
|
|
@@ -3113,7 +4167,7 @@ export * from '@/lib/types/index';
|
|
|
3113
4167
|
|
|
3114
4168
|
export * from '@/server/entities/index';
|
|
3115
4169
|
`;
|
|
3116
|
-
|
|
4170
|
+
writeFileSync16(join23(fnDir, "src/index.ts"), content);
|
|
3117
4171
|
}
|
|
3118
4172
|
function generateServerIndex(fnDir) {
|
|
3119
4173
|
const content = `/**
|
|
@@ -3148,7 +4202,7 @@ export * from '@/server/repositories/index';
|
|
|
3148
4202
|
|
|
3149
4203
|
// TODO: Export helpers here
|
|
3150
4204
|
`;
|
|
3151
|
-
|
|
4205
|
+
writeFileSync16(join23(fnDir, "src/server.ts"), content);
|
|
3152
4206
|
}
|
|
3153
4207
|
function generateClientIndex(fnDir) {
|
|
3154
4208
|
const content = `/**
|
|
@@ -3181,7 +4235,7 @@ export * from './client/store';
|
|
|
3181
4235
|
|
|
3182
4236
|
export * from './client/components';
|
|
3183
4237
|
`;
|
|
3184
|
-
|
|
4238
|
+
writeFileSync16(join23(fnDir, "src/client.ts"), content);
|
|
3185
4239
|
}
|
|
3186
4240
|
function generateTypesFile(fnDir, fnName) {
|
|
3187
4241
|
const content = `/**
|
|
@@ -3193,7 +4247,7 @@ function generateTypesFile(fnDir, fnName) {
|
|
|
3193
4247
|
|
|
3194
4248
|
export * from '@/lib/types/index';
|
|
3195
4249
|
`;
|
|
3196
|
-
|
|
4250
|
+
writeFileSync16(join23(fnDir, "src/types.ts"), content);
|
|
3197
4251
|
}
|
|
3198
4252
|
|
|
3199
4253
|
// src/commands/generate/generators/structure.ts
|
|
@@ -3215,7 +4269,7 @@ async function generateFunctionStructure(options) {
|
|
|
3215
4269
|
"src/client/store",
|
|
3216
4270
|
"src/client/components"
|
|
3217
4271
|
];
|
|
3218
|
-
dirs.forEach((dir) => mkdirSync5(
|
|
4272
|
+
dirs.forEach((dir) => mkdirSync5(join24(fnDir, dir), { recursive: true }));
|
|
3219
4273
|
generatePackageJson(fnDir, scope, fnName, description);
|
|
3220
4274
|
generateTsConfig(fnDir);
|
|
3221
4275
|
generateTsupConfig(fnDir);
|
|
@@ -3235,15 +4289,15 @@ async function generateFunctionStructure(options) {
|
|
|
3235
4289
|
generateEntitiesIndex(fnDir, entities);
|
|
3236
4290
|
generateRepositoriesIndex(fnDir, entities);
|
|
3237
4291
|
} else {
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
4292
|
+
writeFileSync17(join24(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
|
|
4293
|
+
writeFileSync17(join24(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
|
|
4294
|
+
}
|
|
4295
|
+
writeFileSync17(join24(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
|
|
4296
|
+
writeFileSync17(join24(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
|
|
4297
|
+
writeFileSync17(join24(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
|
|
4298
|
+
writeFileSync17(join24(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
|
|
4299
|
+
writeFileSync17(join24(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
|
|
4300
|
+
writeFileSync17(join24(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
|
|
3247
4301
|
generateMainIndex(fnDir, fnName);
|
|
3248
4302
|
generateServerIndex(fnDir);
|
|
3249
4303
|
generateClientIndex(fnDir);
|
|
@@ -3267,8 +4321,8 @@ async function generateFunction(name, options) {
|
|
|
3267
4321
|
logger.error("Function name is required");
|
|
3268
4322
|
process.exit(1);
|
|
3269
4323
|
}
|
|
3270
|
-
const fnDir =
|
|
3271
|
-
if (
|
|
4324
|
+
const fnDir = join25(cwd, fnName);
|
|
4325
|
+
if (existsSync22(fnDir)) {
|
|
3272
4326
|
logger.error(`Directory ${fnName} already exists at ${fnDir}`);
|
|
3273
4327
|
process.exit(1);
|
|
3274
4328
|
}
|
|
@@ -3300,7 +4354,7 @@ async function generateFunction(name, options) {
|
|
|
3300
4354
|
process.exit(0);
|
|
3301
4355
|
}
|
|
3302
4356
|
}
|
|
3303
|
-
const spinner =
|
|
4357
|
+
const spinner = ora12("Generating function structure...").start();
|
|
3304
4358
|
try {
|
|
3305
4359
|
await generateFunctionStructure({
|
|
3306
4360
|
fnDir,
|
|
@@ -3313,13 +4367,13 @@ async function generateFunction(name, options) {
|
|
|
3313
4367
|
});
|
|
3314
4368
|
spinner.succeed("Function structure generated");
|
|
3315
4369
|
console.log("");
|
|
3316
|
-
logger.success(`\u2728 Package ${
|
|
4370
|
+
logger.success(`\u2728 Package ${chalk25.cyan(`${scope}/${fnName}`)} created successfully!
|
|
3317
4371
|
`);
|
|
3318
|
-
logger.info(
|
|
3319
|
-
console.log(` ${
|
|
3320
|
-
console.log(` ${
|
|
3321
|
-
console.log(` ${
|
|
3322
|
-
console.log(` ${
|
|
4372
|
+
logger.info(chalk25.bold("\u{1F4DA} Next steps:"));
|
|
4373
|
+
console.log(` ${chalk25.gray("1.")} cd ${fnName}`);
|
|
4374
|
+
console.log(` ${chalk25.gray("2.")} pnpm install ${chalk25.dim("(in monorepo root)")}`);
|
|
4375
|
+
console.log(` ${chalk25.gray("3.")} pnpm build`);
|
|
4376
|
+
console.log(` ${chalk25.gray("4.")} ${chalk25.dim("Use the package in your app")}`);
|
|
3323
4377
|
console.log("");
|
|
3324
4378
|
} catch (error) {
|
|
3325
4379
|
spinner.fail("Failed to generate function");
|
|
@@ -3327,12 +4381,434 @@ async function generateFunction(name, options) {
|
|
|
3327
4381
|
process.exit(1);
|
|
3328
4382
|
}
|
|
3329
4383
|
}
|
|
3330
|
-
var generateCommand = new
|
|
4384
|
+
var generateCommand = new Command11("generate").alias("g").description("Generate SPFN resources");
|
|
3331
4385
|
generateCommand.command("fn").description("Generate a new SPFN function module").argument("[name]", "Function name").option("-s, --scope <scope>", "NPM scope (e.g., @spfn, @mycompany)").option("-d, --description <text>", "Function description").option("-e, --entities <list>", "Comma-separated entity names").option("--skip-cache", "Skip cache generation").option("--skip-routes", "Skip route generation").option("-y, --yes", "Skip all prompts").action(generateFunction);
|
|
3332
4386
|
|
|
4387
|
+
// src/commands/env.ts
|
|
4388
|
+
import { Command as Command12 } from "commander";
|
|
4389
|
+
import chalk26 from "chalk";
|
|
4390
|
+
import { existsSync as existsSync23, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
4391
|
+
import { resolve } from "path";
|
|
4392
|
+
import { parse } from "dotenv";
|
|
4393
|
+
var ENV_FILES = {
|
|
4394
|
+
nextjs: [".env", ".env.local"],
|
|
4395
|
+
server: [".env.server", ".env.server.local"]
|
|
4396
|
+
};
|
|
4397
|
+
function getTargetFile(schema) {
|
|
4398
|
+
const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
|
|
4399
|
+
if (isNextjs) {
|
|
4400
|
+
return schema.sensitive ? ".env.local" : ".env";
|
|
4401
|
+
}
|
|
4402
|
+
return schema.sensitive ? ".env.server.local" : ".env.server";
|
|
4403
|
+
}
|
|
4404
|
+
async function loadEnvSchema(packageName) {
|
|
4405
|
+
try {
|
|
4406
|
+
const schemaPath = `${packageName}/config`;
|
|
4407
|
+
const module = await import(schemaPath);
|
|
4408
|
+
if (!module.envSchema) {
|
|
4409
|
+
throw new Error(`Package ${packageName} does not export envSchema from config`);
|
|
4410
|
+
}
|
|
4411
|
+
return module.envSchema;
|
|
4412
|
+
} catch (error) {
|
|
4413
|
+
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4414
|
+
throw error;
|
|
4415
|
+
}
|
|
4416
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4417
|
+
throw new Error(`Failed to load package ${packageName}: ${errorMessage}`);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
function formatType(type) {
|
|
4421
|
+
const typeColors = {
|
|
4422
|
+
string: chalk26.green,
|
|
4423
|
+
number: chalk26.blue,
|
|
4424
|
+
boolean: chalk26.yellow,
|
|
4425
|
+
url: chalk26.cyan,
|
|
4426
|
+
enum: chalk26.magenta,
|
|
4427
|
+
json: chalk26.red
|
|
4428
|
+
};
|
|
4429
|
+
const colorFn = typeColors[type] || chalk26.white;
|
|
4430
|
+
return colorFn(type);
|
|
4431
|
+
}
|
|
4432
|
+
function formatDefault(value, type) {
|
|
4433
|
+
if (value === void 0) {
|
|
4434
|
+
return chalk26.dim("(none)");
|
|
4435
|
+
}
|
|
4436
|
+
if (type === "string" || type === "url") {
|
|
4437
|
+
return chalk26.green(`"${value}"`);
|
|
4438
|
+
}
|
|
4439
|
+
if (type === "boolean") {
|
|
4440
|
+
return value ? chalk26.green("true") : chalk26.red("false");
|
|
4441
|
+
}
|
|
4442
|
+
return chalk26.cyan(String(value));
|
|
4443
|
+
}
|
|
4444
|
+
async function listEnvVars(options) {
|
|
4445
|
+
const packageName = options.package || "@spfn/core";
|
|
4446
|
+
try {
|
|
4447
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4448
|
+
const allVars = Object.entries(envSchema);
|
|
4449
|
+
if (options.group) {
|
|
4450
|
+
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
4451
|
+
const target = getTargetFile(schema);
|
|
4452
|
+
if (!acc[target]) acc[target] = [];
|
|
4453
|
+
acc[target].push([key, schema]);
|
|
4454
|
+
return acc;
|
|
4455
|
+
}, {});
|
|
4456
|
+
console.log(chalk26.blue.bold(`
|
|
4457
|
+
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4458
|
+
`));
|
|
4459
|
+
for (const [file, vars] of Object.entries(grouped)) {
|
|
4460
|
+
console.log(chalk26.bold.magenta(`
|
|
4461
|
+
${file}`));
|
|
4462
|
+
console.log(chalk26.dim("\u2500".repeat(50)));
|
|
4463
|
+
for (const [key, schema] of vars) {
|
|
4464
|
+
printEnvVar(key, schema);
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
} else {
|
|
4468
|
+
console.log(chalk26.blue.bold(`
|
|
4469
|
+
\u{1F4CB} Environment Variables (${packageName})
|
|
4470
|
+
`));
|
|
4471
|
+
for (const [key, schema] of allVars) {
|
|
4472
|
+
printEnvVar(key, schema, true);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
console.log(chalk26.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4476
|
+
} catch (error) {
|
|
4477
|
+
console.error(chalk26.red(`
|
|
4478
|
+
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4479
|
+
`));
|
|
4480
|
+
process.exit(1);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
function printEnvVar(key, schema, showFile = false) {
|
|
4484
|
+
const typeStr = formatType(schema.type);
|
|
4485
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
|
|
4486
|
+
const sensitiveStr = schema.sensitive ? chalk26.yellow(" [sensitive]") : "";
|
|
4487
|
+
const fileStr = showFile ? chalk26.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
4488
|
+
console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
4489
|
+
console.log(` ${chalk26.dim(schema.description)}`);
|
|
4490
|
+
if (schema.default !== void 0) {
|
|
4491
|
+
console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4492
|
+
}
|
|
4493
|
+
if (schema.examples && schema.examples.length > 0) {
|
|
4494
|
+
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4495
|
+
console.log(` ${chalk26.dim("Examples:")} ${exampleStr}`);
|
|
4496
|
+
}
|
|
4497
|
+
console.log();
|
|
4498
|
+
}
|
|
4499
|
+
async function showEnvStats(options) {
|
|
4500
|
+
const packageName = options.package || "@spfn/core";
|
|
4501
|
+
try {
|
|
4502
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4503
|
+
console.log(chalk26.blue.bold(`
|
|
4504
|
+
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4505
|
+
`));
|
|
4506
|
+
const allVars = Object.entries(envSchema);
|
|
4507
|
+
const required = allVars.filter(([_, schema]) => schema.required || schema.default !== void 0);
|
|
4508
|
+
const optional = allVars.filter(([_, schema]) => !schema.required && schema.default === void 0);
|
|
4509
|
+
const sensitive = allVars.filter(([_, schema]) => schema.sensitive);
|
|
4510
|
+
const nextjsVars = allVars.filter(
|
|
4511
|
+
([_, schema]) => schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_")
|
|
4512
|
+
);
|
|
4513
|
+
const serverOnlyVars = allVars.filter(
|
|
4514
|
+
([_, schema]) => !(schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_"))
|
|
4515
|
+
);
|
|
4516
|
+
const typeCount = allVars.reduce((acc, [_, schema]) => {
|
|
4517
|
+
acc[schema.type] = (acc[schema.type] || 0) + 1;
|
|
4518
|
+
return acc;
|
|
4519
|
+
}, {});
|
|
4520
|
+
const fileCount = allVars.reduce((acc, [_, schema]) => {
|
|
4521
|
+
const file = getTargetFile(schema);
|
|
4522
|
+
acc[file] = (acc[file] || 0) + 1;
|
|
4523
|
+
return acc;
|
|
4524
|
+
}, {});
|
|
4525
|
+
console.log(`${chalk26.bold("Total variables:")} ${chalk26.cyan(allVars.length)}`);
|
|
4526
|
+
console.log(`${chalk26.bold("Required:")} ${chalk26.red(required.length)}`);
|
|
4527
|
+
console.log(`${chalk26.bold("Optional:")} ${chalk26.dim(optional.length)}`);
|
|
4528
|
+
console.log(`${chalk26.bold("Sensitive:")} ${chalk26.yellow(sensitive.length)}`);
|
|
4529
|
+
console.log(chalk26.bold("\nBy Target:"));
|
|
4530
|
+
console.log(` ${chalk26.blue("Next.js accessible:")} ${chalk26.cyan(nextjsVars.length)}`);
|
|
4531
|
+
console.log(` ${chalk26.magenta("SPFN server only:")} ${chalk26.cyan(serverOnlyVars.length)}`);
|
|
4532
|
+
console.log(chalk26.bold("\nBy File:"));
|
|
4533
|
+
for (const [file, count] of Object.entries(fileCount)) {
|
|
4534
|
+
console.log(` ${chalk26.dim(file)}: ${chalk26.cyan(count)}`);
|
|
4535
|
+
}
|
|
4536
|
+
console.log(chalk26.bold("\nBy Type:"));
|
|
4537
|
+
for (const [type, count] of Object.entries(typeCount)) {
|
|
4538
|
+
console.log(` ${formatType(type)}: ${chalk26.cyan(count)}`);
|
|
4539
|
+
}
|
|
4540
|
+
console.log();
|
|
4541
|
+
} catch (error) {
|
|
4542
|
+
console.error(chalk26.red(`
|
|
4543
|
+
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4544
|
+
`));
|
|
4545
|
+
process.exit(1);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
async function searchEnvVars(query, options) {
|
|
4549
|
+
const packageName = options.package || "@spfn/core";
|
|
4550
|
+
try {
|
|
4551
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4552
|
+
const normalizedQuery = query.toLowerCase();
|
|
4553
|
+
const results = [];
|
|
4554
|
+
for (const [key, schema] of Object.entries(envSchema)) {
|
|
4555
|
+
const matchesKey = key.toLowerCase().includes(normalizedQuery);
|
|
4556
|
+
const matchesDescription = schema.description.toLowerCase().includes(normalizedQuery);
|
|
4557
|
+
if (matchesKey || matchesDescription) {
|
|
4558
|
+
results.push([key, schema]);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
if (results.length === 0) {
|
|
4562
|
+
console.log(chalk26.yellow(`
|
|
4563
|
+
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4564
|
+
`));
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
console.log(chalk26.blue.bold(`
|
|
4568
|
+
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4569
|
+
`));
|
|
4570
|
+
for (const [key, schema] of results) {
|
|
4571
|
+
const typeStr = formatType(schema.type);
|
|
4572
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
|
|
4573
|
+
console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}`);
|
|
4574
|
+
console.log(` ${chalk26.dim(schema.description)}`);
|
|
4575
|
+
if (schema.default !== void 0) {
|
|
4576
|
+
console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4577
|
+
}
|
|
4578
|
+
console.log();
|
|
4579
|
+
}
|
|
4580
|
+
} catch (error) {
|
|
4581
|
+
console.error(chalk26.red(`
|
|
4582
|
+
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4583
|
+
`));
|
|
4584
|
+
process.exit(1);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
var envCommand = new Command12("env").description("Manage environment variables");
|
|
4588
|
+
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);
|
|
4589
|
+
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4590
|
+
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);
|
|
4591
|
+
async function initEnvFiles(options) {
|
|
4592
|
+
const packageName = options.package || "@spfn/core";
|
|
4593
|
+
const cwd = process.cwd();
|
|
4594
|
+
try {
|
|
4595
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4596
|
+
const allVars = Object.entries(envSchema);
|
|
4597
|
+
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
4598
|
+
const target = getTargetFile(schema);
|
|
4599
|
+
const exampleFile = target + ".example";
|
|
4600
|
+
if (!acc[exampleFile]) acc[exampleFile] = [];
|
|
4601
|
+
acc[exampleFile].push([key, schema]);
|
|
4602
|
+
return acc;
|
|
4603
|
+
}, {});
|
|
4604
|
+
console.log(chalk26.blue.bold(`
|
|
4605
|
+
\u{1F680} Generating .env template files
|
|
4606
|
+
`));
|
|
4607
|
+
for (const [file, vars] of Object.entries(grouped)) {
|
|
4608
|
+
const filePath = resolve(cwd, file);
|
|
4609
|
+
if (existsSync23(filePath) && !options.force) {
|
|
4610
|
+
console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4611
|
+
continue;
|
|
4612
|
+
}
|
|
4613
|
+
const content = generateEnvFileContent(vars);
|
|
4614
|
+
writeFileSync18(filePath, content, "utf-8");
|
|
4615
|
+
console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4616
|
+
}
|
|
4617
|
+
console.log(chalk26.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
4618
|
+
console.log(chalk26.dim(" cp .env.example .env"));
|
|
4619
|
+
console.log(chalk26.dim(" cp .env.local.example .env.local"));
|
|
4620
|
+
console.log(chalk26.dim(" cp .env.server.example .env.server"));
|
|
4621
|
+
console.log(chalk26.dim(" cp .env.server.local.example .env.server.local\n"));
|
|
4622
|
+
} catch (error) {
|
|
4623
|
+
console.error(chalk26.red(`
|
|
4624
|
+
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4625
|
+
`));
|
|
4626
|
+
process.exit(1);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
function generateEnvFileContent(vars) {
|
|
4630
|
+
const lines = [
|
|
4631
|
+
"# Auto-generated by spfn env init",
|
|
4632
|
+
"# Copy this file and fill in the values",
|
|
4633
|
+
""
|
|
4634
|
+
];
|
|
4635
|
+
for (const [key, schema] of vars) {
|
|
4636
|
+
lines.push(`# ${schema.description}`);
|
|
4637
|
+
if (schema.required) {
|
|
4638
|
+
lines.push(`# [required]`);
|
|
4639
|
+
}
|
|
4640
|
+
if (schema.sensitive) {
|
|
4641
|
+
lines.push(`# [sensitive] - Do not commit this value!`);
|
|
4642
|
+
}
|
|
4643
|
+
let value = "";
|
|
4644
|
+
if (schema.default !== void 0) {
|
|
4645
|
+
value = String(schema.default);
|
|
4646
|
+
} else if (schema.examples && schema.examples.length > 0) {
|
|
4647
|
+
value = String(schema.examples[0]);
|
|
4648
|
+
}
|
|
4649
|
+
lines.push(`${key}=${value}`);
|
|
4650
|
+
lines.push("");
|
|
4651
|
+
}
|
|
4652
|
+
return lines.join("\n");
|
|
4653
|
+
}
|
|
4654
|
+
async function checkEnvFiles(options) {
|
|
4655
|
+
const packageName = options.package || "@spfn/core";
|
|
4656
|
+
const cwd = process.cwd();
|
|
4657
|
+
try {
|
|
4658
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4659
|
+
const allVars = Object.entries(envSchema);
|
|
4660
|
+
console.log(chalk26.blue.bold(`
|
|
4661
|
+
\u{1F50D} Checking .env files against schema
|
|
4662
|
+
`));
|
|
4663
|
+
const allFiles = [...ENV_FILES.nextjs, ...ENV_FILES.server];
|
|
4664
|
+
const loadedEnv = {};
|
|
4665
|
+
const issues = [];
|
|
4666
|
+
const warnings = [];
|
|
4667
|
+
for (const file of allFiles) {
|
|
4668
|
+
const filePath = resolve(cwd, file);
|
|
4669
|
+
if (!existsSync23(filePath)) {
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
4672
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
4673
|
+
const parsed = parse(content);
|
|
4674
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
4675
|
+
loadedEnv[key] = { value: value || "", file };
|
|
4676
|
+
}
|
|
4677
|
+
console.log(chalk26.dim(` \u{1F4C4} ${file} loaded`));
|
|
4678
|
+
}
|
|
4679
|
+
console.log("");
|
|
4680
|
+
for (const [key, schema] of allVars) {
|
|
4681
|
+
const expectedFile = getTargetFile(schema);
|
|
4682
|
+
const found = loadedEnv[key];
|
|
4683
|
+
if (!found) {
|
|
4684
|
+
if (schema.required && schema.default === void 0) {
|
|
4685
|
+
issues.push(`${chalk26.red("\u2717")} ${chalk26.cyan(key)} is required but not found in any .env file`);
|
|
4686
|
+
}
|
|
4687
|
+
continue;
|
|
4688
|
+
}
|
|
4689
|
+
const isNextjsFile = ENV_FILES.nextjs.includes(found.file);
|
|
4690
|
+
const isServerFile = ENV_FILES.server.includes(found.file);
|
|
4691
|
+
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
4692
|
+
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4693
|
+
if (schema.sensitive) {
|
|
4694
|
+
issues.push(
|
|
4695
|
+
`${chalk26.red("\u2717")} ${chalk26.cyan(key)} is sensitive and should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.yellow(found.file)} (security risk!)`
|
|
4696
|
+
);
|
|
4697
|
+
} else {
|
|
4698
|
+
warnings.push(
|
|
4699
|
+
`${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.dim(found.file)}`
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4705
|
+
const inSchema = allVars.some(([k]) => k === key);
|
|
4706
|
+
if (!inSchema) {
|
|
4707
|
+
warnings.push(`${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} in ${chalk26.dim(file)} is not in schema`);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
if (issues.length > 0) {
|
|
4711
|
+
console.log(chalk26.red.bold("Issues:"));
|
|
4712
|
+
for (const issue of issues) {
|
|
4713
|
+
console.log(` ${issue}`);
|
|
4714
|
+
}
|
|
4715
|
+
console.log("");
|
|
4716
|
+
}
|
|
4717
|
+
if (warnings.length > 0) {
|
|
4718
|
+
console.log(chalk26.yellow.bold("Warnings:"));
|
|
4719
|
+
for (const warning of warnings) {
|
|
4720
|
+
console.log(` ${warning}`);
|
|
4721
|
+
}
|
|
4722
|
+
console.log("");
|
|
4723
|
+
}
|
|
4724
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
4725
|
+
console.log(chalk26.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4726
|
+
} else {
|
|
4727
|
+
console.log(chalk26.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4728
|
+
`));
|
|
4729
|
+
if (issues.length > 0) {
|
|
4730
|
+
process.exit(1);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
} catch (error) {
|
|
4734
|
+
console.error(chalk26.red(`
|
|
4735
|
+
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4736
|
+
`));
|
|
4737
|
+
process.exit(1);
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
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);
|
|
4741
|
+
envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(checkEnvFiles);
|
|
4742
|
+
async function validateEnvVars(options) {
|
|
4743
|
+
const packages = options.packages || ["@spfn/core"];
|
|
4744
|
+
console.log(chalk26.blue.bold(`
|
|
4745
|
+
\u{1F50D} Validating environment variables
|
|
4746
|
+
`));
|
|
4747
|
+
const allErrors = [];
|
|
4748
|
+
const allWarnings = [];
|
|
4749
|
+
for (const packageName of packages) {
|
|
4750
|
+
try {
|
|
4751
|
+
console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
|
|
4752
|
+
const envSchema = await loadEnvSchema(packageName);
|
|
4753
|
+
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4754
|
+
const registry = createEnvRegistry(envSchema);
|
|
4755
|
+
const result = registry.validateAll();
|
|
4756
|
+
for (const error of result.errors) {
|
|
4757
|
+
allErrors.push({ ...error, package: packageName });
|
|
4758
|
+
}
|
|
4759
|
+
for (const warning of result.warnings) {
|
|
4760
|
+
allWarnings.push({ ...warning, package: packageName });
|
|
4761
|
+
}
|
|
4762
|
+
} catch (error) {
|
|
4763
|
+
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4764
|
+
console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4765
|
+
continue;
|
|
4766
|
+
}
|
|
4767
|
+
console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4768
|
+
if (options.strict) {
|
|
4769
|
+
process.exit(1);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
console.log("");
|
|
4774
|
+
if (allErrors.length > 0) {
|
|
4775
|
+
console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4776
|
+
`));
|
|
4777
|
+
for (const error of allErrors) {
|
|
4778
|
+
console.log(` ${chalk26.red("\u2717")} ${chalk26.cyan(error.key)}`);
|
|
4779
|
+
console.log(` ${chalk26.dim(error.message)}`);
|
|
4780
|
+
console.log(` ${chalk26.dim(`from ${error.package}`)}`);
|
|
4781
|
+
console.log("");
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
if (allWarnings.length > 0) {
|
|
4785
|
+
console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4786
|
+
`));
|
|
4787
|
+
for (const warning of allWarnings) {
|
|
4788
|
+
console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
|
|
4789
|
+
console.log(` ${chalk26.dim(warning.message)}`);
|
|
4790
|
+
console.log("");
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4794
|
+
console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4795
|
+
} else if (allErrors.length === 0) {
|
|
4796
|
+
console.log(chalk26.green("\u2705 No errors found."));
|
|
4797
|
+
console.log(chalk26.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4798
|
+
`));
|
|
4799
|
+
} else {
|
|
4800
|
+
console.log(chalk26.red(`
|
|
4801
|
+
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4802
|
+
`));
|
|
4803
|
+
process.exit(1);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
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);
|
|
4807
|
+
|
|
3333
4808
|
// src/index.ts
|
|
3334
|
-
|
|
3335
|
-
program
|
|
4809
|
+
init_version();
|
|
4810
|
+
var program = new Command13();
|
|
4811
|
+
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
3336
4812
|
program.addCommand(createCommand);
|
|
3337
4813
|
program.addCommand(initCommand);
|
|
3338
4814
|
program.addCommand(addCommand);
|
|
@@ -3344,6 +4820,7 @@ program.addCommand(codegenCommand);
|
|
|
3344
4820
|
program.addCommand(keyCommand);
|
|
3345
4821
|
program.addCommand(setupCommand);
|
|
3346
4822
|
program.addCommand(dbCommand);
|
|
4823
|
+
program.addCommand(envCommand);
|
|
3347
4824
|
async function run() {
|
|
3348
4825
|
await program.parseAsync(process.argv);
|
|
3349
4826
|
}
|