nibula 1.2.3 โ†’ 1.2.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to Nibula are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.4] - 2026-07-31
9
+
10
+ ### Changed
11
+ - Refactor interno di `tools/`: la logica รจ divisa in `lib/`, `cli/` e
12
+ `config/`, con valori e messaggi estratti in `settings.json` e
13
+ `messages.json`. Nessun cambiamento di comportamento.
14
+ - La CLI usa una palette uniforme: il colore resta solo sul riquadro del
15
+ titolo, i numeri delle voci sono attenuati.
16
+
17
+ ### Fixed
18
+ - L'ultima riga del menu non chiudeva il codice colore `dim`, che restava
19
+ attivo sulla riga successiva.
20
+
8
21
  ## [1.2.3] - 2026-07-29
9
22
 
10
23
  ### Fixed
package/README.md CHANGED
@@ -21,7 +21,7 @@ Building a website from scratch involves a lot of moving parts: templating, buil
21
21
  - ๐Ÿชถ **Lightweight by default** โ€” SCSS frameworks can be filtered so you ship only what you actually use
22
22
  - ๐ŸŒ **Open source** โ€” free to use, free to modify, free to share
23
23
 
24
- ![Version](https://img.shields.io/badge/version-1.2.3-blue)
24
+ ![Version](https://img.shields.io/badge/version-1.2.4-blue)
25
25
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
26
26
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
27
27
 
package/bin/create.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { writeSync } = require('fs');
7
7
  const { spawnSync } = require('child_process');
8
- const { color } = require('../tools/modules/constants');
8
+ const { color } = require('../tools/lib/colors');
9
9
 
10
10
  // โ”€โ”€ PATHS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
11
 
package/bin/nibula.js CHANGED
@@ -1,11 +1,12 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const https = require('https');
6
6
  const readline = require('readline');
7
7
  const { spawnSync } = require('child_process');
8
- const { findProjectRoot, color, NOT_INSIDE_PROJECT_MESSAGE } = require('../tools/modules/constants');
8
+ const { findProjectRoot, NOT_INSIDE_PROJECT_MESSAGE } = require('../tools/lib/paths');
9
+ const { color } = require('../tools/lib/colors');
9
10
 
10
11
  const pkg = require('../package.json');
11
12
 
@@ -41,8 +42,6 @@ function maybeDelegateToLocal(root) {
41
42
  const local = path.join(root, 'node_modules', 'nibula', 'bin', 'nibula.js');
42
43
  if (!fs.existsSync(local)) return;
43
44
 
44
- // Compare REAL paths so symlinks (e.g. from `npm link`) resolve to the same
45
- // physical file; otherwise the guard would loop forever under npm link.
46
45
  let localReal;
47
46
  let selfReal;
48
47
  try { localReal = fs.realpathSync(local); } catch { return; }
@@ -150,8 +149,6 @@ function globalNibulaPath() {
150
149
  return fs.existsSync(candidate) ? candidate : null;
151
150
  }
152
151
 
153
- // Riesegue il comando con la versione appena installata: questo processo ha
154
- // ancora in memoria il codice vecchio, quindi non puรฒ scaffoldare da solo.
155
152
  function reexecAfterUpdate(args) {
156
153
  const target = globalNibulaPath();
157
154
  if (!target) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nibula",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "A beginner-friendly static site generator built on Eleventy that stays close to vanilla HTML, CSS and JS โ€” with a page-management CLI, an optional PHP backend, and SEO files generated for you.",
5
5
  "keywords": [
6
6
  "static-site-generator",
@@ -1,152 +1,123 @@
1
- const readline = require('readline');
2
-
3
- const { addPage, removePage, renamePage, pageExists } = require('./modules/updatePage');
4
- const { updateOutputPath, getCurrentOutputPath } = require('./modules/updateOutputPath');
5
- const { validatePageName, validateOutputPath, checkRequiredFiles } = require('./modules/validation');
6
- const { toKebabCase } = require('./modules/utils');
7
- const { color } = require('./modules/constants');
8
-
9
- const rl = readline.createInterface({
10
- input: process.stdin,
11
- output: process.stdout,
12
- terminal: true,
13
- });
14
-
15
- function sanitizeInput(value) {
16
- return (value ?? '').replace(/[\x00-\x1F\x7F]/g, '').trim();
17
- }
18
-
19
- function ask(prompt) {
20
- return new Promise((resolve) => {
21
- const onClose = () => resolve(null);
22
- rl.once('close', onClose);
23
- rl.question(prompt, (answer) => {
24
- rl.off('close', onClose);
25
- resolve(sanitizeInput(answer));
26
- });
27
- });
28
- }
29
-
30
- async function confirm(prompt) {
31
- const answer = await ask(`${prompt} ${color.dim}[y/N]${color.reset} `);
32
- return /^y(es)?$/i.test((answer ?? '').trim());
33
- }
34
-
35
- async function askPageName(prompt) {
36
- const raw = await ask(prompt);
37
- if (raw === null) return null;
38
-
39
- const name = toKebabCase(raw);
40
- const error = validatePageName(name);
41
- if (error) {
42
- console.log(`\n${color.red}โœ– ${error}${color.reset}`);
43
- return null;
44
- }
45
- return name;
46
- }
47
-
48
- async function handleCreate() {
49
- const name = await askPageName(`\n${color.green}โฏ${color.reset} Name of the new page: `);
50
- if (name) addPage(name);
51
- }
52
-
53
- async function handleRemove() {
54
- const name = await askPageName(`\n${color.red}โฏ${color.reset} Name of the page to remove: `);
55
- if (!name) return;
56
-
57
- if (!pageExists(name)) {
58
- console.log(`\n${color.yellow}โš  Page "${name}" does not exist.${color.reset}`);
59
- return;
60
- }
61
-
62
- const confirmed = await confirm(`This permanently deletes all files for "${name}".`);
63
- if (!confirmed) {
64
- console.log(`\n${color.dim}Cancelled.${color.reset}`);
65
- return;
66
- }
67
- removePage(name);
68
- }
69
-
70
- async function handleRename() {
71
- const oldName = await askPageName(`\n${color.yellow}โฏ${color.reset} Page to rename: `);
72
- if (!oldName) return;
73
-
74
- const newName = await askPageName(`${color.yellow}โฏ${color.reset} New name: `);
75
- if (!newName) return;
76
-
77
- if (oldName === newName) {
78
- console.log(`\n${color.yellow}โš  Old and new name are the same.${color.reset}`);
79
- return;
80
- }
81
- renamePage(oldName, newName);
82
- }
83
-
84
- async function handleOutputPath() {
85
- const current = getCurrentOutputPath();
86
- const label = current ? `\n${color.dim}Current path: "${current}"${color.reset}\n` : '\n';
87
-
88
- const input = await ask(`${label}${color.magenta}โฏ${color.reset} New output path: `);
89
- if (input === null) return;
90
-
91
- const error = validateOutputPath(input);
92
- if (error) {
93
- console.log(`\n${color.red}โœ– ${error}${color.reset}`);
94
- return;
95
- }
96
- updateOutputPath(input);
97
- }
98
-
99
- const MENU_ACTIONS = {
100
- '1': handleCreate,
101
- '2': handleRemove,
102
- '3': handleRename,
103
- '4': handleOutputPath,
104
- };
105
-
106
- function renderMenu() {
107
- console.log(`\n${color.cyan}${color.bold}โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ`);
108
- console.log(`โ”‚ Nibula CLI โ”‚`);
109
- console.log(`โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ${color.reset}\n`);
110
- console.log(` ${color.green}1.${color.reset} Create page`);
111
- console.log(` ${color.red}2.${color.reset} Remove page`);
112
- console.log(` ${color.yellow}3.${color.reset} Rename page`);
113
- console.log(` ${color.magenta}4.${color.reset} Configure output path`);
114
- console.log(` ${color.dim}CTRL + C to exit\n`);
115
- }
116
-
117
- async function main() {
118
- const missing = checkRequiredFiles();
119
- if (missing.length > 0) {
120
- console.log(`\n${color.red}โœ– This project is missing required files:${color.reset}`);
121
- for (const item of missing) {
122
- console.log(` ${color.red}-${color.reset} ${item.label}`);
123
- }
124
- console.log(`\n${color.dim}The project may be incomplete or created with a different Nibula version.${color.reset}`);
125
- rl.close();
126
- process.exit(1);
127
- }
128
-
129
- while (true) {
130
- renderMenu();
131
-
132
- const choice = await ask(`${color.cyan}โฏ${color.reset} Choose an option: `);
133
- if (choice === null) break;
134
-
135
- const action = MENU_ACTIONS[choice];
136
- if (!action) {
137
- console.log(`\n${color.red}โœ– Invalid option.${color.reset}`);
138
- continue;
139
- }
140
-
141
- try {
142
- await action();
143
- } catch (err) {
144
- console.log(`\n${color.red}โœ– Unexpected error: ${err.message}${color.reset}`);
145
- }
146
- }
147
-
148
- rl.close();
149
- process.exit(0);
150
- }
151
-
1
+ const settings = require('./config/settings.json');
2
+ const { message } = require('./lib/logger');
3
+ const { toKebabCase } = require('./lib/text');
4
+ const { validatePageName, validateOutputPath, checkRequiredFiles } = require('./lib/validation');
5
+ const { addPage, removePage, renamePage } = require('./lib/pageActions');
6
+ const { pageExists } = require('./lib/pageArtifacts');
7
+ const { updateOutputPath, getCurrentOutputPath } = require('./lib/outputPath');
8
+ const { ask, confirm, close } = require('./cli/prompt');
9
+ const ui = require('./cli/ui');
10
+
11
+ const NEW_LINE = '\n';
12
+ const NO_PREFIX = '';
13
+
14
+ async function askPageName(prompt) {
15
+ const raw = await ask(prompt);
16
+ if (raw === null) return null;
17
+
18
+ const name = toKebabCase(raw);
19
+ const error = validatePageName(name);
20
+ if (error) {
21
+ ui.error(error);
22
+ return null;
23
+ }
24
+ return name;
25
+ }
26
+
27
+ async function handleCreate() {
28
+ const name = await askPageName(ui.promptLine(message('cli.createPrompt')));
29
+ if (name) addPage(name);
30
+ }
31
+
32
+ async function handleRemove() {
33
+ const name = await askPageName(ui.promptLine(message('cli.removePrompt')));
34
+ if (!name) return;
35
+
36
+ if (!pageExists(name)) {
37
+ ui.warning(message('cli.pageNotFound', { name }));
38
+ return;
39
+ }
40
+
41
+ const confirmed = await confirm(message('cli.confirmRemove', { name }));
42
+ if (!confirmed) {
43
+ ui.notice(message('cli.cancelled'));
44
+ return;
45
+ }
46
+ removePage(name);
47
+ }
48
+
49
+ async function handleRename() {
50
+ const oldName = await askPageName(ui.promptLine(message('cli.renameFromPrompt')));
51
+ if (!oldName) return;
52
+
53
+ const newName = await askPageName(ui.promptLine(message('cli.renameToPrompt'), NO_PREFIX));
54
+ if (!newName) return;
55
+
56
+ if (oldName === newName) {
57
+ ui.warning(message('cli.sameName'));
58
+ return;
59
+ }
60
+ renamePage(oldName, newName);
61
+ }
62
+
63
+ async function handleOutputPath() {
64
+ const current = getCurrentOutputPath();
65
+ const prefix = current
66
+ ? ui.noticePrefix(message('cli.currentOutputPath', { path: current }))
67
+ : NEW_LINE;
68
+
69
+ const input = await ask(ui.promptLine(message('cli.outputPathPrompt'), prefix));
70
+ if (input === null) return;
71
+
72
+ const error = validateOutputPath(input);
73
+ if (error) {
74
+ ui.error(error);
75
+ return;
76
+ }
77
+ updateOutputPath(input);
78
+ }
79
+
80
+ const ACTIONS = {
81
+ createPage: handleCreate,
82
+ removePage: handleRemove,
83
+ renamePage: handleRename,
84
+ outputPath: handleOutputPath,
85
+ };
86
+
87
+ function findAction(choice) {
88
+ const item = settings.cli.menu.find((entry) => entry.key === choice);
89
+ return item ? ACTIONS[item.action] : null;
90
+ }
91
+
92
+ async function main() {
93
+ const missing = checkRequiredFiles();
94
+ if (missing.length > 0) {
95
+ ui.renderMissingFiles(missing);
96
+ close();
97
+ process.exit(1);
98
+ }
99
+
100
+ while (true) {
101
+ ui.renderMenu();
102
+
103
+ const choice = await ask(ui.promptLine(message('cli.menuPrompt'), NO_PREFIX));
104
+ if (choice === null) break;
105
+
106
+ const action = findAction(choice);
107
+ if (!action) {
108
+ ui.error(message('cli.invalidOption'));
109
+ continue;
110
+ }
111
+
112
+ try {
113
+ await action();
114
+ } catch (error) {
115
+ ui.error(message('cli.unexpectedError', { error: error.message }));
116
+ }
117
+ }
118
+
119
+ close();
120
+ process.exit(0);
121
+ }
122
+
152
123
  main();
package/tools/buildJs.js CHANGED
@@ -1,37 +1,39 @@
1
- const esbuild = require('esbuild');
2
- const glob = require('glob');
3
- const path = require('path');
4
- const fs = require('fs');
5
- const { findProjectRoot, NOT_INSIDE_PROJECT_MESSAGE } = require('./modules/constants');
6
-
7
- const root = findProjectRoot(process.cwd());
8
- if (!root) {
9
- console.error(NOT_INSIDE_PROJECT_MESSAGE);
10
- process.exit(1);
11
- }
12
-
13
- const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
14
- const outputDir = pkg.outputDir || 'out';
15
- const isWatch = process.argv.includes('--watch');
16
-
17
- const posix = (p) => p.split(path.sep).join('/');
18
- const jsFiles = glob.sync(posix(path.join(root, 'src/frontend/js/pages/*.js')));
19
- const tsFiles = glob.sync(posix(path.join(root, 'src/frontend/ts/pages/*.ts')));
20
- const entryPoints = [...jsFiles, ...tsFiles];
21
-
22
- if (entryPoints.length === 0) {
23
- process.exit(0);
24
- }
25
-
26
- const options = {
27
- entryPoints,
28
- bundle: true,
29
- outdir: path.join(root, outputDir, 'js/pages'),
30
- minify: !isWatch,
31
- };
32
-
33
- if (isWatch) {
34
- esbuild.context(options).then((ctx) => ctx.watch()).catch(() => process.exit(1));
35
- } else {
36
- esbuild.build(options).catch(() => process.exit(1));
37
- }
1
+ const path = require('path');
2
+ const esbuild = require('esbuild');
3
+ const glob = require('glob');
4
+ const settings = require('./config/settings.json');
5
+ const { PATHS } = require('./lib/paths');
6
+ const { readPackageJson, allScriptEntries } = require('./lib/project');
7
+
8
+ const WATCH_FLAG = '--watch';
9
+ const PATH_SEPARATOR = '/';
10
+
11
+ function toPosix(target) {
12
+ return target.split(path.sep).join(PATH_SEPARATOR);
13
+ }
14
+
15
+ function collectEntryPoints() {
16
+ return allScriptEntries().flatMap((entries) => glob.sync(toPosix(path.join(PATHS.root, entries))));
17
+ }
18
+
19
+ const packageJson = readPackageJson();
20
+ const outputDirectory = packageJson[settings.packageJson.outputKey] || settings.project.defaultOutputDirectory;
21
+ const watch = process.argv.includes(WATCH_FLAG);
22
+ const entryPoints = collectEntryPoints();
23
+
24
+ if (entryPoints.length === 0) {
25
+ process.exit(0);
26
+ }
27
+
28
+ const options = {
29
+ entryPoints,
30
+ bundle: true,
31
+ outdir: path.join(PATHS.root, outputDirectory, ...settings.build.scriptOutput.split(PATH_SEPARATOR)),
32
+ minify: !watch,
33
+ };
34
+
35
+ if (watch) {
36
+ esbuild.context(options).then((context) => context.watch()).catch(() => process.exit(1));
37
+ } else {
38
+ esbuild.build(options).catch(() => process.exit(1));
39
+ }
@@ -1,25 +1,23 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { findProjectRoot, NOT_INSIDE_PROJECT_MESSAGE } = require('./modules/constants');
4
-
5
- const root = findProjectRoot(process.cwd());
6
- if (!root) {
7
- console.error(NOT_INSIDE_PROJECT_MESSAGE);
8
- process.exit(1);
9
- }
10
-
11
- const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
12
-
13
- if (!pkg.outputDir) {
14
- console.log('(!) outputDir not found in package.json');
15
- process.exit(1);
16
- }
17
-
18
- const outputDir = path.resolve(root, pkg.outputDir);
19
-
20
- if (fs.existsSync(outputDir)) {
21
- fs.rmSync(outputDir, { recursive: true, force: true });
22
- console.log(`(โœ“) cleaned โ†’ ${outputDir}`);
23
- } else {
24
- console.log(`(i) nothing to clean โ†’ ${outputDir}`);
25
- }
1
+ const path = require('path');
2
+ const settings = require('./config/settings.json');
3
+ const { PATHS } = require('./lib/paths');
4
+ const { readPackageJson } = require('./lib/project');
5
+ const { removeDirectory } = require('./lib/files');
6
+ const { log } = require('./lib/logger');
7
+
8
+ const OUTPUT_KEY = settings.packageJson.outputKey;
9
+
10
+ const packageJson = readPackageJson();
11
+
12
+ if (!packageJson[OUTPUT_KEY]) {
13
+ log('output.missingConfiguration', { key: OUTPUT_KEY, file: settings.paths.packageJson });
14
+ process.exit(1);
15
+ }
16
+
17
+ const outputDirectory = path.resolve(PATHS.root, packageJson[OUTPUT_KEY]);
18
+
19
+ if (removeDirectory(outputDirectory)) {
20
+ log('output.cleaned', { path: outputDirectory });
21
+ } else {
22
+ log('output.nothingToClean', { path: outputDirectory });
23
+ }
@@ -0,0 +1,40 @@
1
+ const readline = require('readline');
2
+ const settings = require('../config/settings.json');
3
+ const { color } = require('../lib/colors');
4
+
5
+ const CONTROL_CHARACTERS_PATTERN = /[\x00-\x1F\x7F]/g;
6
+ const AFFIRMATIVE_PATTERN = /^y(es)?$/i;
7
+ const CONFIRM_HINT = settings.cli.confirmHint;
8
+ const EMPTY = '';
9
+
10
+ const readlineInterface = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ terminal: true,
14
+ });
15
+
16
+ function sanitizeInput(value) {
17
+ return (value ?? EMPTY).replace(CONTROL_CHARACTERS_PATTERN, EMPTY).trim();
18
+ }
19
+
20
+ function ask(prompt) {
21
+ return new Promise((resolve) => {
22
+ const onClose = () => resolve(null);
23
+ readlineInterface.once('close', onClose);
24
+ readlineInterface.question(prompt, (answer) => {
25
+ readlineInterface.off('close', onClose);
26
+ resolve(sanitizeInput(answer));
27
+ });
28
+ });
29
+ }
30
+
31
+ async function confirm(prompt) {
32
+ const answer = await ask(`${prompt} ${color.dim}${CONFIRM_HINT}${color.reset} `);
33
+ return AFFIRMATIVE_PATTERN.test((answer ?? EMPTY).trim());
34
+ }
35
+
36
+ function close() {
37
+ readlineInterface.close();
38
+ }
39
+
40
+ module.exports = { ask, confirm, close };
@@ -0,0 +1,74 @@
1
+ const settings = require('../config/settings.json');
2
+ const { color } = require('../lib/colors');
3
+ const { message } = require('../lib/logger');
4
+
5
+ const SYMBOLS = settings.cli.symbols;
6
+ const COLORS = settings.cli.colors;
7
+ const MENU = settings.cli.menu;
8
+ const BOX_WIDTH = settings.cli.boxWidth;
9
+ const BOX_TITLE = settings.cli.title;
10
+ const BOX_TOP_LEFT = '\u256d';
11
+ const BOX_TOP_RIGHT = '\u256e';
12
+ const BOX_BOTTOM_LEFT = '\u2570';
13
+ const BOX_BOTTOM_RIGHT = '\u256f';
14
+ const BOX_HORIZONTAL = '\u2500';
15
+ const BOX_VERTICAL = '\u2502';
16
+ const NEW_LINE = '\n';
17
+ const SPACE = ' ';
18
+
19
+ function line(colorName, text) {
20
+ return `${color[colorName]}${text}${color.reset}`;
21
+ }
22
+
23
+ function promptLine(text, prefix = NEW_LINE) {
24
+ return `${prefix}${line(COLORS.prompt, SYMBOLS.prompt)} ${text}`;
25
+ }
26
+
27
+ function error(text) {
28
+ console.log(`${NEW_LINE}${line(COLORS.error, `${SYMBOLS.error} ${text}`)}`);
29
+ }
30
+
31
+ function warning(text) {
32
+ console.log(`${NEW_LINE}${line(COLORS.warning, `${SYMBOLS.warning} ${text}`)}`);
33
+ }
34
+
35
+ function notice(text) {
36
+ console.log(`${NEW_LINE}${line(COLORS.notice, text)}`);
37
+ }
38
+
39
+ function noticePrefix(text) {
40
+ return `${NEW_LINE}${line(COLORS.notice, text)}${NEW_LINE}`;
41
+ }
42
+
43
+ function centeredTitle() {
44
+ const padding = BOX_WIDTH - BOX_TITLE.length;
45
+ const left = SPACE.repeat(Math.ceil(padding / 2));
46
+ const right = SPACE.repeat(Math.floor(padding / 2));
47
+ return `${left}${BOX_TITLE}${right}`;
48
+ }
49
+
50
+ function renderMenu() {
51
+ const border = BOX_HORIZONTAL.repeat(BOX_WIDTH);
52
+
53
+ console.log(`${NEW_LINE}${color[COLORS.box]}${color.bold}${BOX_TOP_LEFT}${border}${BOX_TOP_RIGHT}`);
54
+ console.log(`${BOX_VERTICAL}${centeredTitle()}${BOX_VERTICAL}`);
55
+ console.log(`${BOX_BOTTOM_LEFT}${border}${BOX_BOTTOM_RIGHT}${color.reset}${NEW_LINE}`);
56
+
57
+ for (const item of MENU) {
58
+ console.log(` ${line(COLORS.menuKey, `${item.key}.`)} ${item.label}`);
59
+ }
60
+
61
+ console.log(`${NEW_LINE}${line(COLORS.notice, message('cli.exitHint'))}${NEW_LINE}`);
62
+ }
63
+
64
+ function renderMissingFiles(items) {
65
+ console.log(`${NEW_LINE}${line(COLORS.error, `${SYMBOLS.error} ${message('cli.missingFiles')}`)}`);
66
+
67
+ for (const item of items) {
68
+ console.log(` ${line(COLORS.error, SYMBOLS.bullet)} ${item.label}`);
69
+ }
70
+
71
+ console.log(`${NEW_LINE}${line(COLORS.notice, message('cli.incompleteProject'))}`);
72
+ }
73
+
74
+ module.exports = { promptLine, noticePrefix, error, warning, notice, renderMenu, renderMissingFiles };