@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 +48 -0
- package/src/cli.js +32 -0
- package/src/commands/create.js +29 -0
- package/src/commands/edit.js +77 -0
- package/src/commands/export.js +191 -0
- package/src/commands/list.js +48 -0
- package/src/commands/search.js +132 -0
- package/src/commands/view.js +50 -0
- package/src/core/config.service.js +34 -0
- package/src/core/notes.service.js +75 -0
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
|
+
}
|