@acristhian1411/notecli 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/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@acristhian1411/notecli",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "bin": {
7
+ "notecli": "./src/cli.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "dev": "node ./src/cli.js",
14
+ "lint": "eslint . --ext .js",
15
+ "bundle": "esbuild ./src/cli.js --bundle --platform=node --target=node18 --outfile=dist/notecli.js",
16
+ "build": "pkg . --output notecli",
17
+ "build:win": "nexe ./src/cli.js -t windows-x64-22.16.0 -o dist/notecli-win.exe --build",
18
+ "build:linux": "nexe ./src/cli.js -t linux-x64-22.16.0 -o dist/notecli-linux --build",
19
+ "build:winp": "pkg dist/notecli.js --targets node18-win-x64 --output dist/notecli-win.exe --debug",
20
+ "build:linuxp": "pkg dist/notecli.js --targets node18-linux-x64 --output dist/notecli-linux --debug"
21
+ },
22
+ "pkg": {
23
+ "scripts": "src/**/*.js",
24
+ "targets": [
25
+ "node18-win-x64"
26
+ ]
27
+ },
28
+ "keywords": [],
29
+ "author": "",
30
+ "license": "ISC",
31
+ "packageManager": "pnpm@10.11.0",
32
+ "devDependencies": {
33
+ "esbuild": "^0.27.3",
34
+ "nexe": "5.0.0-beta.4",
35
+ "pkg": "^5.8.1"
36
+ },
37
+ "dependencies": {
38
+ "archiver": "^7.0.1",
39
+ "chalk": "^5.6.2",
40
+ "cli-highlight": "^2.1.11",
41
+ "cli-table3": "^0.6.5",
42
+ "commander": "^14.0.3",
43
+ "conf": "^15.1.0",
44
+ "inquirer": "^13.3.0",
45
+ "marked": "^17.0.3",
46
+ "marked-terminal": "^7.3.0"
47
+ }
48
+ }
package/src/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI principal de NoteApp.
5
+ * Se encarga únicamente de registrar comandos y delegar ejecución.
6
+ */
7
+
8
+ import { Command } from "commander";
9
+ import createCommand from "./commands/create.js";
10
+ import listCommand from "./commands/list.js";
11
+ import viewCommand from "./commands/view.js";
12
+ import editCommand from "./commands/edit.js";
13
+ import searchCommand from "./commands/search.js";
14
+ import exportCommand from "./commands/export.js";
15
+ // import statsCommand from "./commands/stats.js";
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name("notecli")
21
+ .description("CLI para gestión de notas Markdown")
22
+ .version("1.0.0");
23
+
24
+ createCommand(program);
25
+ listCommand(program);
26
+ viewCommand(program);
27
+ editCommand(program);
28
+ searchCommand(program);
29
+ exportCommand(program);
30
+ // statsCommand(program);
31
+
32
+ program.parse();
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Registra el comando `create`.
3
+ * @param {Command} program instancia principal de commander
4
+ */
5
+
6
+ import fs from "fs/promises";
7
+ import path from "path";
8
+ import os from "os";
9
+
10
+ export default function createCommand(program) {
11
+ program
12
+ .command("create <name>")
13
+ .description("Crea una nueva nota Markdown")
14
+ .action(async (name) => {
15
+ try {
16
+ const notesDir = path.join(os.homedir(), ".noteapp");
17
+
18
+ await fs.mkdir(notesDir, { recursive: true });
19
+
20
+ const filePath = path.join(notesDir, `${name}.md`);
21
+
22
+ await fs.writeFile(filePath, `# ${name}\n\n`);
23
+
24
+ console.log(`Nota creada: ${filePath}`);
25
+ } catch (error) {
26
+ console.error("Error creando nota:", error.message);
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Comando `edit`
3
+ * Abre una nota existente en el editor configurado.
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import { spawn } from "child_process";
8
+ import { getNotePath, noteExists } from "../core/notes.service.js";
9
+ import { getConfig } from "../core/config.service.js";
10
+
11
+ /**
12
+ * Abre el archivo en el editor configurado.
13
+ * @param {string} filePath Ruta completa del archivo
14
+ * @param {string} editor Nombre del editor
15
+ * @returns {Promise<void>}
16
+ */
17
+ function openInEditor(filePath, editor) {
18
+ return new Promise((resolve, reject) => {
19
+ const editorProcess = spawn(editor, [filePath], {
20
+ stdio: "inherit",
21
+ shell: true
22
+ });
23
+
24
+ editorProcess.on("exit", (code) => {
25
+ if (code === 0) {
26
+ resolve();
27
+ } else {
28
+ reject(new Error(`El editor terminó con código ${code}`));
29
+ }
30
+ });
31
+
32
+ editorProcess.on("error", (err) => {
33
+ reject(err);
34
+ });
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Registra el comando edit.
40
+ * @param {Command} program
41
+ */
42
+ export default function editCommand(program) {
43
+ program
44
+ .command("edit <name>")
45
+ .description("Abre una nota existente en el editor")
46
+ .option("-e, --editor <editor>", "Editor a utilizar (por defecto: configurado)")
47
+ .action(async (name, options) => {
48
+ try {
49
+ // Verificar que la nota existe
50
+ const exists = await noteExists(name);
51
+ if (!exists) {
52
+ console.error(
53
+ chalk.red(`La nota "${name}" no existe. Usa 'noteapp create ${name}' para crearla.`)
54
+ );
55
+ process.exit(1);
56
+ }
57
+
58
+ // Obtener el editor configurado o usar el de las opciones
59
+ const config = getConfig();
60
+ const editor = options.editor || config.editor || process.env.EDITOR || "nano";
61
+
62
+ const filePath = getNotePath(name);
63
+
64
+ console.log(chalk.blue(`Abriendo "${name}.md" en ${editor}...`));
65
+
66
+ await openInEditor(filePath, editor);
67
+
68
+ console.log(chalk.green(`✓ Nota "${name}" editada exitosamente.`));
69
+ } catch (error) {
70
+ console.error(
71
+ chalk.red(`Error al editar la nota "${name}":`),
72
+ error.message
73
+ );
74
+ process.exit(1);
75
+ }
76
+ });
77
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Comando `export`
3
+ * Menú interactivo para selección múltiple y exportación (MD/ZIP).
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import inquirer from "inquirer";
8
+ import fs from "fs/promises";
9
+ import fsSync from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import archiver from "archiver";
13
+ import { getAllNotes, getNotePath } from "../core/notes.service.js";
14
+
15
+ /**
16
+ * Detecta la carpeta de descargas del usuario.
17
+ * @returns {string}
18
+ */
19
+ function getDownloadsFolder() {
20
+ const homeDir = os.homedir();
21
+
22
+ // Intenta diferentes rutas según el sistema operativo
23
+ const possiblePaths = [
24
+ path.join(homeDir, "Downloads"),
25
+ path.join(homeDir, "Descargas"),
26
+ path.join(homeDir, "downloads"),
27
+ homeDir // Fallback al home si no encuentra Downloads
28
+ ];
29
+
30
+ for (const downloadPath of possiblePaths) {
31
+ try {
32
+ if (fsSync.existsSync(downloadPath)) {
33
+ return downloadPath;
34
+ }
35
+ } catch {
36
+ continue;
37
+ }
38
+ }
39
+
40
+ return homeDir;
41
+ }
42
+
43
+ /**
44
+ * Copia un archivo único al destino.
45
+ * @param {string} sourcePath Ruta del archivo origen
46
+ * @param {string} destPath Ruta del destino
47
+ */
48
+ async function copyFile(sourcePath, destPath) {
49
+ await fs.copyFile(sourcePath, destPath);
50
+ }
51
+
52
+ /**
53
+ * Crea un archivo ZIP con las notas seleccionadas.
54
+ * @param {Array<string>} notePaths Rutas de las notas a comprimir
55
+ * @param {string} outputPath Ruta del archivo ZIP de salida
56
+ * @returns {Promise<void>}
57
+ */
58
+ function createZip(notePaths, outputPath) {
59
+ return new Promise((resolve, reject) => {
60
+ const output = fsSync.createWriteStream(outputPath);
61
+ const archive = archiver("zip", {
62
+ zlib: { level: 9 } // Máxima compresión
63
+ });
64
+
65
+ output.on("close", () => {
66
+ resolve();
67
+ });
68
+
69
+ archive.on("error", (err) => {
70
+ reject(err);
71
+ });
72
+
73
+ archive.on("warning", (err) => {
74
+ if (err.code === "ENOENT") {
75
+ console.warn(chalk.yellow(`Advertencia: ${err.message}`));
76
+ } else {
77
+ reject(err);
78
+ }
79
+ });
80
+
81
+ archive.pipe(output);
82
+
83
+ // Agregar cada archivo al ZIP
84
+ notePaths.forEach((notePath) => {
85
+ const fileName = path.basename(notePath);
86
+ archive.file(notePath, { name: fileName });
87
+ });
88
+
89
+ archive.finalize();
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Formatea el tamaño de archivo en bytes a formato legible.
95
+ * @param {number} bytes
96
+ * @returns {string}
97
+ */
98
+ function formatBytes(bytes) {
99
+ if (bytes === 0) return "0 B";
100
+ const k = 1024;
101
+ const sizes = ["B", "KB", "MB", "GB"];
102
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
103
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
104
+ }
105
+
106
+ /**
107
+ * Registra el comando export.
108
+ * @param {Command} program
109
+ */
110
+ export default function exportCommand(program) {
111
+ program
112
+ .command("export")
113
+ .description("Exporta notas a la carpeta de Descargas")
114
+ .option("-o, --output <path>", "Carpeta de destino personalizada")
115
+ .action(async (options) => {
116
+ try {
117
+ const notes = await getAllNotes();
118
+
119
+ if (notes.length === 0) {
120
+ console.log(chalk.yellow("No hay notas disponibles para exportar."));
121
+ return;
122
+ }
123
+
124
+ // Menú interactivo para seleccionar notas
125
+ const answers = await inquirer.prompt([
126
+ {
127
+ type: "checkbox",
128
+ name: "selectedNotes",
129
+ message: "Selecciona las notas a exportar:",
130
+ choices: notes.map((note) => ({
131
+ name: `${note.name}.md ${chalk.gray(`(${formatBytes(note.size)})`)}`,
132
+ value: note.name,
133
+ checked: false
134
+ })),
135
+ validate: (answer) => {
136
+ if (answer.length === 0) {
137
+ return "Debes seleccionar al menos una nota.";
138
+ }
139
+ return true;
140
+ }
141
+ }
142
+ ]);
143
+
144
+ const selectedNotes = answers.selectedNotes;
145
+
146
+ if (selectedNotes.length === 0) {
147
+ console.log(chalk.yellow("No se seleccionó ninguna nota."));
148
+ return;
149
+ }
150
+
151
+ // Determinar carpeta de destino
152
+ const outputDir = options.output || getDownloadsFolder();
153
+
154
+ console.log(chalk.blue(`\nExportando ${selectedNotes.length} nota(s)...\n`));
155
+
156
+ if (selectedNotes.length === 1) {
157
+ // Un solo archivo: copia directa
158
+ const noteName = selectedNotes[0];
159
+ const sourcePath = getNotePath(noteName);
160
+ const destPath = path.join(outputDir, `${noteName}.md`);
161
+
162
+ await copyFile(sourcePath, destPath);
163
+
164
+ console.log(chalk.green(`✓ Nota exportada: ${destPath}`));
165
+ } else {
166
+ // Múltiples archivos: crear ZIP
167
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
168
+ const zipFileName = `notes_export_${timestamp}.zip`;
169
+ const zipPath = path.join(outputDir, zipFileName);
170
+
171
+ const notePaths = selectedNotes.map((name) => getNotePath(name));
172
+
173
+ console.log(chalk.blue("Comprimiendo archivos..."));
174
+
175
+ await createZip(notePaths, zipPath);
176
+
177
+ // Obtener tamaño del ZIP creado
178
+ const stats = await fs.stat(zipPath);
179
+
180
+ console.log(chalk.green(`\n✓ ZIP creado: ${zipPath}`));
181
+ console.log(chalk.gray(` Tamaño: ${formatBytes(stats.size)}`));
182
+ console.log(chalk.gray(` Notas incluidas: ${selectedNotes.length}`));
183
+ }
184
+
185
+ console.log(chalk.blue(`\n✓ Exportación completada exitosamente.`));
186
+ } catch (error) {
187
+ console.error(chalk.red("Error durante la exportación:"), error.message);
188
+ process.exit(1);
189
+ }
190
+ });
191
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Comando `list`
3
+ * Muestra una tabla con nombre, fecha de modificación y tamaño.
4
+ */
5
+
6
+ import Table from "cli-table3";
7
+ import chalk from "chalk";
8
+ import { getAllNotes } from "../core/notes.service.js";
9
+
10
+ /**
11
+ * Registra el comando list en commander.
12
+ * @param {Command} program
13
+ */
14
+ export default function listCommand(program) {
15
+ program
16
+ .command("list")
17
+ .description("Lista todas las notas disponibles")
18
+ .action(async () => {
19
+ try {
20
+ const notes = await getAllNotes();
21
+
22
+ if (!notes.length) {
23
+ console.log(chalk.yellow("No hay notas creadas aún."));
24
+ return;
25
+ }
26
+
27
+ const table = new Table({
28
+ head: [
29
+ chalk.cyan("Nombre"),
30
+ chalk.cyan("Última Modificación"),
31
+ chalk.cyan("Tamaño (bytes)")
32
+ ]
33
+ });
34
+
35
+ notes.forEach((note) => {
36
+ table.push([
37
+ note.name,
38
+ note.mtime.toLocaleString(),
39
+ note.size
40
+ ]);
41
+ });
42
+
43
+ console.log(table.toString());
44
+ } catch (error) {
45
+ console.error(chalk.red("Error listando notas:"), error.message);
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Comando `search`
3
+ * Realiza búsqueda de texto completo (full-text search) en todas las notas.
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import { getAllNotes, getNoteContent } from "../core/notes.service.js";
8
+
9
+ /**
10
+ * Obtiene el contexto alrededor de una coincidencia.
11
+ * @param {string} content Contenido completo
12
+ * @param {number} index Índice de la coincidencia
13
+ * @param {number} contextLength Cantidad de caracteres de contexto
14
+ * @returns {string}
15
+ */
16
+ function getContext(content, index, contextLength = 50) {
17
+ const start = Math.max(0, index - contextLength);
18
+ const end = Math.min(content.length, index + contextLength);
19
+
20
+ let context = content.slice(start, end);
21
+
22
+ if (start > 0) context = "..." + context;
23
+ if (end < content.length) context = context + "...";
24
+
25
+ return context;
26
+ }
27
+
28
+ /**
29
+ * Busca una query en el contenido y retorna las coincidencias con contexto.
30
+ * @param {string} content Contenido de la nota
31
+ * @param {string} query Texto a buscar
32
+ * @returns {Array<{line: number, context: string, index: number}>}
33
+ */
34
+ function findMatches(content, query) {
35
+ const matches = [];
36
+ const lowerContent = content.toLowerCase();
37
+ const lowerQuery = query.toLowerCase();
38
+
39
+ let index = lowerContent.indexOf(lowerQuery);
40
+
41
+ while (index !== -1) {
42
+ // Calcular número de línea
43
+ const beforeMatch = content.slice(0, index);
44
+ const line = beforeMatch.split("\n").length;
45
+
46
+ // Obtener contexto
47
+ const context = getContext(content, index, 60);
48
+
49
+ matches.push({ line, context, index });
50
+
51
+ // Buscar siguiente coincidencia
52
+ index = lowerContent.indexOf(lowerQuery, index + 1);
53
+ }
54
+
55
+ return matches;
56
+ }
57
+
58
+ /**
59
+ * Resalta la query en el texto.
60
+ * @param {string} text Texto donde resaltar
61
+ * @param {string} query Texto a resaltar
62
+ * @returns {string}
63
+ */
64
+ function highlightQuery(text, query) {
65
+ const regex = new RegExp(`(${query})`, "gi");
66
+ return text.replace(regex, chalk.bgYellow.black("$1"));
67
+ }
68
+
69
+ /**
70
+ * Registra el comando search.
71
+ * @param {Command} program
72
+ */
73
+ export default function searchCommand(program) {
74
+ program
75
+ .command("search <query>")
76
+ .description("Busca texto en todas las notas")
77
+ .option("-l, --limit <number>", "Limitar resultados por nota", "3")
78
+ .action(async (query, options) => {
79
+ try {
80
+ const notes = await getAllNotes();
81
+
82
+ if (notes.length === 0) {
83
+ console.log(chalk.yellow("No hay notas disponibles para buscar."));
84
+ return;
85
+ }
86
+
87
+ console.log(chalk.blue(`\nBuscando "${query}" en ${notes.length} nota(s)...\n`));
88
+
89
+ let totalMatches = 0;
90
+ const limit = parseInt(options.limit, 10);
91
+
92
+ for (const note of notes) {
93
+ try {
94
+ const content = await getNoteContent(note.name);
95
+ const matches = findMatches(content, query);
96
+
97
+ if (matches.length > 0) {
98
+ totalMatches += matches.length;
99
+
100
+ console.log(chalk.green.bold(`📄 ${note.name}.md`) + chalk.gray(` (${matches.length} coincidencia(s))`));
101
+
102
+ // Limitar resultados mostrados por nota
103
+ const displayMatches = matches.slice(0, limit);
104
+
105
+ displayMatches.forEach((match) => {
106
+ const highlighted = highlightQuery(match.context, query);
107
+ console.log(chalk.gray(` Línea ${match.line}: `) + highlighted);
108
+ });
109
+
110
+ if (matches.length > limit) {
111
+ console.log(chalk.gray(` ... y ${matches.length - limit} coincidencia(s) más`));
112
+ }
113
+
114
+ console.log(); // Línea en blanco
115
+ }
116
+ } catch (error) {
117
+ // Ignorar errores de lectura individual y continuar
118
+ console.error(chalk.red(` Error leyendo ${note.name}: ${error.message}`));
119
+ }
120
+ }
121
+
122
+ if (totalMatches === 0) {
123
+ console.log(chalk.yellow(`No se encontraron coincidencias para "${query}".`));
124
+ } else {
125
+ console.log(chalk.blue(`\n✓ Total: ${totalMatches} coincidencia(s) encontrada(s).`));
126
+ }
127
+ } catch (error) {
128
+ console.error(chalk.red("Error durante la búsqueda:"), error.message);
129
+ process.exit(1);
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Comando `view`
3
+ * Renderiza una nota Markdown en la terminal usando marked + marked-terminal.
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import { marked, Renderer} from "marked";
8
+ import TerminalRenderer, {markedTerminal} from "marked-terminal";
9
+ import { highlight } from "cli-highlight";
10
+ import { getNoteContent } from "../core/notes.service.js";
11
+ import { CommanderError } from "commander";
12
+
13
+ /**
14
+ * Configura marked para usar el renderer de terminal.
15
+ */
16
+ marked.use(markedTerminal());
17
+ marked.use({ renderer: {
18
+ code(code,lang){
19
+ const language = lang || "plaintext";
20
+ const highlighted = highlight(code, { language, ignoreIllegals: true });
21
+ return `\n${highlighted}\n`;
22
+ }
23
+ }
24
+ });
25
+
26
+
27
+ /**
28
+ * Registra el comando view.
29
+ * @param {Command} program
30
+ */
31
+ export default function viewCommand(program) {
32
+ program
33
+ .command("view <name>")
34
+ .description("Muestra el contenido renderizado de una nota")
35
+ .action(async (name) => {
36
+ try {
37
+ const content = await getNoteContent(name);
38
+
39
+ const rendered = marked.parse(content);
40
+
41
+
42
+ console.log(rendered);
43
+ } catch (error) {
44
+ console.error(
45
+ chalk.red(`No se pudo abrir la nota "${name}".`),
46
+ error.message
47
+ );
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Servicio de configuración persistente usando `conf`.
3
+ * Permite almacenar ruta personalizada de notas y editor.
4
+ */
5
+
6
+ import Conf from "conf";
7
+ import os from "os";
8
+ import path from "path";
9
+
10
+ const config = new Conf({
11
+ projectName: "noteapp",
12
+ defaults: {
13
+ notesDir: null,
14
+ editor: "code"
15
+ }
16
+ });
17
+
18
+ export function getConfig() {
19
+ return config.store;
20
+ }
21
+
22
+ export function setConfig(key, value) {
23
+ config.set(key, value);
24
+ }
25
+
26
+ export function resolveNotesDir() {
27
+ const customDir = config.get("notesDir");
28
+
29
+ if (customDir && typeof customDir === "string") {
30
+ return path.resolve(customDir);
31
+ }
32
+
33
+ return path.join(os.homedir(), ".noteapp");
34
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Servicio para gestión de notas en el sistema de archivos.
3
+ * Encapsula toda la lógica de lectura y metadata.
4
+ */
5
+
6
+ import fs from "fs/promises";
7
+ import path from "path";
8
+ import { resolveNotesDir } from "./config.service.js";
9
+
10
+ /**
11
+ * Obtiene todas las notas .md disponibles.
12
+ * @returns {Promise<Array<{ name: string, path: string, size: number, mtime: Date }>>}
13
+ */
14
+ export async function getAllNotes() {
15
+ const notesDir = resolveNotesDir();
16
+
17
+ await fs.mkdir(notesDir, { recursive: true });
18
+
19
+ const files = await fs.readdir(notesDir);
20
+
21
+ const mdFiles = files.filter((file) => file.endsWith(".md"));
22
+
23
+ const notes = await Promise.all(
24
+ mdFiles.map(async (file) => {
25
+ const filePath = path.join(notesDir, file);
26
+ const stats = await fs.stat(filePath);
27
+
28
+ return {
29
+ name: file.replace(".md", ""),
30
+ path: filePath,
31
+ size: stats.size,
32
+ mtime: stats.mtime
33
+ };
34
+ })
35
+ );
36
+
37
+ return notes;
38
+ }
39
+
40
+ /**
41
+ * Obtiene el contenido de una nota específica.
42
+ * @param {string} name Nombre sin extensión
43
+ * @returns {Promise<string>}
44
+ */
45
+ export async function getNoteContent(name) {
46
+ const notesDir = resolveNotesDir();
47
+ const filePath = path.join(notesDir, `${name}.md`);
48
+
49
+ return fs.readFile(filePath, "utf-8");
50
+ }
51
+
52
+ /**
53
+ * Obtiene la ruta completa de una nota.
54
+ * @param {string} name Nombre sin extensión
55
+ * @returns {string}
56
+ */
57
+ export function getNotePath(name) {
58
+ const notesDir = resolveNotesDir();
59
+ return path.join(notesDir, `${name}.md`);
60
+ }
61
+
62
+ /**
63
+ * Verifica si una nota existe.
64
+ * @param {string} name Nombre sin extensión
65
+ * @returns {Promise<boolean>}
66
+ */
67
+ export async function noteExists(name) {
68
+ try {
69
+ const filePath = getNotePath(name);
70
+ await fs.access(filePath);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }