@icarusmx/creta 1.5.12 → 1.5.13
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 +30 -1
- package/lib/data/command-help/aws-ec2.js +34 -0
- package/lib/data/command-help/grep.js +72 -0
- package/lib/data/command-help/index.js +9 -1
- 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/README.md +180 -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,180 @@
|
|
|
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
|
+
### 4. [Array & Object Manipulation](./04-array-object-manipulation.md)
|
|
38
|
+
**What:** JavaScript warmups - map, filter, reduce, destructuring
|
|
39
|
+
**Why:** Foundation for real app data transformations
|
|
40
|
+
**Time:** 45 minutes
|
|
41
|
+
**Level:** 🟡 Intermediate
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Phase 2: Build
|
|
46
|
+
|
|
47
|
+
**Create real applications with modern frameworks**
|
|
48
|
+
|
|
49
|
+
### 5. [SvelteKit First Steps](./05-svelte-first-steps.md)
|
|
50
|
+
**What:** Understand SvelteKit project structure and file conventions
|
|
51
|
+
**Why:** Stop guessing where files go - learn the routing system
|
|
52
|
+
**Time:** 30 minutes
|
|
53
|
+
**Level:** 🟡 Intermediate
|
|
54
|
+
|
|
55
|
+
### 6. [curl + Pipes](./06-curl-and-pipes.md)
|
|
56
|
+
**What:** Process APIs in real-time with curl and bash pipes
|
|
57
|
+
**Why:** Test endpoints, debug requests, automate data workflows
|
|
58
|
+
**Time:** 45 minutes
|
|
59
|
+
**Level:** 🟡 Intermediate
|
|
60
|
+
|
|
61
|
+
### 7. [Claude API First Steps](./07-claude-api-first-steps.md)
|
|
62
|
+
**What:** Call Claude API directly from terminal with curl
|
|
63
|
+
**Why:** Get AI assistance without leaving your workflow
|
|
64
|
+
**Time:** 30 minutes
|
|
65
|
+
**Level:** 🟡 Intermediate
|
|
66
|
+
|
|
67
|
+
### 8. [Playwright + Svelte Testing](./08-playwright-svelte-guide.md)
|
|
68
|
+
**What:** E2E testing for SvelteKit apps with Tailwind
|
|
69
|
+
**Why:** Test user flows without breaking when you change CSS
|
|
70
|
+
**Time:** 2-3 hours
|
|
71
|
+
**Level:** 🟡 Intermediate
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 3: Ship
|
|
76
|
+
|
|
77
|
+
**Deploy applications to production**
|
|
78
|
+
|
|
79
|
+
### 9. [Docker First Steps](./09-docker-first-steps.md)
|
|
80
|
+
**What:** Containerize Node.js apps to solve "works on my machine"
|
|
81
|
+
**Why:** Consistent environments from dev to production
|
|
82
|
+
**Time:** 1 hour
|
|
83
|
+
**Level:** 🟡 Intermediate
|
|
84
|
+
|
|
85
|
+
### 10. [Railway Deployment](./10-railway-deployment.md)
|
|
86
|
+
**What:** Deploy Node.js/Express apps with custom domains
|
|
87
|
+
**Why:** Ship to production in minutes, not hours
|
|
88
|
+
**Time:** 45 minutes
|
|
89
|
+
**Level:** 🟡 Intermediate
|
|
90
|
+
|
|
91
|
+
### 11. [AWS Billing Detective](./11-aws-billing-detective.md)
|
|
92
|
+
**What:** Hunt down hidden AWS costs across all regions
|
|
93
|
+
**Why:** Stop getting charged for forgotten EBS volumes and Elastic IPs
|
|
94
|
+
**Time:** 30-60 minutes
|
|
95
|
+
**Level:** 🔴 Advanced
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Phase 4: Scale
|
|
100
|
+
|
|
101
|
+
**Advanced tooling and workflow optimization**
|
|
102
|
+
|
|
103
|
+
### 12. [Install Skills Command](./12-install-skills.md)
|
|
104
|
+
**What:** Design a CLI skill installer with project analysis
|
|
105
|
+
**Why:** Automate development workflow setup
|
|
106
|
+
**Time:** Design exercise
|
|
107
|
+
**Level:** 🔴 Advanced
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🎯 How to Use These Guides
|
|
112
|
+
|
|
113
|
+
### Reading in Neovim/LazyVim
|
|
114
|
+
All guides have vim fold markers for structured navigation:
|
|
115
|
+
|
|
116
|
+
```vim
|
|
117
|
+
zM " Close all folds (see just headers)
|
|
118
|
+
za " Toggle current fold
|
|
119
|
+
zR " Open all folds
|
|
120
|
+
} " Jump to next paragraph
|
|
121
|
+
{ " Jump to previous paragraph
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Progressive Learning
|
|
125
|
+
- **New to CLI?** Start with Phase 1
|
|
126
|
+
- **Know basics?** Jump to Phase 2 (SvelteKit)
|
|
127
|
+
- **Ready to deploy?** Go straight to Phase 3
|
|
128
|
+
- **Optimizing workflows?** Phase 4 has advanced patterns
|
|
129
|
+
|
|
130
|
+
### Hands-On Practice
|
|
131
|
+
Every guide includes:
|
|
132
|
+
- 🚨 **Problem** - What confusion does this solve?
|
|
133
|
+
- ✅ **Solution** - Step-by-step walkthrough
|
|
134
|
+
- 💪 **Practice** - Real commands to run
|
|
135
|
+
- 🎯 **Outcomes** - What you'll be able to do
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧭 By Topic
|
|
140
|
+
|
|
141
|
+
Prefer to browse by subject?
|
|
142
|
+
|
|
143
|
+
### Terminal & Workflow
|
|
144
|
+
- [Developing Muscle Memory for Neovim](./01-developing-muscle-for-nvim.md)
|
|
145
|
+
- [iTerm2 Pane Navigation](./02-iterm2-pane-navigation.md)
|
|
146
|
+
- [Git Stash Workflow](./03-git-stash-workflow.md)
|
|
147
|
+
- [curl + Pipes](./06-curl-and-pipes.md)
|
|
148
|
+
|
|
149
|
+
### JavaScript & Development
|
|
150
|
+
- [Array & Object Manipulation](./04-array-object-manipulation.md)
|
|
151
|
+
- [SvelteKit First Steps](./05-svelte-first-steps.md)
|
|
152
|
+
|
|
153
|
+
### AI & APIs
|
|
154
|
+
- [Claude API First Steps](./07-claude-api-first-steps.md)
|
|
155
|
+
|
|
156
|
+
### Testing
|
|
157
|
+
- [Playwright + Svelte Testing](./08-playwright-svelte-guide.md)
|
|
158
|
+
|
|
159
|
+
### Deployment & Infrastructure
|
|
160
|
+
- [Docker First Steps](./09-docker-first-steps.md)
|
|
161
|
+
- [Railway Deployment](./10-railway-deployment.md)
|
|
162
|
+
- [AWS Billing Detective](./11-aws-billing-detective.md)
|
|
163
|
+
|
|
164
|
+
### Meta/Tooling
|
|
165
|
+
- [Install Skills Command](./12-install-skills.md)
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 📚 Philosophy
|
|
170
|
+
|
|
171
|
+
These guides follow Creta's design principles:
|
|
172
|
+
|
|
173
|
+
**Spanish-First** - Native language where it matters (coming soon)
|
|
174
|
+
**Simple Solutions** - No over-engineering, practical examples
|
|
175
|
+
**Evolution Over Replacement** - Skills that grow with you
|
|
176
|
+
**CLI-Native** - Work where developers work
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
**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);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { PDFDocument, degrees } from 'pdf-lib';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
async function createManualDuplexBooklet(inputPath) {
|
|
8
|
+
try {
|
|
9
|
+
const filename = path.basename(inputPath, '.pdf');
|
|
10
|
+
console.log(`\n📚 Creando booklet para: ${filename}`);
|
|
11
|
+
|
|
12
|
+
// Leer PDF original
|
|
13
|
+
const srcFile = await fs.readFile(inputPath);
|
|
14
|
+
const srcDoc = await PDFDocument.load(srcFile);
|
|
15
|
+
const totalPages = srcDoc.getPageCount();
|
|
16
|
+
|
|
17
|
+
console.log(`📄 Páginas originales: ${totalPages}`);
|
|
18
|
+
|
|
19
|
+
// Calcular páginas necesarias (múltiplo de 4)
|
|
20
|
+
const sheetsNeeded = Math.ceil(totalPages / 4);
|
|
21
|
+
const paddedPages = sheetsNeeded * 4;
|
|
22
|
+
const blankPages = paddedPages - totalPages;
|
|
23
|
+
|
|
24
|
+
if (blankPages > 0) {
|
|
25
|
+
console.log(`📄 Páginas en blanco añadidas: ${blankPages}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(`📄 Total hojas físicas: ${sheetsNeeded}`);
|
|
28
|
+
|
|
29
|
+
// Crear array de páginas incluyendo blancos
|
|
30
|
+
const pages = [];
|
|
31
|
+
for (let i = 0; i < totalPages; i++) {
|
|
32
|
+
pages.push(srcDoc.getPage(i));
|
|
33
|
+
}
|
|
34
|
+
// Añadir páginas en blanco si es necesario
|
|
35
|
+
const firstPage = srcDoc.getPage(0);
|
|
36
|
+
const { width, height } = firstPage.getSize();
|
|
37
|
+
for (let i = totalPages; i < paddedPages; i++) {
|
|
38
|
+
// Crear página en blanco temporal (no la añadimos al doc original)
|
|
39
|
+
pages.push(null); // null representa página en blanco
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Crear documentos finales
|
|
43
|
+
const frontDoc = await PDFDocument.create();
|
|
44
|
+
const backDoc = await PDFDocument.create();
|
|
45
|
+
|
|
46
|
+
// Para cada hoja física
|
|
47
|
+
for (let sheet = 0; sheet < sheetsNeeded; sheet++) {
|
|
48
|
+
// Calcular índices de página (0-indexed)
|
|
49
|
+
const frontLeft = paddedPages - (sheet * 2) - 1; // última hacia adentro
|
|
50
|
+
const frontRight = (sheet * 2); // primera hacia afuera
|
|
51
|
+
const backLeft = (sheet * 2) + 1; // segunda
|
|
52
|
+
const backRight = paddedPages - (sheet * 2) - 2; // penúltima hacia adentro
|
|
53
|
+
|
|
54
|
+
console.log(`Hoja ${sheet + 1}: Frente[${frontLeft < totalPages ? frontLeft + 1 : 'blank'}, ${frontRight < totalPages ? frontRight + 1 : 'blank'}] Reverso[${backLeft < totalPages ? backLeft + 1 : 'blank'}, ${backRight < totalPages ? backRight + 1 : 'blank'}]`);
|
|
55
|
+
|
|
56
|
+
// FRENTE: Agregar página derecha (frontRight) y luego izquierda (frontLeft)
|
|
57
|
+
// El orden importa porque así queda al doblar
|
|
58
|
+
if (frontRight < totalPages) {
|
|
59
|
+
const [copied] = await frontDoc.copyPages(srcDoc, [frontRight]);
|
|
60
|
+
frontDoc.addPage(copied);
|
|
61
|
+
} else {
|
|
62
|
+
frontDoc.addPage(frontDoc.addPage([width, height]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (frontLeft < totalPages) {
|
|
66
|
+
const [copied] = await frontDoc.copyPages(srcDoc, [frontLeft]);
|
|
67
|
+
frontDoc.addPage(copied);
|
|
68
|
+
} else {
|
|
69
|
+
frontDoc.addPage(frontDoc.addPage([width, height]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// REVERSO: Agregar en el orden correcto para duplex sin voltear
|
|
73
|
+
// Izquierda primero, derecha después
|
|
74
|
+
if (backLeft < totalPages) {
|
|
75
|
+
const [copied] = await backDoc.copyPages(srcDoc, [backLeft]);
|
|
76
|
+
backDoc.addPage(copied);
|
|
77
|
+
} else {
|
|
78
|
+
backDoc.addPage(backDoc.addPage([width, height]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (backRight < totalPages) {
|
|
82
|
+
const [copied] = await backDoc.copyPages(srcDoc, [backRight]);
|
|
83
|
+
backDoc.addPage(copied);
|
|
84
|
+
} else {
|
|
85
|
+
backDoc.addPage(backDoc.addPage([width, height]));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Guardar PDFs
|
|
90
|
+
const frontPath = path.join(path.dirname(inputPath), `${filename}-FRENTE-v2.pdf`);
|
|
91
|
+
const backPath = path.join(path.dirname(inputPath), `${filename}-REVERSO-v2.pdf`);
|
|
92
|
+
|
|
93
|
+
await fs.writeFile(frontPath, await frontDoc.save());
|
|
94
|
+
await fs.writeFile(backPath, await backDoc.save());
|
|
95
|
+
|
|
96
|
+
console.log('\n✅ Booklets creados:\n');
|
|
97
|
+
console.log(` 📄 FRENTE: ${path.basename(frontPath)}`);
|
|
98
|
+
console.log(` 📄 REVERSO: ${path.basename(backPath)}`);
|
|
99
|
+
|
|
100
|
+
console.log('\n📖 INSTRUCCIONES:\n');
|
|
101
|
+
console.log('1. Imprime FRENTE (todas las páginas)');
|
|
102
|
+
console.log('2. Toma la pila y métela de nuevo SIN voltear');
|
|
103
|
+
console.log('3. Imprime REVERSO');
|
|
104
|
+
console.log('4. Dobla por la mitad y encuaderna a la izquierda\n');
|
|
105
|
+
|
|
106
|
+
return { frontPath, backPath };
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('❌ Error:', error.message);
|
|
110
|
+
console.error(error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
|
|
118
|
+
if (args.length === 0) {
|
|
119
|
+
console.log('Uso: node booklet-simple.js <archivo.pdf>');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const inputPath = path.resolve(args[0]);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(inputPath);
|
|
127
|
+
} catch {
|
|
128
|
+
console.error(`❌ Archivo no encontrado: ${inputPath}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await createManualDuplexBooklet(inputPath);
|
|
133
|
+
console.log('\n¡Listo! 🎉\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch(console.error);
|