nibula 1.2.2 → 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.
Files changed (50) hide show
  1. package/.eleventy.js +0 -5
  2. package/CHANGELOG.md +21 -9
  3. package/README.md +1 -1
  4. package/bin/create.js +1 -2
  5. package/bin/nibula.js +2 -5
  6. package/nginx.conf +75 -75
  7. package/package.json +74 -74
  8. package/src/backend/_core/index.js +267 -267
  9. package/src/backend/_core/init.js +52 -52
  10. package/src/backend/_core/modules/RateLimiter.js +58 -58
  11. package/src/backend/_core/modules/Response.js +59 -59
  12. package/src/backend/api/protected/example-protected.js +23 -23
  13. package/src/backend/api/public/example-public.js +24 -24
  14. package/src/backend/backend-node.service.example +30 -30
  15. package/src/backend/database/Database.js +46 -46
  16. package/src/backend/example.config.js +37 -37
  17. package/src/backend/package.json +18 -18
  18. package/src/frontend/.htaccess +51 -51
  19. package/src/frontend/assets/brand/favicon.svg +54 -54
  20. package/src/frontend/assets/brand/logo.svg +54 -54
  21. package/src/frontend/data/site.json +48 -48
  22. package/src/frontend/web.config +55 -55
  23. package/tools/assistant.js +122 -151
  24. package/tools/buildJs.js +39 -37
  25. package/tools/cleanOutput.js +23 -25
  26. package/tools/cli/prompt.js +40 -0
  27. package/tools/cli/ui.js +74 -0
  28. package/tools/config/messages.json +76 -0
  29. package/tools/config/settings.json +135 -0
  30. package/tools/lib/colors.js +17 -0
  31. package/tools/lib/files.js +65 -0
  32. package/tools/lib/logger.js +22 -0
  33. package/tools/lib/outputPath.js +141 -0
  34. package/tools/lib/pageActions.js +138 -0
  35. package/tools/lib/pageArtifacts.js +46 -0
  36. package/tools/lib/pageComponents.js +88 -0
  37. package/tools/lib/paths.js +61 -0
  38. package/tools/lib/project.js +54 -0
  39. package/tools/lib/siteData.js +93 -0
  40. package/tools/lib/text.js +47 -0
  41. package/tools/lib/validation.js +39 -0
  42. package/tools/res/templates/template.js +3 -1
  43. package/tools/res/templates/template.ts +3 -1
  44. package/tools/modules/constants.js +0 -66
  45. package/tools/modules/pageComponents.js +0 -77
  46. package/tools/modules/updateData.js +0 -90
  47. package/tools/modules/updateOutputPath.js +0 -112
  48. package/tools/modules/updatePage.js +0 -162
  49. package/tools/modules/utils.js +0 -27
  50. package/tools/modules/validation.js +0 -30
@@ -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 };
@@ -0,0 +1,76 @@
1
+ {
2
+ "project": {
3
+ "notInside": "Not inside a Nibula project."
4
+ },
5
+ "validation": {
6
+ "invalidName": "Invalid name.",
7
+ "invalidNameCharacters": "Page name can only contain lowercase letters, numbers, and hyphens.",
8
+ "nameStartsWithNumber": "Page name cannot start with a number.",
9
+ "protectedName": "\"{name}\" is a protected page name.",
10
+ "invalidPath": "Invalid path.",
11
+ "invalidPathCharacters": "Path contains invalid characters.",
12
+ "templatesLabel": "CLI page templates"
13
+ },
14
+ "page": {
15
+ "alreadyExists": "[skip] page \"{name}\" already exists",
16
+ "doesNotExist": "[skip] page \"{name}\" does not exist",
17
+ "targetExists": "[skip] target page \"{name}\" already exists",
18
+ "templateMissing": "[skip] template not found: {path}",
19
+ "fileMissing": "[skip] not found: {path}",
20
+ "fileCreated": "[created] {path}",
21
+ "fileRenamed": "[renamed] {source} \u2192 {destination}",
22
+ "fileDeleted": "[deleted] {path}",
23
+ "createFailed": "[error] could not create page files: {error}",
24
+ "renameFailed": "[error] could not rename page files: {error}",
25
+ "deleteFailed": "[error] {path}: {error}"
26
+ },
27
+ "components": {
28
+ "fileMissing": "[skip] not found: {path}",
29
+ "anchorMissing": "[skip] no {anchor} anchor in {file}",
30
+ "blockAdded": "[updated] page block added for \"{name}\"",
31
+ "blockMissing": "[skip] page block for \"{name}\" not found",
32
+ "blockRemoved": "[cleaned] page block removed for \"{name}\"",
33
+ "blockRenamed": "[renamed] page block \"{source}\" \u2192 \"{destination}\""
34
+ },
35
+ "siteData": {
36
+ "fileMissing": "[skip] not found: {path}",
37
+ "parseFailed": "[error] cannot parse {file}: {error}",
38
+ "recordExists": "[skip] record \"{name}\" already exists",
39
+ "recordMissing": "[skip] record \"{name}\" not found",
40
+ "recordAdded": "[updated] record \"{name}\" added",
41
+ "recordRemoved": "[cleaned] record \"{name}\" removed",
42
+ "recordRenamed": "[updated] record \"{source}\" renamed to \"{destination}\""
43
+ },
44
+ "output": {
45
+ "emptyPath": "[skip] empty output path",
46
+ "readFailed": "[error] cannot read {file}: {error}",
47
+ "updateFailed": "[error] could not update output path: {error}",
48
+ "updating": "\nupdating output path \u2192 \"{path}\"",
49
+ "variableMissing": "[skip] {variable} not found in {file}",
50
+ "keyMissing": "[skip] {key} not found in {file}",
51
+ "fileUpdated": "[updated] {file} \u2192 {path}",
52
+ "directoryDeleted": "[deleted] {path}",
53
+ "refuseDelete": "[skip] refusing to delete \"{path}\" (outside project)",
54
+ "missingConfiguration": "(!) {key} not found in {file}",
55
+ "cleaned": "(\u2713) cleaned \u2192 {path}",
56
+ "nothingToClean": "(i) nothing to clean \u2192 {path}"
57
+ },
58
+ "cli": {
59
+ "createPrompt": "Name of the new page: ",
60
+ "removePrompt": "Name of the page to remove: ",
61
+ "renameFromPrompt": "Page to rename: ",
62
+ "renameToPrompt": "New name: ",
63
+ "outputPathPrompt": "New output path: ",
64
+ "menuPrompt": "Choose an option: ",
65
+ "currentOutputPath": "Current path: \"{path}\"",
66
+ "confirmRemove": "This permanently deletes all files for \"{name}\".",
67
+ "cancelled": "Cancelled.",
68
+ "pageNotFound": "Page \"{name}\" does not exist.",
69
+ "sameName": "Old and new name are the same.",
70
+ "invalidOption": "Invalid option.",
71
+ "unexpectedError": "Unexpected error: {error}",
72
+ "missingFiles": "This project is missing required files:",
73
+ "incompleteProject": "The project may be incomplete or created with a different Nibula version.",
74
+ "exitHint": "CTRL + C to exit"
75
+ }
76
+ }