stackloom-cli 1.0.0
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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/cli.js +306 -0
- package/branding.json +8 -0
- package/package.json +72 -0
- package/src/__tests__/cli-smoke.test.js +46 -0
- package/src/blueprint/__tests__/blueprint.test.js +116 -0
- package/src/blueprint/blueprint.js +181 -0
- package/src/blueprint/default.blueprint.json +78 -0
- package/src/blueprint/index.js +10 -0
- package/src/blueprint/loader.js +101 -0
- package/src/blueprint/schema-kit.js +161 -0
- package/src/blueprint/schema.js +78 -0
- package/src/branding/__tests__/branding.test.js +49 -0
- package/src/branding/index.js +48 -0
- package/src/commands/__tests__/commands.test.js +83 -0
- package/src/commands/check.js +71 -0
- package/src/commands/cleanup.js +347 -0
- package/src/commands/customize.js +263 -0
- package/src/commands/doctor.js +84 -0
- package/src/commands/env.js +75 -0
- package/src/commands/finalize.js +68 -0
- package/src/commands/generate/ci-cd.js +378 -0
- package/src/commands/generate/deploy-advanced.js +253 -0
- package/src/commands/generate/deploy.js +99 -0
- package/src/commands/generate/env.template.js +221 -0
- package/src/commands/generate/index.js +7 -0
- package/src/commands/generate/module.js +836 -0
- package/src/commands/generate/page.js +1415 -0
- package/src/commands/generate/test-scaffold.js +279 -0
- package/src/commands/generate/theme.js +67 -0
- package/src/commands/generate-resource.js +133 -0
- package/src/commands/index.js +9 -0
- package/src/commands/init.js +350 -0
- package/src/commands/make/resource.js +298 -0
- package/src/commands/preset.js +57 -0
- package/src/commands/remove.js +170 -0
- package/src/commands/rename.js +54 -0
- package/src/commands/rollback.js +90 -0
- package/src/commands/wizard.js +303 -0
- package/src/core/__tests__/generator.test.js +67 -0
- package/src/core/__tests__/marker-strategy.test.js +57 -0
- package/src/core/__tests__/resource-definition.test.js +32 -0
- package/src/core/generator.js +542 -0
- package/src/core/marker-strategy.js +138 -0
- package/src/core/resource-definition.js +346 -0
- package/src/core/state-tracker.js +67 -0
- package/src/core/template-loader.js +163 -0
- package/src/engine/__tests__/engine.test.js +306 -0
- package/src/engine/index.js +21 -0
- package/src/engine/injector.js +198 -0
- package/src/engine/pipeline.js +138 -0
- package/src/engine/transaction.js +105 -0
- package/src/engine/validator.js +190 -0
- package/src/index.js +4 -0
- package/src/recipes/__tests__/recipe.test.js +128 -0
- package/src/recipes/builtin/module.json +22 -0
- package/src/recipes/builtin/page.json +21 -0
- package/src/recipes/builtin/resource.json +35 -0
- package/src/recipes/condition.js +147 -0
- package/src/recipes/index.js +11 -0
- package/src/recipes/loader.js +95 -0
- package/src/recipes/recipe.js +89 -0
- package/src/recipes/schema.js +47 -0
- package/src/schemas/__tests__/schemas.test.js +67 -0
- package/src/schemas/index.js +18 -0
- package/src/schemas/options.js +38 -0
- package/src/schemas/resource.js +112 -0
- package/src/services/__tests__/reporter.test.js +98 -0
- package/src/services/clock.js +31 -0
- package/src/services/index.js +43 -0
- package/src/services/reporter.js +136 -0
- package/src/templates/resource/api.js.ejs +39 -0
- package/src/templates/resource/components/form.jsx.ejs +81 -0
- package/src/templates/resource/components/table.jsx.ejs +68 -0
- package/src/templates/resource/controller.js.ejs +154 -0
- package/src/templates/resource/hooks.js.ejs +46 -0
- package/src/templates/resource/model.js.ejs +64 -0
- package/src/templates/resource/page-detail.jsx.ejs +55 -0
- package/src/templates/resource/page-form.jsx.ejs +30 -0
- package/src/templates/resource/page-inline.jsx.ejs +74 -0
- package/src/templates/resource/page-modal.jsx.ejs +98 -0
- package/src/templates/resource/page-page.jsx.ejs +99 -0
- package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
- package/src/templates/resource/routes.js.ejs +35 -0
- package/src/templates/resource/service.js.ejs +132 -0
- package/src/templates/resource/test.ejs +71 -0
- package/src/templates/resource/types.ts.ejs +17 -0
- package/src/templates/resource/validator.js.ejs +26 -0
- package/src/templates/snippets/lazy-import.ejs +1 -0
- package/src/templates/snippets/nav-entry.ejs +1 -0
- package/src/templates/snippets/route-entry.ejs +5 -0
- package/src/templates/snippets/route-mount.ejs +1 -0
- package/src/utils/fieldValidators.js +371 -0
- package/src/utils/logging/logger.js +47 -0
- package/src/utils/namingUtils.js +38 -0
- package/src/utils/sanitize.js +200 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
|
|
9
|
+
const DESIGN_THEMES = [
|
|
10
|
+
"executiveBlue",
|
|
11
|
+
"clinicSoft",
|
|
12
|
+
"studioElevated",
|
|
13
|
+
"operationsDense",
|
|
14
|
+
"commerceWarm",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const DESIGN_LAYOUTS = [
|
|
18
|
+
"hybridSaas",
|
|
19
|
+
"sidebarWorkspace",
|
|
20
|
+
"topbarPortal",
|
|
21
|
+
"rightRailStudio",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DATA_TEMPLATES = ["dashboard", "denseOps", "editorial", "commerce"];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} projectRoot
|
|
28
|
+
*/
|
|
29
|
+
function getPresetPath(projectRoot) {
|
|
30
|
+
return path.join(projectRoot, "frontend/src/config/app-preset.js");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} projectRoot
|
|
35
|
+
*/
|
|
36
|
+
async function ensureProject(projectRoot) {
|
|
37
|
+
const presetPath = getPresetPath(projectRoot);
|
|
38
|
+
if (!fs.existsSync(presetPath)) {
|
|
39
|
+
console.log(
|
|
40
|
+
chalk.red("✖ Not a MERN Starter Kit project (missing app-preset.js)."),
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return await fs.readFile(presetPath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── THEME ──
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} theme
|
|
50
|
+
* @param {any} _options
|
|
51
|
+
*/
|
|
52
|
+
export async function customizeThemeSet(theme, _options) {
|
|
53
|
+
const spinner = ora();
|
|
54
|
+
const projectRoot = process.cwd();
|
|
55
|
+
let presetCode = await ensureProject(projectRoot);
|
|
56
|
+
|
|
57
|
+
let selectedTheme = theme;
|
|
58
|
+
if (!selectedTheme) {
|
|
59
|
+
const answers = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: "list",
|
|
62
|
+
name: "theme",
|
|
63
|
+
message: "Select a theme:",
|
|
64
|
+
choices: DESIGN_THEMES,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
selectedTheme = answers.theme;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (DESIGN_THEMES.includes(selectedTheme)) {
|
|
71
|
+
presetCode = presetCode.replace(
|
|
72
|
+
/theme:\s*designThemes\.\w+/,
|
|
73
|
+
`theme: designThemes.${selectedTheme}`,
|
|
74
|
+
);
|
|
75
|
+
await fs.writeFile(getPresetPath(projectRoot), presetCode);
|
|
76
|
+
spinner.succeed(`Theme set to "${selectedTheme}"`);
|
|
77
|
+
} else {
|
|
78
|
+
spinner.fail(`Invalid theme. Available: ${DESIGN_THEMES.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {any} options
|
|
84
|
+
*/
|
|
85
|
+
export async function customizeThemeImport(options) {
|
|
86
|
+
const spinner = ora();
|
|
87
|
+
const projectRoot = process.cwd();
|
|
88
|
+
await ensureProject(projectRoot);
|
|
89
|
+
|
|
90
|
+
const css = options.file
|
|
91
|
+
? fs.existsSync(options.file)
|
|
92
|
+
? await fs.readFile(options.file, "utf-8")
|
|
93
|
+
: (console.log(chalk.red(`✖ File not found: ${options.file}`)),
|
|
94
|
+
process.exit(1))
|
|
95
|
+
: options.paste;
|
|
96
|
+
|
|
97
|
+
if (!css) {
|
|
98
|
+
console.log(chalk.red('✖ Must provide --file <path> or --paste "<css>"'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cssPath = path.join(
|
|
103
|
+
projectRoot,
|
|
104
|
+
"frontend/src/config/imported-shadcn-theme.css",
|
|
105
|
+
);
|
|
106
|
+
await fs.writeFile(cssPath, css);
|
|
107
|
+
spinner.succeed(
|
|
108
|
+
"Theme CSS saved to frontend/src/config/imported-shadcn-theme.css",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.green(
|
|
113
|
+
"\n✓ Apply it in your app-preset.js using installShadcnDesignPreset:\n",
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
chalk.white(
|
|
118
|
+
`import { installShadcnDesignPreset } from "@/lib/shadcn-theme";`,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.white(`import customCss from "./imported-shadcn-theme.css";`),
|
|
123
|
+
);
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(chalk.white(`theme: installShadcnDesignPreset(customCss, {`));
|
|
126
|
+
console.log(
|
|
127
|
+
chalk.white(` fallback: "${options.fallback || "executiveBlue"}",`),
|
|
128
|
+
);
|
|
129
|
+
console.log(chalk.white(` appearance: "${options.appearance || "quiet"}"`));
|
|
130
|
+
console.log(chalk.white(`});`));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── LAYOUT ──
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} layout
|
|
136
|
+
*/
|
|
137
|
+
export async function customizeLayoutSet(layout) {
|
|
138
|
+
const spinner = ora();
|
|
139
|
+
const projectRoot = process.cwd();
|
|
140
|
+
let presetCode = await ensureProject(projectRoot);
|
|
141
|
+
|
|
142
|
+
let selectedLayout = layout;
|
|
143
|
+
if (!selectedLayout) {
|
|
144
|
+
const answers = await inquirer.prompt([
|
|
145
|
+
{
|
|
146
|
+
type: "list",
|
|
147
|
+
name: "layout",
|
|
148
|
+
message: "Select a layout:",
|
|
149
|
+
choices: DESIGN_LAYOUTS,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
selectedLayout = answers.layout;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (DESIGN_LAYOUTS.includes(selectedLayout)) {
|
|
156
|
+
presetCode = presetCode.replace(
|
|
157
|
+
/layout:\s*designLayouts\.\w+/,
|
|
158
|
+
`layout: designLayouts.${selectedLayout}`,
|
|
159
|
+
);
|
|
160
|
+
await fs.writeFile(getPresetPath(projectRoot), presetCode);
|
|
161
|
+
spinner.succeed(`Layout set to "${selectedLayout}"`);
|
|
162
|
+
} else {
|
|
163
|
+
spinner.fail(`Invalid layout. Available: ${DESIGN_LAYOUTS.join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── BRAND ──
|
|
168
|
+
/**
|
|
169
|
+
* @param {any} options
|
|
170
|
+
*/
|
|
171
|
+
export async function customizeBrandSet(options) {
|
|
172
|
+
const spinner = ora();
|
|
173
|
+
const projectRoot = process.cwd();
|
|
174
|
+
let presetCode = await ensureProject(projectRoot);
|
|
175
|
+
|
|
176
|
+
const name = options.name || options.n;
|
|
177
|
+
const tagline = options.tagline || options.t;
|
|
178
|
+
|
|
179
|
+
if (!name && !tagline) {
|
|
180
|
+
console.log(chalk.red("✖ Must provide at least --name or --tagline"));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (name) {
|
|
185
|
+
presetCode = presetCode.replace(
|
|
186
|
+
/brand:\s*\{\s*name:\s*["'][^"']+["']/,
|
|
187
|
+
`brand: { name: "${name}"`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (tagline) {
|
|
191
|
+
presetCode = presetCode.replace(
|
|
192
|
+
/tagline:\s*["'][^"']+["']/,
|
|
193
|
+
`tagline: "${tagline}"`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await fs.writeFile(getPresetPath(projectRoot), presetCode);
|
|
198
|
+
const parts = [];
|
|
199
|
+
if (name) parts.push("name=" + name);
|
|
200
|
+
if (tagline) parts.push("tagline=" + tagline);
|
|
201
|
+
spinner.succeed(`Brand updated (${parts.join(", ")})`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── DATA DISPLAY ──
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} template
|
|
207
|
+
*/
|
|
208
|
+
export async function customizeDataSet(template) {
|
|
209
|
+
const spinner = ora();
|
|
210
|
+
const projectRoot = process.cwd();
|
|
211
|
+
let presetCode = await ensureProject(projectRoot);
|
|
212
|
+
|
|
213
|
+
let selectedTemplate = template;
|
|
214
|
+
if (!selectedTemplate) {
|
|
215
|
+
const answers = await inquirer.prompt([
|
|
216
|
+
{
|
|
217
|
+
type: "list",
|
|
218
|
+
name: "template",
|
|
219
|
+
message: "Select a data display template:",
|
|
220
|
+
choices: DATA_TEMPLATES,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
selectedTemplate = answers.template;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (DATA_TEMPLATES.includes(selectedTemplate)) {
|
|
227
|
+
presetCode = presetCode.replace(
|
|
228
|
+
/dataDisplay:\s*dataDisplayTemplates\.\w+/,
|
|
229
|
+
`dataDisplay: dataDisplayTemplates.${selectedTemplate}`,
|
|
230
|
+
);
|
|
231
|
+
await fs.writeFile(getPresetPath(projectRoot), presetCode);
|
|
232
|
+
spinner.succeed(`Data display template set to "${selectedTemplate}"`);
|
|
233
|
+
} else {
|
|
234
|
+
spinner.fail(`Invalid template. Available: ${DATA_TEMPLATES.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── LISTERS ──
|
|
239
|
+
export function customizeListThemes() {
|
|
240
|
+
console.log(chalk.cyan("\nAvailable themes:\n"));
|
|
241
|
+
DESIGN_THEMES.forEach((t) => console.log(` ${chalk.white("•")} ${t}`));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function customizeListLayouts() {
|
|
245
|
+
console.log(chalk.cyan("\nAvailable layouts:\n"));
|
|
246
|
+
DESIGN_LAYOUTS.forEach((l) => console.log(` ${chalk.white("•")} ${l}`));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function customizeListData() {
|
|
250
|
+
console.log(chalk.cyan("\nAvailable data display templates:\n"));
|
|
251
|
+
DATA_TEMPLATES.forEach((d) => console.log(` ${chalk.white("•")} ${d}`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default {
|
|
255
|
+
customizeThemeSet,
|
|
256
|
+
customizeThemeImport,
|
|
257
|
+
customizeLayoutSet,
|
|
258
|
+
customizeBrandSet,
|
|
259
|
+
customizeDataSet,
|
|
260
|
+
customizeListThemes,
|
|
261
|
+
customizeListLayouts,
|
|
262
|
+
customizeListData,
|
|
263
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Doctor Command — checks environment and project health
|
|
11
|
+
*/
|
|
12
|
+
export default async function doctorCmd() {
|
|
13
|
+
const spinner = ora();
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
// Track hard failures so the command exits non-zero — a doctor that always
|
|
16
|
+
// exits 0 is useless in CI and scripts.
|
|
17
|
+
let failures = 0;
|
|
18
|
+
|
|
19
|
+
console.log(chalk.cyan.bold('\n🏥 Stackloom Doctor — System Health Check\n'));
|
|
20
|
+
|
|
21
|
+
// 1. Environment Checks
|
|
22
|
+
spinner.start('Checking Node.js version...');
|
|
23
|
+
const nodeVersion = process.version;
|
|
24
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
25
|
+
if (major < 18) {
|
|
26
|
+
spinner.fail(`Node.js version ${nodeVersion} is too old. Required: >=18.0.0`);
|
|
27
|
+
failures++;
|
|
28
|
+
} else {
|
|
29
|
+
spinner.succeed(`Node.js ${nodeVersion} detected`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spinner.start('Checking pnpm...');
|
|
33
|
+
try {
|
|
34
|
+
const pnpmVersion = execSync('pnpm -v').toString().trim();
|
|
35
|
+
spinner.succeed(`pnpm ${pnpmVersion} detected`);
|
|
36
|
+
} catch {
|
|
37
|
+
spinner.warn('pnpm not found. It is recommended for this starter kit.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Project Checks
|
|
41
|
+
spinner.start('Checking project structure...');
|
|
42
|
+
const isProject = await fs.pathExists(path.join(projectRoot, 'backend')) &&
|
|
43
|
+
await fs.pathExists(path.join(projectRoot, 'frontend'));
|
|
44
|
+
|
|
45
|
+
if (!isProject) {
|
|
46
|
+
spinner.fail('Not a MERN Starter Kit project. Run this inside the project root.');
|
|
47
|
+
failures++;
|
|
48
|
+
} else {
|
|
49
|
+
spinner.succeed('Project structure valid');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Dependencies
|
|
53
|
+
if (isProject) {
|
|
54
|
+
spinner.start('Checking backend dependencies...');
|
|
55
|
+
if (await fs.pathExists(path.join(projectRoot, 'backend/node_modules'))) {
|
|
56
|
+
spinner.succeed('Backend dependencies installed');
|
|
57
|
+
} else {
|
|
58
|
+
spinner.warn('Backend dependencies missing. Run `pnpm install`');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
spinner.start('Checking frontend dependencies...');
|
|
62
|
+
if (await fs.pathExists(path.join(projectRoot, 'frontend/node_modules'))) {
|
|
63
|
+
spinner.succeed('Frontend dependencies installed');
|
|
64
|
+
} else {
|
|
65
|
+
spinner.warn('Frontend dependencies missing. Run `pnpm install`');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Configuration
|
|
69
|
+
spinner.start('Checking environment files...');
|
|
70
|
+
const hasBackendEnv = await fs.pathExists(path.join(projectRoot, 'backend/.env'));
|
|
71
|
+
if (hasBackendEnv) {
|
|
72
|
+
spinner.succeed('Backend .env found');
|
|
73
|
+
} else {
|
|
74
|
+
spinner.warn('Backend .env missing (use .env.example)');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (failures > 0) {
|
|
79
|
+
console.log(chalk.red(`\n✖ Health check found ${failures} issue(s).\n`));
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.green('\n✓ Health check complete — all good.\n'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loom env` — keep `.env` in sync with `.env.example`.
|
|
3
|
+
*
|
|
4
|
+
* The most common "works on my machine" bug is a missing env var. This diffs
|
|
5
|
+
* the two files and, with `--sync`, appends any keys the example declares but
|
|
6
|
+
* `.env` is missing (values left blank for the developer to fill).
|
|
7
|
+
*
|
|
8
|
+
* Pure dotenv-style parsing — no dependency on the `dotenv` package.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { reporterFromOptions } from "../services/index.js";
|
|
13
|
+
|
|
14
|
+
/** Parse `KEY=value` lines into an ordered list of keys (comments/blanks skipped). */
|
|
15
|
+
export function parseEnvKeys(content) {
|
|
16
|
+
const keys = [];
|
|
17
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
18
|
+
const line = rawLine.trim();
|
|
19
|
+
if (!line || line.startsWith("#")) continue;
|
|
20
|
+
const eq = line.indexOf("=");
|
|
21
|
+
if (eq > 0) keys.push(line.slice(0, eq).trim());
|
|
22
|
+
}
|
|
23
|
+
return keys;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} [options] - global flags plus:
|
|
28
|
+
* @param {boolean} [options.sync] - append missing keys to .env
|
|
29
|
+
* @param {string} [options.projectRoot]
|
|
30
|
+
* @param {object} [options.reporter]
|
|
31
|
+
*/
|
|
32
|
+
export default async function env(options = {}) {
|
|
33
|
+
const reporter = options.reporter ?? reporterFromOptions(options);
|
|
34
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
35
|
+
const examplePath = path.join(projectRoot, ".env.example");
|
|
36
|
+
const envPath = path.join(projectRoot, ".env");
|
|
37
|
+
|
|
38
|
+
if (!existsSync(examplePath)) {
|
|
39
|
+
reporter.warn("No .env.example found — nothing to compare against.");
|
|
40
|
+
reporter.result({ missing: [], synced: false });
|
|
41
|
+
reporter.flush();
|
|
42
|
+
return { missing: [], synced: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const exampleKeys = parseEnvKeys(readFileSync(examplePath, "utf-8"));
|
|
46
|
+
const currentKeys = existsSync(envPath)
|
|
47
|
+
? new Set(parseEnvKeys(readFileSync(envPath, "utf-8")))
|
|
48
|
+
: new Set();
|
|
49
|
+
const missing = exampleKeys.filter((key) => !currentKeys.has(key));
|
|
50
|
+
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
reporter.success(".env is in sync with .env.example");
|
|
53
|
+
reporter.result({ missing: [], synced: false });
|
|
54
|
+
reporter.flush();
|
|
55
|
+
return { missing: [], synced: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
reporter.warn(`${missing.length} key(s) missing from .env: ${missing.join(", ")}`);
|
|
59
|
+
|
|
60
|
+
let synced = false;
|
|
61
|
+
if (options.sync) {
|
|
62
|
+
const block = `${missing.map((key) => `${key}=`).join("\n")}\n`;
|
|
63
|
+
if (existsSync(envPath)) appendFileSync(envPath, `\n# Added by 'loom env --sync'\n${block}`);
|
|
64
|
+
else writeFileSync(envPath, `# Generated by 'loom env --sync'\n${block}`);
|
|
65
|
+
synced = true;
|
|
66
|
+
reporter.success(`Appended ${missing.length} key(s) to .env — fill in the values.`);
|
|
67
|
+
} else {
|
|
68
|
+
reporter.info("Run 'loom env --sync' to append the missing keys.");
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reporter.result({ missing, synced });
|
|
73
|
+
reporter.flush();
|
|
74
|
+
return { missing, synced };
|
|
75
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Finalize Command — prepares the project for production
|
|
11
|
+
*/
|
|
12
|
+
export default async function finalizeCmd() {
|
|
13
|
+
const spinner = ora();
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan.bold('\n🚀 Finalizing Project for Production\n'));
|
|
17
|
+
|
|
18
|
+
// 1. Linting
|
|
19
|
+
spinner.start('Running linting checks...');
|
|
20
|
+
try {
|
|
21
|
+
execSync('pnpm lint', { cwd: projectRoot, stdio: 'pipe' });
|
|
22
|
+
spinner.succeed('Linting passed');
|
|
23
|
+
} catch {
|
|
24
|
+
spinner.fail('Linting failed. Please fix errors before finalizing.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Type Checking
|
|
29
|
+
spinner.start('Running type checks...');
|
|
30
|
+
try {
|
|
31
|
+
execSync('pnpm -C backend exec tsc --noEmit', { cwd: projectRoot, stdio: 'pipe' });
|
|
32
|
+
execSync('pnpm -C frontend exec tsc --noEmit', { cwd: projectRoot, stdio: 'pipe' });
|
|
33
|
+
spinner.succeed('Type checks passed');
|
|
34
|
+
} catch {
|
|
35
|
+
spinner.warn('Type checks failed or tsc not found. Skipping.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. Tests
|
|
39
|
+
spinner.start('Running all tests...');
|
|
40
|
+
try {
|
|
41
|
+
execSync('pnpm test', { cwd: projectRoot, stdio: 'pipe' });
|
|
42
|
+
spinner.succeed('All tests passed');
|
|
43
|
+
} catch {
|
|
44
|
+
spinner.fail('Tests failed. Please fix before production.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. Build
|
|
49
|
+
spinner.start('Building for production...');
|
|
50
|
+
try {
|
|
51
|
+
execSync('pnpm build', { cwd: projectRoot, stdio: 'pipe' });
|
|
52
|
+
spinner.succeed('Production build successful');
|
|
53
|
+
} catch {
|
|
54
|
+
spinner.fail('Build failed.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 5. Security Audit
|
|
59
|
+
spinner.start('Running security audit...');
|
|
60
|
+
try {
|
|
61
|
+
execSync('npm audit', { cwd: projectRoot, stdio: 'pipe' });
|
|
62
|
+
spinner.succeed('Security audit passed');
|
|
63
|
+
} catch {
|
|
64
|
+
spinner.warn('Security vulnerabilities detected. Run `npm audit fix`.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green.bold('\n✨ Project is production-ready!\n'));
|
|
68
|
+
}
|