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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/cli.js +306 -0
  4. package/branding.json +8 -0
  5. package/package.json +72 -0
  6. package/src/__tests__/cli-smoke.test.js +46 -0
  7. package/src/blueprint/__tests__/blueprint.test.js +116 -0
  8. package/src/blueprint/blueprint.js +181 -0
  9. package/src/blueprint/default.blueprint.json +78 -0
  10. package/src/blueprint/index.js +10 -0
  11. package/src/blueprint/loader.js +101 -0
  12. package/src/blueprint/schema-kit.js +161 -0
  13. package/src/blueprint/schema.js +78 -0
  14. package/src/branding/__tests__/branding.test.js +49 -0
  15. package/src/branding/index.js +48 -0
  16. package/src/commands/__tests__/commands.test.js +83 -0
  17. package/src/commands/check.js +71 -0
  18. package/src/commands/cleanup.js +347 -0
  19. package/src/commands/customize.js +263 -0
  20. package/src/commands/doctor.js +84 -0
  21. package/src/commands/env.js +75 -0
  22. package/src/commands/finalize.js +68 -0
  23. package/src/commands/generate/ci-cd.js +378 -0
  24. package/src/commands/generate/deploy-advanced.js +253 -0
  25. package/src/commands/generate/deploy.js +99 -0
  26. package/src/commands/generate/env.template.js +221 -0
  27. package/src/commands/generate/index.js +7 -0
  28. package/src/commands/generate/module.js +836 -0
  29. package/src/commands/generate/page.js +1415 -0
  30. package/src/commands/generate/test-scaffold.js +279 -0
  31. package/src/commands/generate/theme.js +67 -0
  32. package/src/commands/generate-resource.js +133 -0
  33. package/src/commands/index.js +9 -0
  34. package/src/commands/init.js +350 -0
  35. package/src/commands/make/resource.js +298 -0
  36. package/src/commands/preset.js +57 -0
  37. package/src/commands/remove.js +170 -0
  38. package/src/commands/rename.js +54 -0
  39. package/src/commands/rollback.js +90 -0
  40. package/src/commands/wizard.js +303 -0
  41. package/src/core/__tests__/generator.test.js +67 -0
  42. package/src/core/__tests__/marker-strategy.test.js +57 -0
  43. package/src/core/__tests__/resource-definition.test.js +32 -0
  44. package/src/core/generator.js +542 -0
  45. package/src/core/marker-strategy.js +138 -0
  46. package/src/core/resource-definition.js +346 -0
  47. package/src/core/state-tracker.js +67 -0
  48. package/src/core/template-loader.js +163 -0
  49. package/src/engine/__tests__/engine.test.js +306 -0
  50. package/src/engine/index.js +21 -0
  51. package/src/engine/injector.js +198 -0
  52. package/src/engine/pipeline.js +138 -0
  53. package/src/engine/transaction.js +105 -0
  54. package/src/engine/validator.js +190 -0
  55. package/src/index.js +4 -0
  56. package/src/recipes/__tests__/recipe.test.js +128 -0
  57. package/src/recipes/builtin/module.json +22 -0
  58. package/src/recipes/builtin/page.json +21 -0
  59. package/src/recipes/builtin/resource.json +35 -0
  60. package/src/recipes/condition.js +147 -0
  61. package/src/recipes/index.js +11 -0
  62. package/src/recipes/loader.js +95 -0
  63. package/src/recipes/recipe.js +89 -0
  64. package/src/recipes/schema.js +47 -0
  65. package/src/schemas/__tests__/schemas.test.js +67 -0
  66. package/src/schemas/index.js +18 -0
  67. package/src/schemas/options.js +38 -0
  68. package/src/schemas/resource.js +112 -0
  69. package/src/services/__tests__/reporter.test.js +98 -0
  70. package/src/services/clock.js +31 -0
  71. package/src/services/index.js +43 -0
  72. package/src/services/reporter.js +136 -0
  73. package/src/templates/resource/api.js.ejs +39 -0
  74. package/src/templates/resource/components/form.jsx.ejs +81 -0
  75. package/src/templates/resource/components/table.jsx.ejs +68 -0
  76. package/src/templates/resource/controller.js.ejs +154 -0
  77. package/src/templates/resource/hooks.js.ejs +46 -0
  78. package/src/templates/resource/model.js.ejs +64 -0
  79. package/src/templates/resource/page-detail.jsx.ejs +55 -0
  80. package/src/templates/resource/page-form.jsx.ejs +30 -0
  81. package/src/templates/resource/page-inline.jsx.ejs +74 -0
  82. package/src/templates/resource/page-modal.jsx.ejs +98 -0
  83. package/src/templates/resource/page-page.jsx.ejs +99 -0
  84. package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
  85. package/src/templates/resource/routes.js.ejs +35 -0
  86. package/src/templates/resource/service.js.ejs +132 -0
  87. package/src/templates/resource/test.ejs +71 -0
  88. package/src/templates/resource/types.ts.ejs +17 -0
  89. package/src/templates/resource/validator.js.ejs +26 -0
  90. package/src/templates/snippets/lazy-import.ejs +1 -0
  91. package/src/templates/snippets/nav-entry.ejs +1 -0
  92. package/src/templates/snippets/route-entry.ejs +5 -0
  93. package/src/templates/snippets/route-mount.ejs +1 -0
  94. package/src/utils/fieldValidators.js +371 -0
  95. package/src/utils/logging/logger.js +47 -0
  96. package/src/utils/namingUtils.js +38 -0
  97. 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
+ }