@icarusmx/creta 1.5.12 → 1.5.14
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/bin/creta.js +31 -1
- package/lib/data/command-help/aws-ec2.js +34 -0
- package/lib/data/command-help/grep.js +76 -0
- package/lib/data/command-help/index.js +11 -1
- package/lib/data/command-help/lz.js +57 -0
- package/lib/executors/CommandHelpExecutor.js +6 -1
- package/lib/exercises/.claude/settings.local.json +12 -0
- package/lib/exercises/01-developing-muscle-for-nvim.md +528 -0
- package/lib/exercises/{iterm2-pane-navigation.md → 02-iterm2-pane-navigation.md} +1 -1
- package/lib/exercises/05-svelte-first-steps.md +1340 -0
- package/lib/exercises/{curl-and-pipes.md → 06-curl-and-pipes.md} +187 -72
- package/lib/exercises/07-claude-api-first-steps.md +855 -0
- package/lib/exercises/08-playwright-svelte-guide.md +1384 -0
- package/lib/exercises/09-docker-first-steps.md +1475 -0
- package/lib/exercises/{railway-deployment.md → 10-railway-deployment.md} +1 -0
- package/lib/exercises/{aws-billing-detective.md → 11-aws-billing-detective.md} +215 -35
- package/lib/exercises/12-install-skills.md +755 -0
- package/lib/exercises/13-shell-aliases.md +134 -0
- package/lib/exercises/README.md +187 -0
- package/lib/exercises/utils/booklet-2up.js +133 -0
- package/lib/exercises/utils/booklet-manual-duplex.js +159 -0
- package/lib/exercises/utils/booklet-simple.js +136 -0
- package/lib/exercises/utils/create-booklet.js +116 -0
- package/lib/scripts/aws-ec2-all.sh +58 -0
- package/package.json +3 -2
- /package/lib/exercises/{git-stash-workflow.md → 03-git-stash-workflow.md} +0 -0
- /package/lib/exercises/{array-object-manipulation.md → 04-array-object-manipulation.md} +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Shell Aliases: Personaliza tu Terminal
|
|
2
|
+
|
|
3
|
+
## Objetivo
|
|
4
|
+
|
|
5
|
+
Aprende a crear alias de shell para hacer comandos más intuitivos y personalizados. Crearemos el alias `lz` (localiza recurso) para `curl`.
|
|
6
|
+
|
|
7
|
+
## ¿Qué es un Alias?
|
|
8
|
+
|
|
9
|
+
Un alias es un atajo personalizado para un comando. Por ejemplo:
|
|
10
|
+
- En vez de escribir `curl https://icarus.mx`
|
|
11
|
+
- Escribes `lz https://icarus.mx`
|
|
12
|
+
|
|
13
|
+
## Por qué `lz` para curl
|
|
14
|
+
|
|
15
|
+
**curl** = Client URL (Cliente de URL)
|
|
16
|
+
- **Cliente**: localiza recursos para ti
|
|
17
|
+
- **Universal**: usa HTTP (HyperText Transfer Protocol)
|
|
18
|
+
- **lz**: "localiza recurso" - alias en español
|
|
19
|
+
|
|
20
|
+
## Paso 1: Identificar tu Shell
|
|
21
|
+
|
|
22
|
+
En macOS Catalina+ se usa `zsh` por defecto:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
echo $SHELL
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Si ves `/bin/zsh` → usas zsh (archivo: `~/.zshrc`)
|
|
29
|
+
Si ves `/bin/bash` → usas bash (archivo: `~/.bashrc`)
|
|
30
|
+
|
|
31
|
+
## Paso 2: Abrir el Archivo de Configuración
|
|
32
|
+
|
|
33
|
+
Para zsh (más común en Mac):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
nvim ~/.zshrc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
O si prefieres VS Code:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
code ~/.zshrc
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Paso 3: Agregar el Alias
|
|
46
|
+
|
|
47
|
+
Al final del archivo, agrega:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Alias personalizados
|
|
51
|
+
alias lz='curl'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Guarda el archivo:
|
|
55
|
+
- En nvim: presiona `ESC`, luego escribe `:wq` y `ENTER`
|
|
56
|
+
- En VS Code: `Cmd + S`
|
|
57
|
+
|
|
58
|
+
## Paso 4: Recargar la Configuración
|
|
59
|
+
|
|
60
|
+
Tienes dos opciones:
|
|
61
|
+
|
|
62
|
+
**Opción 1: Recargar el archivo**
|
|
63
|
+
```bash
|
|
64
|
+
source ~/.zshrc
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Opción 2: Cerrar y abrir una nueva terminal**
|
|
68
|
+
|
|
69
|
+
## Paso 5: Probar el Alias
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
lz icarus.mx
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Deberías ver el HTML del sitio de Icarus.
|
|
76
|
+
|
|
77
|
+
## Alias Útiles para Agregar
|
|
78
|
+
|
|
79
|
+
Mientras estás editando `~/.zshrc`, considera agregar:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Alias para comandos comunes
|
|
83
|
+
alias ls='ls -G' # ls con colores en Mac
|
|
84
|
+
alias ll='ls -lah' # ls largo con todo
|
|
85
|
+
alias ..='cd ..' # sube un nivel
|
|
86
|
+
alias ...='cd ../..' # sube dos niveles
|
|
87
|
+
alias gs='git status' # git status rápido
|
|
88
|
+
alias ga='git add' # git add rápido
|
|
89
|
+
alias gc='git commit -m' # git commit con mensaje
|
|
90
|
+
alias gp='git push' # git push rápido
|
|
91
|
+
alias lz='curl' # localiza recurso (nuestro nuevo alias)
|
|
92
|
+
|
|
93
|
+
# Alias para navegación rápida
|
|
94
|
+
alias proyectos='cd ~/proyectos'
|
|
95
|
+
alias icarus='cd ~/icarus'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Verificar que Funciona
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Probar lz
|
|
102
|
+
lz -I icarus.mx
|
|
103
|
+
|
|
104
|
+
# Debería mostrar los headers del sitio
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
**Problema: "command not found: lz"**
|
|
110
|
+
- Solución: Verifica que ejecutaste `source ~/.zshrc`
|
|
111
|
+
|
|
112
|
+
**Problema: "No such file or directory: ~/.zshrc"**
|
|
113
|
+
- Solución: Crea el archivo primero: `touch ~/.zshrc`
|
|
114
|
+
|
|
115
|
+
**Problema: Sigo usando bash en vez de zsh**
|
|
116
|
+
- Solución: Edita `~/.bashrc` en vez de `~/.zshrc`
|
|
117
|
+
|
|
118
|
+
## Siguiente Nivel
|
|
119
|
+
|
|
120
|
+
Una vez que domines los alias básicos, aprende sobre:
|
|
121
|
+
- **Funciones de shell**: alias con argumentos complejos
|
|
122
|
+
- **Dotfiles**: versionar tu configuración con Git
|
|
123
|
+
- **Oh My Zsh**: framework para mejorar tu terminal
|
|
124
|
+
|
|
125
|
+
## Recursos
|
|
126
|
+
|
|
127
|
+
- `man zsh` - Manual de zsh
|
|
128
|
+
- `man bash` - Manual de bash
|
|
129
|
+
- `alias` - Ver todos tus alias actuales
|
|
130
|
+
- `unalias lz` - Eliminar un alias temporal
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
**Pro tip**: Los alias se cargan cada vez que abres una terminal nueva. Si quieres un alias temporal (solo para la sesión actual), simplemente escribe `alias lz='curl'` directo en la terminal sin agregarlo al archivo.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Creta Exercise Guides
|
|
2
|
+
|
|
3
|
+
Progressive learning paths for modern web development - from terminal comfort to production deployment.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🗺️ Learning Journey
|
|
8
|
+
|
|
9
|
+
These exercises follow a natural progression: **Setup → Build → Ship → Scale**
|
|
10
|
+
|
|
11
|
+
Each phase builds on knowledge from previous phases.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Phase 1: Setup & Basics
|
|
16
|
+
|
|
17
|
+
**Get comfortable with your tools and avoid common mistakes**
|
|
18
|
+
|
|
19
|
+
### 1. [Developing Muscle Memory for Neovim](./01-developing-muscle-for-nvim.md)
|
|
20
|
+
**What:** Master vim navigation with `}`, `{`, and fold commands
|
|
21
|
+
**Why:** Efficient code reading without touching the mouse
|
|
22
|
+
**Time:** 15 minutes
|
|
23
|
+
**Level:** 🟢 Beginner
|
|
24
|
+
|
|
25
|
+
### 2. [iTerm2 Pane Navigation](./02-iterm2-pane-navigation.md)
|
|
26
|
+
**What:** Custom keyboard shortcuts for terminal pane switching
|
|
27
|
+
**Why:** Stop using 3-key combos to move between panes
|
|
28
|
+
**Time:** 10 minutes
|
|
29
|
+
**Level:** 🟢 Beginner
|
|
30
|
+
|
|
31
|
+
### 3. [Git Stash Workflow](./03-git-stash-workflow.md)
|
|
32
|
+
**What:** Rescue changes made on the wrong branch
|
|
33
|
+
**Why:** Clean recovery when you code on `main` instead of `dev`
|
|
34
|
+
**Time:** 15 minutes
|
|
35
|
+
**Level:** 🟢 Beginner
|
|
36
|
+
|
|
37
|
+
### 13. [Shell Aliases: Personaliza tu Terminal](./13-shell-aliases.md)
|
|
38
|
+
**What:** Crear alias personalizados (lz para curl)
|
|
39
|
+
**Why:** Comandos más intuitivos en español - piensa "localiza recurso"
|
|
40
|
+
**Time:** 10 minutes
|
|
41
|
+
**Level:** 🟢 Beginner
|
|
42
|
+
|
|
43
|
+
### 4. [Array & Object Manipulation](./04-array-object-manipulation.md)
|
|
44
|
+
**What:** JavaScript warmups - map, filter, reduce, destructuring
|
|
45
|
+
**Why:** Foundation for real app data transformations
|
|
46
|
+
**Time:** 45 minutes
|
|
47
|
+
**Level:** 🟡 Intermediate
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Phase 2: Build
|
|
52
|
+
|
|
53
|
+
**Create real applications with modern frameworks**
|
|
54
|
+
|
|
55
|
+
### 5. [SvelteKit First Steps](./05-svelte-first-steps.md)
|
|
56
|
+
**What:** Understand SvelteKit project structure and file conventions
|
|
57
|
+
**Why:** Stop guessing where files go - learn the routing system
|
|
58
|
+
**Time:** 30 minutes
|
|
59
|
+
**Level:** 🟡 Intermediate
|
|
60
|
+
|
|
61
|
+
### 6. [curl + Pipes](./06-curl-and-pipes.md)
|
|
62
|
+
**What:** Process APIs in real-time with curl and bash pipes
|
|
63
|
+
**Why:** Test endpoints, debug requests, automate data workflows
|
|
64
|
+
**Time:** 45 minutes
|
|
65
|
+
**Level:** 🟡 Intermediate
|
|
66
|
+
|
|
67
|
+
### 7. [Claude API First Steps](./07-claude-api-first-steps.md)
|
|
68
|
+
**What:** Call Claude API directly from terminal with curl
|
|
69
|
+
**Why:** Get AI assistance without leaving your workflow
|
|
70
|
+
**Time:** 30 minutes
|
|
71
|
+
**Level:** 🟡 Intermediate
|
|
72
|
+
|
|
73
|
+
### 8. [Playwright + Svelte Testing](./08-playwright-svelte-guide.md)
|
|
74
|
+
**What:** E2E testing for SvelteKit apps with Tailwind
|
|
75
|
+
**Why:** Test user flows without breaking when you change CSS
|
|
76
|
+
**Time:** 2-3 hours
|
|
77
|
+
**Level:** 🟡 Intermediate
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Phase 3: Ship
|
|
82
|
+
|
|
83
|
+
**Deploy applications to production**
|
|
84
|
+
|
|
85
|
+
### 9. [Docker First Steps](./09-docker-first-steps.md)
|
|
86
|
+
**What:** Containerize Node.js apps to solve "works on my machine"
|
|
87
|
+
**Why:** Consistent environments from dev to production
|
|
88
|
+
**Time:** 1 hour
|
|
89
|
+
**Level:** 🟡 Intermediate
|
|
90
|
+
|
|
91
|
+
### 10. [Railway Deployment](./10-railway-deployment.md)
|
|
92
|
+
**What:** Deploy Node.js/Express apps with custom domains
|
|
93
|
+
**Why:** Ship to production in minutes, not hours
|
|
94
|
+
**Time:** 45 minutes
|
|
95
|
+
**Level:** 🟡 Intermediate
|
|
96
|
+
|
|
97
|
+
### 11. [AWS Billing Detective](./11-aws-billing-detective.md)
|
|
98
|
+
**What:** Hunt down hidden AWS costs across all regions
|
|
99
|
+
**Why:** Stop getting charged for forgotten EBS volumes and Elastic IPs
|
|
100
|
+
**Time:** 30-60 minutes
|
|
101
|
+
**Level:** 🔴 Advanced
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Phase 4: Scale
|
|
106
|
+
|
|
107
|
+
**Advanced tooling and workflow optimization**
|
|
108
|
+
|
|
109
|
+
### 12. [Install Skills Command](./12-install-skills.md)
|
|
110
|
+
**What:** Design a CLI skill installer with project analysis
|
|
111
|
+
**Why:** Automate development workflow setup
|
|
112
|
+
**Time:** Design exercise
|
|
113
|
+
**Level:** 🔴 Advanced
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🎯 How to Use These Guides
|
|
118
|
+
|
|
119
|
+
### Reading in Neovim/LazyVim
|
|
120
|
+
All guides have vim fold markers for structured navigation:
|
|
121
|
+
|
|
122
|
+
```vim
|
|
123
|
+
zM " Close all folds (see just headers)
|
|
124
|
+
za " Toggle current fold
|
|
125
|
+
zR " Open all folds
|
|
126
|
+
} " Jump to next paragraph
|
|
127
|
+
{ " Jump to previous paragraph
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Progressive Learning
|
|
131
|
+
- **New to CLI?** Start with Phase 1
|
|
132
|
+
- **Know basics?** Jump to Phase 2 (SvelteKit)
|
|
133
|
+
- **Ready to deploy?** Go straight to Phase 3
|
|
134
|
+
- **Optimizing workflows?** Phase 4 has advanced patterns
|
|
135
|
+
|
|
136
|
+
### Hands-On Practice
|
|
137
|
+
Every guide includes:
|
|
138
|
+
- 🚨 **Problem** - What confusion does this solve?
|
|
139
|
+
- ✅ **Solution** - Step-by-step walkthrough
|
|
140
|
+
- 💪 **Practice** - Real commands to run
|
|
141
|
+
- 🎯 **Outcomes** - What you'll be able to do
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 🧭 By Topic
|
|
146
|
+
|
|
147
|
+
Prefer to browse by subject?
|
|
148
|
+
|
|
149
|
+
### Terminal & Workflow
|
|
150
|
+
- [Developing Muscle Memory for Neovim](./01-developing-muscle-for-nvim.md)
|
|
151
|
+
- [iTerm2 Pane Navigation](./02-iterm2-pane-navigation.md)
|
|
152
|
+
- [Git Stash Workflow](./03-git-stash-workflow.md)
|
|
153
|
+
- [Shell Aliases](./13-shell-aliases.md)
|
|
154
|
+
- [curl + Pipes](./06-curl-and-pipes.md)
|
|
155
|
+
|
|
156
|
+
### JavaScript & Development
|
|
157
|
+
- [Array & Object Manipulation](./04-array-object-manipulation.md)
|
|
158
|
+
- [SvelteKit First Steps](./05-svelte-first-steps.md)
|
|
159
|
+
|
|
160
|
+
### AI & APIs
|
|
161
|
+
- [Claude API First Steps](./07-claude-api-first-steps.md)
|
|
162
|
+
|
|
163
|
+
### Testing
|
|
164
|
+
- [Playwright + Svelte Testing](./08-playwright-svelte-guide.md)
|
|
165
|
+
|
|
166
|
+
### Deployment & Infrastructure
|
|
167
|
+
- [Docker First Steps](./09-docker-first-steps.md)
|
|
168
|
+
- [Railway Deployment](./10-railway-deployment.md)
|
|
169
|
+
- [AWS Billing Detective](./11-aws-billing-detective.md)
|
|
170
|
+
|
|
171
|
+
### Meta/Tooling
|
|
172
|
+
- [Install Skills Command](./12-install-skills.md)
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 📚 Philosophy
|
|
177
|
+
|
|
178
|
+
These guides follow Creta's design principles:
|
|
179
|
+
|
|
180
|
+
**Spanish-First** - Native language where it matters (coming soon)
|
|
181
|
+
**Simple Solutions** - No over-engineering, practical examples
|
|
182
|
+
**Evolution Over Replacement** - Skills that grow with you
|
|
183
|
+
**CLI-Native** - Work where developers work
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
**Questions? Issues?** Open an issue in the main Creta repo.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PDFDocument } from 'pdf-lib';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
async function createBooklet2Up(inputPath) {
|
|
8
|
+
try {
|
|
9
|
+
const filename = path.basename(inputPath, '.pdf');
|
|
10
|
+
console.log(`\n📚 Creando booklet 2-up para: ${filename}`);
|
|
11
|
+
|
|
12
|
+
const srcFile = await fs.readFile(inputPath);
|
|
13
|
+
const srcDoc = await PDFDocument.load(srcFile);
|
|
14
|
+
const totalPages = srcDoc.getPageCount();
|
|
15
|
+
|
|
16
|
+
console.log(`📄 Páginas originales: ${totalPages}`);
|
|
17
|
+
|
|
18
|
+
// Páginas necesarias (múltiplo de 4)
|
|
19
|
+
const sheetsNeeded = Math.ceil(totalPages / 4);
|
|
20
|
+
const paddedPages = sheetsNeeded * 4;
|
|
21
|
+
|
|
22
|
+
console.log(`📄 Hojas físicas: ${sheetsNeeded}`);
|
|
23
|
+
|
|
24
|
+
// Obtener tamaño de página
|
|
25
|
+
const firstPage = srcDoc.getPage(0);
|
|
26
|
+
const { width: pageWidth, height: pageHeight } = firstPage.getSize();
|
|
27
|
+
|
|
28
|
+
// Crear docs finales
|
|
29
|
+
const frontDoc = await PDFDocument.create();
|
|
30
|
+
const backDoc = await PDFDocument.create();
|
|
31
|
+
|
|
32
|
+
// Para cada hoja física
|
|
33
|
+
for (let sheet = 0; sheet < sheetsNeeded; sheet++) {
|
|
34
|
+
// Índices para booklet (0-indexed)
|
|
35
|
+
const indices = {
|
|
36
|
+
frontRight: sheet * 2, // 0, 2, 4, 6...
|
|
37
|
+
frontLeft: paddedPages - (sheet * 2) - 1, // 23, 21, 19, 17...
|
|
38
|
+
backLeft: sheet * 2 + 1, // 1, 3, 5, 7...
|
|
39
|
+
backRight: paddedPages - (sheet * 2) - 2 // 22, 20, 18, 16...
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
console.log(`Hoja ${sheet + 1}:`);
|
|
43
|
+
console.log(` Frente: [${indices.frontLeft < totalPages ? indices.frontLeft + 1 : 'blank'}] [${indices.frontRight < totalPages ? indices.frontRight + 1 : 'blank'}]`);
|
|
44
|
+
console.log(` Reverso: [${indices.backLeft < totalPages ? indices.backLeft + 1 : 'blank'}] [${indices.backRight < totalPages ? indices.backRight + 1 : 'blank'}]`);
|
|
45
|
+
|
|
46
|
+
// FRENTE: crear hoja 2-up (landscape)
|
|
47
|
+
const frontSheet = frontDoc.addPage([pageWidth * 2, pageHeight]);
|
|
48
|
+
|
|
49
|
+
// Página derecha del frente
|
|
50
|
+
if (indices.frontRight < totalPages) {
|
|
51
|
+
const [page] = await frontDoc.copyPages(srcDoc, [indices.frontRight]);
|
|
52
|
+
const embeddedPage = await frontDoc.embedPage(page);
|
|
53
|
+
frontSheet.drawPage(embeddedPage, {
|
|
54
|
+
x: pageWidth,
|
|
55
|
+
y: 0,
|
|
56
|
+
width: pageWidth,
|
|
57
|
+
height: pageHeight
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Página izquierda del frente
|
|
62
|
+
if (indices.frontLeft < totalPages) {
|
|
63
|
+
const [page] = await frontDoc.copyPages(srcDoc, [indices.frontLeft]);
|
|
64
|
+
const embeddedPage = await frontDoc.embedPage(page);
|
|
65
|
+
frontSheet.drawPage(embeddedPage, {
|
|
66
|
+
x: 0,
|
|
67
|
+
y: 0,
|
|
68
|
+
width: pageWidth,
|
|
69
|
+
height: pageHeight
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// REVERSO: crear hoja 2-up (landscape)
|
|
74
|
+
const backSheet = backDoc.addPage([pageWidth * 2, pageHeight]);
|
|
75
|
+
|
|
76
|
+
// Página izquierda del reverso
|
|
77
|
+
if (indices.backLeft < totalPages) {
|
|
78
|
+
const [page] = await backDoc.copyPages(srcDoc, [indices.backLeft]);
|
|
79
|
+
const embeddedPage = await backDoc.embedPage(page);
|
|
80
|
+
backSheet.drawPage(embeddedPage, {
|
|
81
|
+
x: 0,
|
|
82
|
+
y: 0,
|
|
83
|
+
width: pageWidth,
|
|
84
|
+
height: pageHeight
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Página derecha del reverso
|
|
89
|
+
if (indices.backRight < totalPages) {
|
|
90
|
+
const [page] = await backDoc.copyPages(srcDoc, [indices.backRight]);
|
|
91
|
+
const embeddedPage = await backDoc.embedPage(page);
|
|
92
|
+
backSheet.drawPage(embeddedPage, {
|
|
93
|
+
x: pageWidth,
|
|
94
|
+
y: 0,
|
|
95
|
+
width: pageWidth,
|
|
96
|
+
height: pageHeight
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Guardar
|
|
102
|
+
const frontPath = path.join(path.dirname(inputPath), `${filename}-FRENTE-v3.pdf`);
|
|
103
|
+
const backPath = path.join(path.dirname(inputPath), `${filename}-REVERSO-v3.pdf`);
|
|
104
|
+
|
|
105
|
+
await fs.writeFile(frontPath, await frontDoc.save());
|
|
106
|
+
await fs.writeFile(backPath, await backDoc.save());
|
|
107
|
+
|
|
108
|
+
console.log('\n✅ Creados:\n');
|
|
109
|
+
console.log(` ${path.basename(frontPath)}`);
|
|
110
|
+
console.log(` ${path.basename(backPath)}`);
|
|
111
|
+
console.log('\n📖 IMPRIME: FRENTE → mete hojas SIN voltear → REVERSO\n');
|
|
112
|
+
|
|
113
|
+
return { frontPath, backPath };
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('❌ Error:', error.message);
|
|
117
|
+
console.error(error.stack);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const args = process.argv.slice(2);
|
|
124
|
+
if (args.length === 0) {
|
|
125
|
+
console.log('Uso: node booklet-2up.js <archivo.pdf>');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await createBooklet2Up(path.resolve(args[0]));
|
|
130
|
+
console.log('¡Listo! 🎉\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PDFDocument } from 'pdf-lib';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Crea un booklet para impresoras de duplex manual SIN voltear
|
|
9
|
+
* (para impresoras que imprimen en la cara de abajo)
|
|
10
|
+
*/
|
|
11
|
+
async function createManualDuplexBooklet(inputPath) {
|
|
12
|
+
try {
|
|
13
|
+
const filename = path.basename(inputPath, '.pdf');
|
|
14
|
+
console.log(`\n📚 Creando booklet para: ${filename}`);
|
|
15
|
+
|
|
16
|
+
// Leer PDF original
|
|
17
|
+
const srcFile = await fs.readFile(inputPath);
|
|
18
|
+
const srcDoc = await PDFDocument.load(srcFile);
|
|
19
|
+
const totalPages = srcDoc.getPageCount();
|
|
20
|
+
|
|
21
|
+
console.log(`📄 Páginas originales: ${totalPages}`);
|
|
22
|
+
|
|
23
|
+
// Calcular páginas necesarias (múltiplo de 4)
|
|
24
|
+
const sheetsNeeded = Math.ceil(totalPages / 4);
|
|
25
|
+
const paddedPages = sheetsNeeded * 4;
|
|
26
|
+
const blankPages = paddedPages - totalPages;
|
|
27
|
+
|
|
28
|
+
if (blankPages > 0) {
|
|
29
|
+
console.log(`📄 Páginas en blanco añadidas: ${blankPages}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`📄 Total hojas físicas: ${sheetsNeeded}`);
|
|
33
|
+
|
|
34
|
+
// Crear documentos para frente y reverso
|
|
35
|
+
const frontDoc = await PDFDocument.create();
|
|
36
|
+
const backDoc = await PDFDocument.create();
|
|
37
|
+
|
|
38
|
+
// Crear página en blanco del mismo tamaño que la primera
|
|
39
|
+
const firstPage = srcDoc.getPage(0);
|
|
40
|
+
const { width, height } = firstPage.getSize();
|
|
41
|
+
const blankPage = await PDFDocument.create();
|
|
42
|
+
const blank = blankPage.addPage([width, height]);
|
|
43
|
+
|
|
44
|
+
// Algoritmo de booklet para duplex manual SIN voltear
|
|
45
|
+
// Para cada hoja física, el orden es:
|
|
46
|
+
// Frente: [última | primera]
|
|
47
|
+
// Reverso: [segunda | penúltima]
|
|
48
|
+
|
|
49
|
+
for (let sheet = 0; sheet < sheetsNeeded; sheet++) {
|
|
50
|
+
// Calcular números de página para esta hoja física (0-indexed)
|
|
51
|
+
const frontLeft = paddedPages - (sheet * 2) - 1; // última, antepenúltima, etc
|
|
52
|
+
const frontRight = (sheet * 2); // primera, tercera, etc
|
|
53
|
+
const backLeft = (sheet * 2) + 1; // segunda, cuarta, etc
|
|
54
|
+
const backRight = paddedPages - (sheet * 2) - 2; // penúltima, etc
|
|
55
|
+
|
|
56
|
+
console.log(`Hoja ${sheet + 1}: Frente[${frontLeft >= 0 && frontLeft < totalPages ? frontLeft + 1 : 'blank'}, ${frontRight >= 0 && frontRight < totalPages ? frontRight + 1 : 'blank'}] Reverso[${backLeft >= 0 && backLeft < totalPages ? backLeft + 1 : 'blank'}, ${backRight >= 0 && backRight < totalPages ? backRight + 1 : 'blank'}]`);
|
|
57
|
+
|
|
58
|
+
// Crear página de FRENTE (2 páginas lado a lado)
|
|
59
|
+
const frontPageDoc = await PDFDocument.create();
|
|
60
|
+
const frontPageSheet = frontPageDoc.addPage([width * 2, height]);
|
|
61
|
+
|
|
62
|
+
// Izquierda del frente
|
|
63
|
+
if (frontLeft >= 0 && frontLeft < totalPages) {
|
|
64
|
+
const [leftPage] = await frontPageDoc.copyPages(srcDoc, [frontLeft]);
|
|
65
|
+
frontPageSheet.drawPage(leftPage, { x: 0, y: 0, width, height });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Derecha del frente
|
|
69
|
+
if (frontRight >= 0 && frontRight < totalPages) {
|
|
70
|
+
const [rightPage] = await frontPageDoc.copyPages(srcDoc, [frontRight]);
|
|
71
|
+
frontPageSheet.drawPage(rightPage, { x: width, y: 0, width, height });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Copiar hoja de frente al documento final
|
|
75
|
+
const [finalFrontPage] = await frontDoc.copyPages(frontPageDoc, [0]);
|
|
76
|
+
frontDoc.addPage(finalFrontPage);
|
|
77
|
+
|
|
78
|
+
// Crear página de REVERSO (2 páginas lado a lado)
|
|
79
|
+
const backPageDoc = await PDFDocument.create();
|
|
80
|
+
const backPageSheet = backPageDoc.addPage([width * 2, height]);
|
|
81
|
+
|
|
82
|
+
// Izquierda del reverso
|
|
83
|
+
if (backLeft >= 0 && backLeft < totalPages) {
|
|
84
|
+
const [leftPage] = await backPageDoc.copyPages(srcDoc, [backLeft]);
|
|
85
|
+
backPageSheet.drawPage(leftPage, { x: 0, y: 0, width, height });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Derecha del reverso
|
|
89
|
+
if (backRight >= 0 && backRight < totalPages) {
|
|
90
|
+
const [rightPage] = await backPageDoc.copyPages(srcDoc, [backRight]);
|
|
91
|
+
backPageSheet.drawPage(rightPage, { x: width, y: 0, width, height });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Copiar hoja de reverso al documento final
|
|
95
|
+
const [finalBackPage] = await backDoc.copyPages(backPageDoc, [0]);
|
|
96
|
+
backDoc.addPage(finalBackPage);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Como la impresora imprime del lado de abajo y las hojas salen al revés,
|
|
100
|
+
// necesitamos INVERTIR el orden de las páginas del REVERSO
|
|
101
|
+
const backPagesReversed = await PDFDocument.create();
|
|
102
|
+
const backPageCount = backDoc.getPageCount();
|
|
103
|
+
for (let i = backPageCount - 1; i >= 0; i--) {
|
|
104
|
+
const [page] = await backPagesReversed.copyPages(backDoc, [i]);
|
|
105
|
+
backPagesReversed.addPage(page);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Guardar PDFs
|
|
109
|
+
const frontPath = path.join(path.dirname(inputPath), `${filename}-FRENTE.pdf`);
|
|
110
|
+
const backPath = path.join(path.dirname(inputPath), `${filename}-REVERSO.pdf`);
|
|
111
|
+
|
|
112
|
+
await fs.writeFile(frontPath, await frontDoc.save());
|
|
113
|
+
await fs.writeFile(backPath, await backPagesReversed.save());
|
|
114
|
+
|
|
115
|
+
console.log('\n✅ Booklets creados:\n');
|
|
116
|
+
console.log(` 📄 FRENTE: ${path.basename(frontPath)}`);
|
|
117
|
+
console.log(` 📄 REVERSO: ${path.basename(backPath)}`);
|
|
118
|
+
|
|
119
|
+
console.log('\n📖 INSTRUCCIONES DE IMPRESIÓN:\n');
|
|
120
|
+
console.log('1. Imprime el archivo FRENTE (todas las páginas)');
|
|
121
|
+
console.log('2. Toma la pila tal como salió');
|
|
122
|
+
console.log('3. Métela de nuevo en la bandeja SIN voltear');
|
|
123
|
+
console.log(' → Cara impresa ARRIBA (visible)');
|
|
124
|
+
console.log(' → Cara blanca ABAJO (donde imprimirá)');
|
|
125
|
+
console.log('4. Imprime el archivo REVERSO');
|
|
126
|
+
console.log('5. Dobla todas las hojas por la mitad');
|
|
127
|
+
console.log('6. Encuaderna por el lado IZQUIERDO\n');
|
|
128
|
+
|
|
129
|
+
return { frontPath, backPath };
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('❌ Error:', error.message);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const args = process.argv.slice(2);
|
|
139
|
+
|
|
140
|
+
if (args.length === 0) {
|
|
141
|
+
console.log('Uso: node booklet-manual-duplex.js <archivo.pdf>');
|
|
142
|
+
console.log('Ejemplo: node booklet-manual-duplex.js railway-deployment.pdf');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const inputPath = path.resolve(args[0]);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await fs.access(inputPath);
|
|
150
|
+
} catch {
|
|
151
|
+
console.error(`❌ Archivo no encontrado: ${inputPath}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await createManualDuplexBooklet(inputPath);
|
|
156
|
+
console.log('\n¡Listo! 🎉\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch(console.error);
|