elsabro 2.1.0 → 2.3.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/agents/elsabro-orchestrator.md +113 -0
- package/commands/elsabro/add-phase.md +17 -0
- package/commands/elsabro/add-todo.md +111 -53
- package/commands/elsabro/audit-milestone.md +19 -0
- package/commands/elsabro/check-todos.md +210 -31
- package/commands/elsabro/complete-milestone.md +20 -1
- package/commands/elsabro/debug.md +19 -0
- package/commands/elsabro/discuss-phase.md +18 -1
- package/commands/elsabro/execute.md +511 -58
- package/commands/elsabro/insert-phase.md +18 -1
- package/commands/elsabro/list-phase-assumptions.md +17 -0
- package/commands/elsabro/new-milestone.md +19 -0
- package/commands/elsabro/new.md +19 -0
- package/commands/elsabro/pause-work.md +19 -0
- package/commands/elsabro/plan-milestone-gaps.md +20 -1
- package/commands/elsabro/plan.md +264 -36
- package/commands/elsabro/progress.md +203 -79
- package/commands/elsabro/quick.md +19 -0
- package/commands/elsabro/remove-phase.md +17 -0
- package/commands/elsabro/research-phase.md +18 -1
- package/commands/elsabro/resume-work.md +19 -0
- package/commands/elsabro/start.md +399 -98
- package/commands/elsabro/verify-work.md +138 -5
- package/hooks/confirm-destructive.sh +145 -0
- package/hooks/hooks-config.json +81 -0
- package/hooks/lint-check.sh +238 -0
- package/hooks/post-edit-test.sh +189 -0
- package/package.json +3 -2
- package/references/SYSTEM_INDEX.md +241 -0
- package/references/command-flow.md +352 -0
- package/references/enforcement-rules.md +331 -0
- package/references/error-contracts-tests.md +1171 -0
- package/references/error-contracts.md +3102 -0
- package/references/error-handling-instructions.md +26 -12
- package/references/parallel-worktrees.md +293 -0
- package/references/state-sync.md +381 -0
- package/references/task-dispatcher.md +388 -0
- package/references/tasks-integration.md +380 -0
- package/scripts/setup-parallel-worktrees.sh +319 -0
- package/skills/api-microservice.md +765 -0
- package/skills/api-setup.md +76 -3
- package/skills/auth-setup.md +46 -6
- package/skills/chrome-extension.md +584 -0
- package/skills/cicd-setup.md +1206 -0
- package/skills/cli-tool.md +884 -0
- package/skills/database-setup.md +41 -5
- package/skills/desktop-app.md +1351 -0
- package/skills/expo-app.md +35 -2
- package/skills/full-stack-app.md +543 -0
- package/skills/memory-update.md +207 -0
- package/skills/mobile-app.md +813 -0
- package/skills/nextjs-app.md +33 -2
- package/skills/payments-setup.md +76 -1
- package/skills/review.md +331 -0
- package/skills/saas-starter.md +639 -0
- package/skills/sentry-setup.md +41 -7
- package/skills/techdebt.md +289 -0
- package/skills/testing-setup.md +1218 -0
- package/skills/tutor.md +219 -0
- package/templates/.planning/notes/.gitkeep +0 -0
- package/templates/CLAUDE.md.template +48 -0
- package/templates/error-handling-config.json +79 -2
- package/templates/mistakes.md.template +52 -0
- package/templates/patterns.md.template +114 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-tool
|
|
3
|
+
description: Crear herramientas CLI profesionales con Node.js, Commander.js, publicacion en npm y testing
|
|
4
|
+
tags: [cli, command-line, npm, node, terminal, automation]
|
|
5
|
+
difficulty: intermediate
|
|
6
|
+
estimated_time: 25min
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: CLI Tool
|
|
10
|
+
|
|
11
|
+
<when_to_use>
|
|
12
|
+
Usar cuando el usuario menciona:
|
|
13
|
+
- "crear CLI"
|
|
14
|
+
- "herramienta de terminal"
|
|
15
|
+
- "comando npm"
|
|
16
|
+
- "script interactivo"
|
|
17
|
+
- "utilidad de linea de comandos"
|
|
18
|
+
- "tool para consola"
|
|
19
|
+
- "automatizar con scripts"
|
|
20
|
+
</when_to_use>
|
|
21
|
+
|
|
22
|
+
<pre_requisites>
|
|
23
|
+
## Pre-requisitos
|
|
24
|
+
- Node.js 20+
|
|
25
|
+
- Cuenta npm (para publicar)
|
|
26
|
+
- Conocimiento basico de JavaScript/TypeScript
|
|
27
|
+
</pre_requisites>
|
|
28
|
+
|
|
29
|
+
<tech_stack>
|
|
30
|
+
## Stack Tecnologico
|
|
31
|
+
| Categoria | Tecnologia | Version |
|
|
32
|
+
|-----------|------------|---------|
|
|
33
|
+
| Runtime | Node.js | 20.x |
|
|
34
|
+
| CLI Framework | Commander.js | 12.x |
|
|
35
|
+
| Colors | Chalk | 5.x |
|
|
36
|
+
| Prompts | Inquirer | 9.x |
|
|
37
|
+
| Testing | Vitest | 2.x |
|
|
38
|
+
| Build | tsup | 8.x |
|
|
39
|
+
| Linting | ESLint | 9.x |
|
|
40
|
+
</tech_stack>
|
|
41
|
+
|
|
42
|
+
<project_structure>
|
|
43
|
+
## Estructura de Proyecto
|
|
44
|
+
```
|
|
45
|
+
my-cli/
|
|
46
|
+
├── src/
|
|
47
|
+
│ ├── index.ts # Entry point
|
|
48
|
+
│ ├── commands/
|
|
49
|
+
│ │ ├── index.ts # Export all commands
|
|
50
|
+
│ │ ├── init.ts # Init command
|
|
51
|
+
│ │ └── run.ts # Run command
|
|
52
|
+
│ ├── lib/
|
|
53
|
+
│ │ ├── config.ts # Configuration loader
|
|
54
|
+
│ │ ├── logger.ts # Styled console output
|
|
55
|
+
│ │ └── utils.ts # Helper functions
|
|
56
|
+
│ └── types.ts # TypeScript types
|
|
57
|
+
├── tests/
|
|
58
|
+
│ ├── commands.test.ts # Command tests
|
|
59
|
+
│ └── utils.test.ts # Utility tests
|
|
60
|
+
├── bin/
|
|
61
|
+
│ └── cli.js # Shebang wrapper
|
|
62
|
+
├── package.json
|
|
63
|
+
├── tsconfig.json
|
|
64
|
+
├── tsup.config.ts
|
|
65
|
+
├── vitest.config.ts
|
|
66
|
+
└── README.md
|
|
67
|
+
```
|
|
68
|
+
</project_structure>
|
|
69
|
+
|
|
70
|
+
<setup_steps>
|
|
71
|
+
## Pasos de Setup
|
|
72
|
+
|
|
73
|
+
### 1. Inicializar Proyecto
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
mkdir my-cli && cd my-cli
|
|
77
|
+
npm init -y
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Instalar Dependencias
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Dependencias de produccion
|
|
84
|
+
npm install commander@12 chalk@5 inquirer@9
|
|
85
|
+
|
|
86
|
+
# Dependencias de desarrollo
|
|
87
|
+
npm install -D typescript @types/node @types/inquirer tsup vitest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Configurar TypeScript
|
|
91
|
+
|
|
92
|
+
**tsconfig.json:**
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"compilerOptions": {
|
|
96
|
+
"target": "ES2022",
|
|
97
|
+
"module": "ESNext",
|
|
98
|
+
"moduleResolution": "bundler",
|
|
99
|
+
"lib": ["ES2022"],
|
|
100
|
+
"outDir": "./dist",
|
|
101
|
+
"rootDir": "./src",
|
|
102
|
+
"strict": true,
|
|
103
|
+
"esModuleInterop": true,
|
|
104
|
+
"skipLibCheck": true,
|
|
105
|
+
"forceConsistentCasingInFileNames": true,
|
|
106
|
+
"declaration": true,
|
|
107
|
+
"declarationMap": true,
|
|
108
|
+
"sourceMap": true
|
|
109
|
+
},
|
|
110
|
+
"include": ["src/**/*"],
|
|
111
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Configurar tsup
|
|
116
|
+
|
|
117
|
+
**tsup.config.ts:**
|
|
118
|
+
```typescript
|
|
119
|
+
import { defineConfig } from 'tsup';
|
|
120
|
+
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
entry: ['src/index.ts'],
|
|
123
|
+
format: ['esm'],
|
|
124
|
+
dts: true,
|
|
125
|
+
clean: true,
|
|
126
|
+
sourcemap: true,
|
|
127
|
+
target: 'node20',
|
|
128
|
+
outDir: 'dist',
|
|
129
|
+
shims: true,
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 5. Configurar Vitest
|
|
134
|
+
|
|
135
|
+
**vitest.config.ts:**
|
|
136
|
+
```typescript
|
|
137
|
+
import { defineConfig } from 'vitest/config';
|
|
138
|
+
|
|
139
|
+
export default defineConfig({
|
|
140
|
+
test: {
|
|
141
|
+
globals: true,
|
|
142
|
+
environment: 'node',
|
|
143
|
+
include: ['tests/**/*.test.ts'],
|
|
144
|
+
coverage: {
|
|
145
|
+
provider: 'v8',
|
|
146
|
+
reporter: ['text', 'json', 'html'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 6. Crear Entry Point
|
|
153
|
+
|
|
154
|
+
**src/index.ts:**
|
|
155
|
+
```typescript
|
|
156
|
+
#!/usr/bin/env node
|
|
157
|
+
import { Command } from 'commander';
|
|
158
|
+
import chalk from 'chalk';
|
|
159
|
+
import { initCommand } from './commands/init.js';
|
|
160
|
+
import { runCommand } from './commands/run.js';
|
|
161
|
+
|
|
162
|
+
const program = new Command();
|
|
163
|
+
|
|
164
|
+
program
|
|
165
|
+
.name('my-cli')
|
|
166
|
+
.description('A professional CLI tool')
|
|
167
|
+
.version('1.0.0');
|
|
168
|
+
|
|
169
|
+
// Register commands
|
|
170
|
+
program.addCommand(initCommand);
|
|
171
|
+
program.addCommand(runCommand);
|
|
172
|
+
|
|
173
|
+
// Error handling
|
|
174
|
+
program.exitOverride();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await program.parseAsync(process.argv);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
console.error(chalk.red('Error:'), error.message);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 7. Crear Comandos
|
|
187
|
+
|
|
188
|
+
**src/commands/init.ts:**
|
|
189
|
+
```typescript
|
|
190
|
+
import { Command } from 'commander';
|
|
191
|
+
import chalk from 'chalk';
|
|
192
|
+
import inquirer from 'inquirer';
|
|
193
|
+
import { logger } from '../lib/logger.js';
|
|
194
|
+
|
|
195
|
+
interface InitOptions {
|
|
196
|
+
name?: string;
|
|
197
|
+
template?: string;
|
|
198
|
+
force?: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const initCommand = new Command('init')
|
|
202
|
+
.description('Initialize a new project')
|
|
203
|
+
.option('-n, --name <name>', 'Project name')
|
|
204
|
+
.option('-t, --template <template>', 'Template to use', 'default')
|
|
205
|
+
.option('-f, --force', 'Overwrite existing files', false)
|
|
206
|
+
.action(async (options: InitOptions) => {
|
|
207
|
+
logger.info('Initializing project...');
|
|
208
|
+
|
|
209
|
+
// Interactive prompts if name not provided
|
|
210
|
+
let projectName = options.name;
|
|
211
|
+
if (!projectName) {
|
|
212
|
+
const answers = await inquirer.prompt([
|
|
213
|
+
{
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'name',
|
|
216
|
+
message: 'Project name:',
|
|
217
|
+
default: 'my-project',
|
|
218
|
+
validate: (input: string) => {
|
|
219
|
+
if (/^[a-z0-9-]+$/.test(input)) return true;
|
|
220
|
+
return 'Name must be lowercase with hyphens only';
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
projectName = answers.name;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Template selection
|
|
228
|
+
const { confirmTemplate } = await inquirer.prompt([
|
|
229
|
+
{
|
|
230
|
+
type: 'confirm',
|
|
231
|
+
name: 'confirmTemplate',
|
|
232
|
+
message: `Use template "${options.template}"?`,
|
|
233
|
+
default: true,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
if (!confirmTemplate) {
|
|
238
|
+
const { template } = await inquirer.prompt([
|
|
239
|
+
{
|
|
240
|
+
type: 'list',
|
|
241
|
+
name: 'template',
|
|
242
|
+
message: 'Select template:',
|
|
243
|
+
choices: ['default', 'minimal', 'full'],
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
options.template = template;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
logger.success(`Project "${projectName}" initialized with ${options.template} template`);
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**src/commands/run.ts:**
|
|
254
|
+
```typescript
|
|
255
|
+
import { Command } from 'commander';
|
|
256
|
+
import { logger } from '../lib/logger.js';
|
|
257
|
+
|
|
258
|
+
interface RunOptions {
|
|
259
|
+
watch?: boolean;
|
|
260
|
+
verbose?: boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const runCommand = new Command('run')
|
|
264
|
+
.description('Run the project')
|
|
265
|
+
.argument('[script]', 'Script to run', 'start')
|
|
266
|
+
.option('-w, --watch', 'Watch mode', false)
|
|
267
|
+
.option('-v, --verbose', 'Verbose output', false)
|
|
268
|
+
.action(async (script: string, options: RunOptions) => {
|
|
269
|
+
logger.info(`Running script: ${script}`);
|
|
270
|
+
|
|
271
|
+
if (options.verbose) {
|
|
272
|
+
logger.debug('Options:', JSON.stringify(options, null, 2));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (options.watch) {
|
|
276
|
+
logger.info('Watch mode enabled');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Simulate running
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
281
|
+
logger.success('Script completed successfully');
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**src/commands/index.ts:**
|
|
286
|
+
```typescript
|
|
287
|
+
export { initCommand } from './init.js';
|
|
288
|
+
export { runCommand } from './run.js';
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 8. Crear Utilities
|
|
292
|
+
|
|
293
|
+
**src/lib/logger.ts:**
|
|
294
|
+
```typescript
|
|
295
|
+
import chalk from 'chalk';
|
|
296
|
+
|
|
297
|
+
export const logger = {
|
|
298
|
+
info: (message: string, ...args: unknown[]) => {
|
|
299
|
+
console.log(chalk.blue('info'), message, ...args);
|
|
300
|
+
},
|
|
301
|
+
success: (message: string, ...args: unknown[]) => {
|
|
302
|
+
console.log(chalk.green('success'), message, ...args);
|
|
303
|
+
},
|
|
304
|
+
warn: (message: string, ...args: unknown[]) => {
|
|
305
|
+
console.log(chalk.yellow('warn'), message, ...args);
|
|
306
|
+
},
|
|
307
|
+
error: (message: string, ...args: unknown[]) => {
|
|
308
|
+
console.error(chalk.red('error'), message, ...args);
|
|
309
|
+
},
|
|
310
|
+
debug: (message: string, ...args: unknown[]) => {
|
|
311
|
+
if (process.env.DEBUG) {
|
|
312
|
+
console.log(chalk.gray('debug'), message, ...args);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**src/lib/config.ts:**
|
|
319
|
+
```typescript
|
|
320
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
321
|
+
import { join } from 'node:path';
|
|
322
|
+
|
|
323
|
+
export interface Config {
|
|
324
|
+
name: string;
|
|
325
|
+
version: string;
|
|
326
|
+
settings: Record<string, unknown>;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const CONFIG_FILE = '.myclirc.json';
|
|
330
|
+
|
|
331
|
+
export function loadConfig(cwd: string = process.cwd()): Config | null {
|
|
332
|
+
const configPath = join(cwd, CONFIG_FILE);
|
|
333
|
+
|
|
334
|
+
if (!existsSync(configPath)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
340
|
+
return JSON.parse(content) as Config;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function getDefaultConfig(): Config {
|
|
347
|
+
return {
|
|
348
|
+
name: 'my-project',
|
|
349
|
+
version: '1.0.0',
|
|
350
|
+
settings: {},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**src/lib/utils.ts:**
|
|
356
|
+
```typescript
|
|
357
|
+
export function isGitRepo(): boolean {
|
|
358
|
+
try {
|
|
359
|
+
const { spawnSync } = require('node:child_process');
|
|
360
|
+
const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' });
|
|
361
|
+
return result.status === 0;
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function slugify(text: string): string {
|
|
368
|
+
return text
|
|
369
|
+
.toLowerCase()
|
|
370
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
371
|
+
.replace(/^-|-$/g, '');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function sleep(ms: number): Promise<void> {
|
|
375
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function formatBytes(bytes: number): string {
|
|
379
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
380
|
+
let unitIndex = 0;
|
|
381
|
+
let value = bytes;
|
|
382
|
+
|
|
383
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
384
|
+
value /= 1024;
|
|
385
|
+
unitIndex++;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return `${value.toFixed(2)} ${units[unitIndex]}`;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**src/types.ts:**
|
|
393
|
+
```typescript
|
|
394
|
+
export interface CliContext {
|
|
395
|
+
cwd: string;
|
|
396
|
+
verbose: boolean;
|
|
397
|
+
config: import('./lib/config.js').Config | null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface CommandResult {
|
|
401
|
+
success: boolean;
|
|
402
|
+
message?: string;
|
|
403
|
+
data?: unknown;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 9. Crear Bin Wrapper
|
|
408
|
+
|
|
409
|
+
**bin/cli.js:**
|
|
410
|
+
```javascript
|
|
411
|
+
#!/usr/bin/env node
|
|
412
|
+
import '../dist/index.js';
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 10. Configurar package.json
|
|
416
|
+
|
|
417
|
+
**package.json:**
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"name": "my-cli",
|
|
421
|
+
"version": "1.0.0",
|
|
422
|
+
"description": "A professional CLI tool",
|
|
423
|
+
"type": "module",
|
|
424
|
+
"bin": {
|
|
425
|
+
"my-cli": "./bin/cli.js"
|
|
426
|
+
},
|
|
427
|
+
"main": "./dist/index.js",
|
|
428
|
+
"types": "./dist/index.d.ts",
|
|
429
|
+
"files": [
|
|
430
|
+
"dist",
|
|
431
|
+
"bin"
|
|
432
|
+
],
|
|
433
|
+
"scripts": {
|
|
434
|
+
"dev": "tsup --watch",
|
|
435
|
+
"build": "tsup",
|
|
436
|
+
"test": "vitest run",
|
|
437
|
+
"test:watch": "vitest",
|
|
438
|
+
"test:coverage": "vitest run --coverage",
|
|
439
|
+
"lint": "eslint src",
|
|
440
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
441
|
+
},
|
|
442
|
+
"keywords": ["cli", "tool", "command-line"],
|
|
443
|
+
"author": "Your Name",
|
|
444
|
+
"license": "MIT",
|
|
445
|
+
"engines": {
|
|
446
|
+
"node": ">=20.0.0"
|
|
447
|
+
},
|
|
448
|
+
"repository": {
|
|
449
|
+
"type": "git",
|
|
450
|
+
"url": "https://github.com/username/my-cli"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 11. Crear Tests
|
|
456
|
+
|
|
457
|
+
**tests/commands.test.ts:**
|
|
458
|
+
```typescript
|
|
459
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
460
|
+
import { Command } from 'commander';
|
|
461
|
+
|
|
462
|
+
describe('CLI Commands', () => {
|
|
463
|
+
beforeEach(() => {
|
|
464
|
+
vi.clearAllMocks();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe('init command', () => {
|
|
468
|
+
it('should accept name option', async () => {
|
|
469
|
+
const program = new Command();
|
|
470
|
+
let capturedOptions: Record<string, unknown> = {};
|
|
471
|
+
|
|
472
|
+
program
|
|
473
|
+
.command('init')
|
|
474
|
+
.option('-n, --name <name>', 'Project name')
|
|
475
|
+
.action((options) => {
|
|
476
|
+
capturedOptions = options;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
await program.parseAsync(['node', 'cli', 'init', '-n', 'test-project']);
|
|
480
|
+
expect(capturedOptions.name).toBe('test-project');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should use default template', async () => {
|
|
484
|
+
const program = new Command();
|
|
485
|
+
let capturedOptions: Record<string, unknown> = {};
|
|
486
|
+
|
|
487
|
+
program
|
|
488
|
+
.command('init')
|
|
489
|
+
.option('-t, --template <template>', 'Template', 'default')
|
|
490
|
+
.action((options) => {
|
|
491
|
+
capturedOptions = options;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await program.parseAsync(['node', 'cli', 'init']);
|
|
495
|
+
expect(capturedOptions.template).toBe('default');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('run command', () => {
|
|
500
|
+
it('should accept script argument', async () => {
|
|
501
|
+
const program = new Command();
|
|
502
|
+
let capturedScript = '';
|
|
503
|
+
|
|
504
|
+
program
|
|
505
|
+
.command('run')
|
|
506
|
+
.argument('[script]', 'Script to run', 'start')
|
|
507
|
+
.action((script) => {
|
|
508
|
+
capturedScript = script;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await program.parseAsync(['node', 'cli', 'run', 'build']);
|
|
512
|
+
expect(capturedScript).toBe('build');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should use default script when not provided', async () => {
|
|
516
|
+
const program = new Command();
|
|
517
|
+
let capturedScript = '';
|
|
518
|
+
|
|
519
|
+
program
|
|
520
|
+
.command('run')
|
|
521
|
+
.argument('[script]', 'Script to run', 'start')
|
|
522
|
+
.action((script) => {
|
|
523
|
+
capturedScript = script;
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await program.parseAsync(['node', 'cli', 'run']);
|
|
527
|
+
expect(capturedScript).toBe('start');
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**tests/utils.test.ts:**
|
|
534
|
+
```typescript
|
|
535
|
+
import { describe, it, expect } from 'vitest';
|
|
536
|
+
import { slugify, formatBytes } from '../src/lib/utils.js';
|
|
537
|
+
|
|
538
|
+
describe('Utils', () => {
|
|
539
|
+
describe('slugify', () => {
|
|
540
|
+
it('should convert to lowercase', () => {
|
|
541
|
+
expect(slugify('Hello World')).toBe('hello-world');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should replace spaces with hyphens', () => {
|
|
545
|
+
expect(slugify('my project name')).toBe('my-project-name');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should remove special characters', () => {
|
|
549
|
+
expect(slugify('hello@world!')).toBe('hello-world');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should remove leading and trailing hyphens', () => {
|
|
553
|
+
expect(slugify('--hello--')).toBe('hello');
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('formatBytes', () => {
|
|
558
|
+
it('should format bytes', () => {
|
|
559
|
+
expect(formatBytes(500)).toBe('500.00 B');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should format kilobytes', () => {
|
|
563
|
+
expect(formatBytes(1024)).toBe('1.00 KB');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should format megabytes', () => {
|
|
567
|
+
expect(formatBytes(1048576)).toBe('1.00 MB');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should format gigabytes', () => {
|
|
571
|
+
expect(formatBytes(1073741824)).toBe('1.00 GB');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
</setup_steps>
|
|
577
|
+
|
|
578
|
+
<verification>
|
|
579
|
+
## Verificacion
|
|
580
|
+
|
|
581
|
+
### Probar Localmente
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
# Build del proyecto
|
|
585
|
+
npm run build
|
|
586
|
+
|
|
587
|
+
# Link global para testing
|
|
588
|
+
npm link
|
|
589
|
+
|
|
590
|
+
# Probar comandos
|
|
591
|
+
my-cli --help
|
|
592
|
+
my-cli init --help
|
|
593
|
+
my-cli init -n test-project
|
|
594
|
+
my-cli run build --verbose
|
|
595
|
+
|
|
596
|
+
# Desinstalar link
|
|
597
|
+
npm unlink -g my-cli
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Ejecutar Tests
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
# Tests una vez
|
|
604
|
+
npm run test
|
|
605
|
+
|
|
606
|
+
# Tests en modo watch
|
|
607
|
+
npm run test:watch
|
|
608
|
+
|
|
609
|
+
# Tests con coverage
|
|
610
|
+
npm run test:coverage
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Verificar Build
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
# Verificar que compila
|
|
617
|
+
npm run build
|
|
618
|
+
|
|
619
|
+
# Verificar estructura de dist
|
|
620
|
+
ls -la dist/
|
|
621
|
+
|
|
622
|
+
# Verificar dry-run de publicacion
|
|
623
|
+
npm publish --dry-run
|
|
624
|
+
```
|
|
625
|
+
</verification>
|
|
626
|
+
|
|
627
|
+
<publishing>
|
|
628
|
+
## Publicar en npm
|
|
629
|
+
|
|
630
|
+
### Preparacion
|
|
631
|
+
|
|
632
|
+
1. **Verificar nombre disponible:**
|
|
633
|
+
```bash
|
|
634
|
+
npm search my-cli
|
|
635
|
+
# O visitar https://www.npmjs.com/package/my-cli
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
2. **Login en npm:**
|
|
639
|
+
```bash
|
|
640
|
+
npm login
|
|
641
|
+
# Seguir prompts de autenticacion
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
3. **Verificar configuracion:**
|
|
645
|
+
```bash
|
|
646
|
+
npm whoami
|
|
647
|
+
npm config list
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Publicacion
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
# Primera publicacion
|
|
654
|
+
npm publish
|
|
655
|
+
|
|
656
|
+
# Si es scoped package (@usuario/my-cli)
|
|
657
|
+
npm publish --access public
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Versionado
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
# Patch: 1.0.0 -> 1.0.1 (bug fixes)
|
|
664
|
+
npm version patch
|
|
665
|
+
|
|
666
|
+
# Minor: 1.0.0 -> 1.1.0 (new features)
|
|
667
|
+
npm version minor
|
|
668
|
+
|
|
669
|
+
# Major: 1.0.0 -> 2.0.0 (breaking changes)
|
|
670
|
+
npm version major
|
|
671
|
+
|
|
672
|
+
# Publicar nueva version
|
|
673
|
+
npm publish
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Tags
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
# Publicar como beta
|
|
680
|
+
npm publish --tag beta
|
|
681
|
+
|
|
682
|
+
# Publicar como next
|
|
683
|
+
npm publish --tag next
|
|
684
|
+
|
|
685
|
+
# Instalar version especifica
|
|
686
|
+
npm install my-cli@beta
|
|
687
|
+
```
|
|
688
|
+
</publishing>
|
|
689
|
+
|
|
690
|
+
<common_issues>
|
|
691
|
+
## Problemas Comunes
|
|
692
|
+
|
|
693
|
+
### Shebang no funciona
|
|
694
|
+
|
|
695
|
+
**Problema:** Error "command not found" o permisos denegados.
|
|
696
|
+
|
|
697
|
+
**Solucion:**
|
|
698
|
+
```bash
|
|
699
|
+
# Verificar shebang en bin/cli.js
|
|
700
|
+
# Debe ser: #!/usr/bin/env node
|
|
701
|
+
|
|
702
|
+
# Dar permisos de ejecucion
|
|
703
|
+
chmod +x bin/cli.js
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### ESM vs CommonJS
|
|
707
|
+
|
|
708
|
+
**Problema:** "Cannot use import statement outside a module"
|
|
709
|
+
|
|
710
|
+
**Solucion:**
|
|
711
|
+
```json
|
|
712
|
+
// package.json
|
|
713
|
+
{
|
|
714
|
+
"type": "module"
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Chalk no muestra colores
|
|
719
|
+
|
|
720
|
+
**Problema:** Output sin colores en terminal.
|
|
721
|
+
|
|
722
|
+
**Causas:**
|
|
723
|
+
- Variable `NO_COLOR` activa
|
|
724
|
+
- Terminal no soporta colores
|
|
725
|
+
- Stdout no es TTY
|
|
726
|
+
|
|
727
|
+
**Solucion:**
|
|
728
|
+
```typescript
|
|
729
|
+
import chalk from 'chalk';
|
|
730
|
+
|
|
731
|
+
// Forzar colores (no recomendado generalmente)
|
|
732
|
+
const forceChalk = new chalk.Instance({ level: 3 });
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Inquirer no funciona en CI
|
|
736
|
+
|
|
737
|
+
**Problema:** Prompts cuelgan en CI/CD.
|
|
738
|
+
|
|
739
|
+
**Solucion:**
|
|
740
|
+
```typescript
|
|
741
|
+
// Detectar CI y usar defaults
|
|
742
|
+
const isCI = process.env.CI === 'true';
|
|
743
|
+
|
|
744
|
+
if (isCI) {
|
|
745
|
+
// Usar valores por defecto sin prompts
|
|
746
|
+
return defaultOptions;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Prompts interactivos
|
|
750
|
+
const answers = await inquirer.prompt([...]);
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### TypeScript paths no resuelven
|
|
754
|
+
|
|
755
|
+
**Problema:** Imports con `@/` no funcionan.
|
|
756
|
+
|
|
757
|
+
**Solucion:**
|
|
758
|
+
```typescript
|
|
759
|
+
// tsup.config.ts
|
|
760
|
+
import { defineConfig } from 'tsup';
|
|
761
|
+
|
|
762
|
+
export default defineConfig({
|
|
763
|
+
// ... otras opciones
|
|
764
|
+
esbuildOptions(options) {
|
|
765
|
+
options.alias = {
|
|
766
|
+
'@': './src',
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Build falla con dependencias nativas
|
|
773
|
+
|
|
774
|
+
**Problema:** Modulos nativos causan error en build.
|
|
775
|
+
|
|
776
|
+
**Solucion:**
|
|
777
|
+
```typescript
|
|
778
|
+
// tsup.config.ts
|
|
779
|
+
export default defineConfig({
|
|
780
|
+
// Excluir modulos nativos del bundle
|
|
781
|
+
external: ['fsevents'],
|
|
782
|
+
noExternal: [], // Incluir todo lo demas
|
|
783
|
+
});
|
|
784
|
+
```
|
|
785
|
+
</common_issues>
|
|
786
|
+
|
|
787
|
+
<best_practices>
|
|
788
|
+
## Mejores Practicas
|
|
789
|
+
|
|
790
|
+
### Estructura de Comandos
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
// Patron recomendado para comandos complejos
|
|
794
|
+
export function createCommand(): Command {
|
|
795
|
+
return new Command('name')
|
|
796
|
+
.description('...')
|
|
797
|
+
.option('...')
|
|
798
|
+
.action(async (options) => {
|
|
799
|
+
try {
|
|
800
|
+
await executeCommand(options);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
handleError(error);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Manejo de Errores
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
// Errores con codigos de salida apropiados
|
|
813
|
+
process.exit(0); // Exito
|
|
814
|
+
process.exit(1); // Error general
|
|
815
|
+
process.exit(2); // Mal uso del comando
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Configuracion Progresiva
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
// Prioridad: CLI args > env vars > config file > defaults
|
|
822
|
+
function getConfig(cliOptions: Options): Config {
|
|
823
|
+
const fileConfig = loadConfigFile();
|
|
824
|
+
const envConfig = loadEnvConfig();
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
...getDefaults(),
|
|
828
|
+
...fileConfig,
|
|
829
|
+
...envConfig,
|
|
830
|
+
...cliOptions,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Output Amigable
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
// Usar spinners para operaciones largas
|
|
839
|
+
import ora from 'ora';
|
|
840
|
+
|
|
841
|
+
const spinner = ora('Loading...').start();
|
|
842
|
+
await longOperation();
|
|
843
|
+
spinner.succeed('Done!');
|
|
844
|
+
```
|
|
845
|
+
</best_practices>
|
|
846
|
+
|
|
847
|
+
<examples>
|
|
848
|
+
## Ejemplos de Uso
|
|
849
|
+
|
|
850
|
+
### CLI Basico
|
|
851
|
+
|
|
852
|
+
```bash
|
|
853
|
+
# Crear proyecto
|
|
854
|
+
my-cli init -n my-app
|
|
855
|
+
|
|
856
|
+
# Ejecutar script
|
|
857
|
+
my-cli run dev --watch
|
|
858
|
+
|
|
859
|
+
# Ver ayuda
|
|
860
|
+
my-cli --help
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### CLI con Config
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
# Generar configuracion
|
|
867
|
+
my-cli config init
|
|
868
|
+
|
|
869
|
+
# Mostrar configuracion actual
|
|
870
|
+
my-cli config show
|
|
871
|
+
|
|
872
|
+
# Modificar valor
|
|
873
|
+
my-cli config set key value
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### CLI con Subcomandos
|
|
877
|
+
|
|
878
|
+
```bash
|
|
879
|
+
# Comandos anidados
|
|
880
|
+
my-cli project create
|
|
881
|
+
my-cli project list
|
|
882
|
+
my-cli project delete <id>
|
|
883
|
+
```
|
|
884
|
+
</examples>
|