@codepassion/skills 1.1.1 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codepassion/skills",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Agent skills (SKILL.md) for CodePassion (C9N) engineering conventions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -172,3 +172,68 @@ Alpha: tag from develop, `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push orig
172
172
  Production: confirm Go → pull release branch → create release → CI deploys → monitor.
173
173
  Reference: https://semver.org/spec/v2.0.0.html
174
174
 
175
+
176
+
177
+ ---
178
+
179
+ ## Deliverables
180
+
181
+ Repeatable customer project delivery for the Code Passion team. Turns a codebase into a
182
+ clean, customer-ready documentation package. Fully generic — adapt to whatever stack the
183
+ project uses. Bundled `scripts/` are proven references; read and adapt them per project.
184
+
185
+ ## Four mandatory gates
186
+
187
+ These ALWAYS run, in order — never skip them:
188
+
189
+ 1. **Clarify-first** — interview before generating anything (see Workflow step 1).
190
+ 2. **Security/sanitize** — never ship secrets. Before packaging: scan for and exclude
191
+ `.env`, credentials, service-account JSON, `*.pem`/`*.key`; sanitize real env files to
192
+ `.env.example` (names + descriptions, no values); strip `.DS_Store`. If you copied any
193
+ credential into a build dir to make tooling work, delete it before finishing.
194
+ 3. **Verify rendered output** — after creating PDFs, screenshot each (headless browser
195
+ `--screenshot`) and visually confirm. Catches illegible/empty diagrams and truncation.
196
+ Do not trust `file`'s page count — use `mdls -name kMDItemNumberOfPages` on macOS.
197
+ 4. **Delivery email** — always draft one. Thai for Thai customer teams, Code Passion
198
+ framing (from Code Passion → client). Leave recipient/contact as fill-in blanks.
199
+
200
+ ## Workflow
201
+
202
+ 1. **Clarify** (ask, don't assume): audience (customer vs internal); which documents are
203
+ wanted; document language (default: English docs + Thai email); output format (PDF?);
204
+ package structure — **plain docs package (default)** or **ISO/IEC 29110 work-product
205
+ layout** (offer it, see [REFERENCE.md](../c9n-deliverables/REFERENCE.md)); any contractual scope.
206
+ 2. **Inventory** the codebase — detect stack, framework, DB, API doc tooling; decide which
207
+ generators apply. Skip what doesn't fit the stack.
208
+ 3. **Generate** the doc set (only what was requested). Common artifacts and recipes are in
209
+ [REFERENCE.md](../c9n-deliverables/REFERENCE.md): system/architecture overview (+ Mermaid diagrams),
210
+ database design (ER diagram + data dictionary), API reference (export spec → PDF),
211
+ deployment runbook, configuration reference, source snapshot.
212
+ 4. **Render PDFs** with `scripts/md-to-pdf.mjs` (Markdown+Mermaid) and
213
+ `scripts/openapi-to-pdf.mjs` (OpenAPI). Then run gate 3 (verify).
214
+ 5. **Organize** per the chosen structure. Master `README.md` index; for 29110 include a
215
+ supporting-artifact → work-product traceability map.
216
+ 6. **Security gate** (gate 2), then **package**: zip the organized folder directly (or, if a
217
+ dedicated packaging skill such as `deliverables-packager` is installed, hand off to it for
218
+ final folder polish + zip).
219
+ 7. **Delivery email** (gate 4).
220
+
221
+ ## Scripts
222
+
223
+ | Script | Purpose |
224
+ |--------|---------|
225
+ | `scripts/md-to-pdf.mjs <in.md> <out.pdf>` | Markdown → PDF, renders Mermaid. Needs a Chromium-family browser. |
226
+ | `scripts/openapi-to-pdf.mjs <spec.json> <out.pdf> [guide.md]` | OpenAPI → print-friendly PDF (no lazy-load truncation). |
227
+ | `scripts/export-openapi.cjs <out.json>` | TEMPLATE: boot a NestJS app, dump Swagger spec. Adapt to project. |
228
+ | `scripts/data-dictionary.cjs <out.md>` | Data dictionary from live MySQL `INFORMATION_SCHEMA`. |
229
+ | `scripts/snapshot-source.sh <repo> <out.zip>` | Clean `git archive` source snapshot. |
230
+
231
+ ## Gotchas
232
+
233
+ - **Wide Mermaid diagrams** render as an illegible strip at page width. Use `flowchart TB`
234
+ and split large ER diagrams into per-domain groups (portrait aspect ratio).
235
+ - **OpenAPI export**: build first (`nest build`); ts-node mis-emits decorator metadata.
236
+ Booting connects to the DB — use a dev DB, never production.
237
+ - **Redoc/Swagger UI → PDF**: they lazy-render; printing truncates. Use `openapi-to-pdf.mjs`.
238
+ - See [REFERENCE.md](../c9n-deliverables/REFERENCE.md) for the ISO/IEC 29110 layout and per-artifact recipes.
239
+
@@ -0,0 +1,97 @@
1
+ # C9N Deliverables — Reference
2
+
3
+ ## Document set (artifacts & recipes)
4
+
5
+ Generate only what the clarify step asked for. Each is a Markdown source rendered to PDF.
6
+
7
+ ### System / Architecture Overview
8
+ - Content: tech stack table, module map, auth model, API versioning, **architecture
9
+ diagrams** (Mermaid `flowchart`): System Context, Layered Request Flow, Deployment.
10
+ - Source from README + code structure. Render with `md-to-pdf.mjs`.
11
+ - Keep flow diagrams `flowchart TB` so they stay legible at page width.
12
+
13
+ ### Database Design
14
+ - ER diagram(s) as Mermaid `erDiagram`. **Split into per-domain groups** — one giant ER is
15
+ unreadable. Plus a domain-grouped entity catalog.
16
+ - **Data dictionary**: every table & column. Generate from the live DB with
17
+ `data-dictionary.cjs` (most accurate) — adjust the env var names to the project.
18
+
19
+ ### API Reference
20
+ - Export the spec from the running/built app (`export-openapi.cjs` for NestJS/Swagger, or
21
+ `curl <host>/api-json`). Render with `openapi-to-pdf.mjs` (optionally pass a guide.md
22
+ intro covering auth, versioning, how to import the spec).
23
+ - Also ship `openapi.json` (Postman/codegen) and optionally a Redoc HTML for browsing.
24
+
25
+ ### Deployment Runbook
26
+ - Prerequisites, env vars (point to `.env.example`), local dev, build, Docker, CI/CD
27
+ (e.g. Cloud Build → Cloud Run), production checklist, troubleshooting.
28
+
29
+ ### Configuration Reference
30
+ - Sanitized `.env.example` — variable names + descriptions, **no values**. Plus notes on
31
+ Docker/compose/CI config files.
32
+
33
+ ### Source Code Snapshot
34
+ - `snapshot-source.sh <repo> <out.zip>`. Run at final delivery so it isn't stale.
35
+
36
+ ## Plain docs package layout (default)
37
+
38
+ ```
39
+ deliverables/
40
+ ├── README.md # index
41
+ ├── 01-documentation/
42
+ │ ├── markdown/ # editable sources
43
+ │ └── pdf/ # rendered PDFs
44
+ ├── 02-database/ # data dictionary, ER
45
+ ├── 03-configuration/ # .env.example, notes
46
+ ├── 04-utilities/ # helper scripts
47
+ └── 05-source-code/ # snapshot (or placeholder)
48
+ ```
49
+
50
+ ## ISO/IEC 29110 layout (Basic Profile, offer when engagement needs it)
51
+
52
+ Two process groups. Formal work products at their slots; supporting engineering artifacts
53
+ filed under the SI work product they evidence, with a traceability map in the README.
54
+
55
+ ```
56
+ deliverables/
57
+ ├── README.md # master index + traceability
58
+ ├── 00-project-management/ # PM work products
59
+ │ ├── PM-01_Statement-of-Work.pdf
60
+ │ ├── PM-02_Project-Plan.pdf
61
+ │ ├── PM-03_Progress-Status-Record.pdf
62
+ │ ├── PM-04_Change-Request.pdf
63
+ │ ├── PM-05_Acceptance-Record.pdf
64
+ │ └── PM-06_Verification-Validation-Record.pdf
65
+ └── 01-software-implementation/ # SI work products
66
+ ├── SI-01_Requirements-Specification.pdf
67
+ ├── SI-02_Software-Design.pdf
68
+ ├── SI-03_Traceability-Record.pdf
69
+ ├── SI-04_Test-Cases-and-Procedures.pdf
70
+ ├── SI-05_Test-Report.pdf
71
+ ├── SI-06_Product-Operation-Guide.pdf
72
+ ├── SI-07_Maintenance-Documentation.pdf
73
+ └── supporting/
74
+ ├── architecture/ → SI.2
75
+ ├── database/ → SI.2
76
+ ├── api/ → SI.2 (interfaces)
77
+ ├── operations/ → SI.6 / SI.7
78
+ └── source-code/ → SI Software Components
79
+ ```
80
+
81
+ Traceability: architecture/database/api → **SI.2 Software Design**; operations →
82
+ **SI.6 Product Operation Guide** + **SI.7 Maintenance**; source-code → **SI Software
83
+ Components**. PM acceptance/sign-off rows stay open for the customer.
84
+
85
+ ## Delivery email shape (Thai default)
86
+
87
+ - Subject: `ส่งมอบชุด Deliverables โครงการ [PROJECT]`
88
+ - From Code Passion → customer team. One paragraph: package attached. One: contents
89
+ overview. One: PDFs for reading + Markdown sources retained. Note where the source
90
+ snapshot lives. Close with support availability. Leave `[ชื่อผู้รับ]` / `[ช่องทางติดต่อ]` blanks.
91
+
92
+ ## Dependency notes
93
+
94
+ - PDF rendering needs a Chromium-family browser (Brave/Chrome/Edge/Chromium). Never auto-
95
+ install heavy deps (puppeteer, pandoc, LaTeX) without asking the user first.
96
+ - `data-dictionary.cjs` needs `mysql2` resolvable from a project that has it.
97
+ - Mermaid + marked load from CDN → diagram rendering needs internet.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: c9n-deliverables
3
+ description: Generate a customer-facing delivery/handover package FROM a codebase — architecture docs, ER/data-dictionary, API reference, all rendered to PDF, organized, security-scanned, zipped, with a delivery email. Use when handing a project off to a customer or another team, preparing deliverables, "delivery package", "handover", "ส่งมอบงาน", or when asked to document a codebase for an external recipient. Complements deliverables-packager (this generates docs from code + organizes; that packages existing docs).
4
+ ---
5
+
6
+ Repeatable customer project delivery for the Code Passion team. Turns a codebase into a
7
+ clean, customer-ready documentation package. Fully generic — adapt to whatever stack the
8
+ project uses. Bundled `scripts/` are proven references; read and adapt them per project.
9
+
10
+ ## Four mandatory gates
11
+
12
+ These ALWAYS run, in order — never skip them:
13
+
14
+ 1. **Clarify-first** — interview before generating anything (see Workflow step 1).
15
+ 2. **Security/sanitize** — never ship secrets. Before packaging: scan for and exclude
16
+ `.env`, credentials, service-account JSON, `*.pem`/`*.key`; sanitize real env files to
17
+ `.env.example` (names + descriptions, no values); strip `.DS_Store`. If you copied any
18
+ credential into a build dir to make tooling work, delete it before finishing.
19
+ 3. **Verify rendered output** — after creating PDFs, screenshot each (headless browser
20
+ `--screenshot`) and visually confirm. Catches illegible/empty diagrams and truncation.
21
+ Do not trust `file`'s page count — use `mdls -name kMDItemNumberOfPages` on macOS.
22
+ 4. **Delivery email** — always draft one. Thai for Thai customer teams, Code Passion
23
+ framing (from Code Passion → client). Leave recipient/contact as fill-in blanks.
24
+
25
+ ## Workflow
26
+
27
+ 1. **Clarify** (ask, don't assume): audience (customer vs internal); which documents are
28
+ wanted; document language (default: English docs + Thai email); output format (PDF?);
29
+ package structure — **plain docs package (default)** or **ISO/IEC 29110 work-product
30
+ layout** (offer it, see [REFERENCE.md](../c9n-deliverables/REFERENCE.md)); any contractual scope.
31
+ 2. **Inventory** the codebase — detect stack, framework, DB, API doc tooling; decide which
32
+ generators apply. Skip what doesn't fit the stack.
33
+ 3. **Generate** the doc set (only what was requested). Common artifacts and recipes are in
34
+ [REFERENCE.md](../c9n-deliverables/REFERENCE.md): system/architecture overview (+ Mermaid diagrams),
35
+ database design (ER diagram + data dictionary), API reference (export spec → PDF),
36
+ deployment runbook, configuration reference, source snapshot.
37
+ 4. **Render PDFs** with `scripts/md-to-pdf.mjs` (Markdown+Mermaid) and
38
+ `scripts/openapi-to-pdf.mjs` (OpenAPI). Then run gate 3 (verify).
39
+ 5. **Organize** per the chosen structure. Master `README.md` index; for 29110 include a
40
+ supporting-artifact → work-product traceability map.
41
+ 6. **Security gate** (gate 2), then **package**: zip the organized folder directly (or, if a
42
+ dedicated packaging skill such as `deliverables-packager` is installed, hand off to it for
43
+ final folder polish + zip).
44
+ 7. **Delivery email** (gate 4).
45
+
46
+ ## Scripts
47
+
48
+ | Script | Purpose |
49
+ |--------|---------|
50
+ | `scripts/md-to-pdf.mjs <in.md> <out.pdf>` | Markdown → PDF, renders Mermaid. Needs a Chromium-family browser. |
51
+ | `scripts/openapi-to-pdf.mjs <spec.json> <out.pdf> [guide.md]` | OpenAPI → print-friendly PDF (no lazy-load truncation). |
52
+ | `scripts/export-openapi.cjs <out.json>` | TEMPLATE: boot a NestJS app, dump Swagger spec. Adapt to project. |
53
+ | `scripts/data-dictionary.cjs <out.md>` | Data dictionary from live MySQL `INFORMATION_SCHEMA`. |
54
+ | `scripts/snapshot-source.sh <repo> <out.zip>` | Clean `git archive` source snapshot. |
55
+
56
+ ## Gotchas
57
+
58
+ - **Wide Mermaid diagrams** render as an illegible strip at page width. Use `flowchart TB`
59
+ and split large ER diagrams into per-domain groups (portrait aspect ratio).
60
+ - **OpenAPI export**: build first (`nest build`); ts-node mis-emits decorator metadata.
61
+ Booting connects to the DB — use a dev DB, never production.
62
+ - **Redoc/Swagger UI → PDF**: they lazy-render; printing truncates. Use `openapi-to-pdf.mjs`.
63
+ - See [REFERENCE.md](../c9n-deliverables/REFERENCE.md) for the ISO/IEC 29110 layout and per-artifact recipes.
@@ -0,0 +1,49 @@
1
+ // Generate a data dictionary (Markdown) from a live MySQL/MariaDB INFORMATION_SCHEMA.
2
+ // Schema only — no row data. Requires `mysql2` resolvable (run from a project that has it).
3
+ //
4
+ // Usage: node data-dictionary.cjs <output.md>
5
+ // Env (or a .env loaded via dotenv): SQL_HOST SQL_PORT SQL_USERNAME SQL_PASSWORD SQL_DATABASE_1
6
+ // Adjust the env var names below to match the target project's config.
7
+ const mysql = require('mysql2/promise')
8
+ const fs = require('fs')
9
+
10
+ const OUT = process.argv[2]
11
+ if (!OUT) { console.error('usage: node data-dictionary.cjs <output.md>'); process.exit(2) }
12
+ try { require('dotenv').config({ path: process.env.ENV_FILE || '.env' }) } catch {}
13
+
14
+ const cfg = {
15
+ host: process.env.SQL_HOST || process.env.DB_HOST,
16
+ port: +(process.env.SQL_PORT || process.env.DB_PORT || 3306),
17
+ user: process.env.SQL_USERNAME || process.env.DB_USER,
18
+ password: process.env.SQL_PASSWORD || process.env.DB_PASSWORD,
19
+ database: process.env.SQL_DATABASE_1 || process.env.DB_NAME,
20
+ }
21
+ const esc = (s) => String(s ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ')
22
+
23
+ ;(async () => {
24
+ const c = await mysql.createConnection({ ...cfg, connectTimeout: 10000 })
25
+ const [tables] = await c.query(
26
+ `SELECT TABLE_NAME, TABLE_COMMENT,
27
+ (SELECT COUNT(*) FROM information_schema.columns col WHERE col.table_schema=t.table_schema AND col.table_name=t.table_name) COLS
28
+ FROM information_schema.tables t WHERE t.table_schema=? AND t.table_type='BASE TABLE' ORDER BY t.table_name`, [cfg.database])
29
+ const [cols] = await c.query(
30
+ `SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT
31
+ FROM information_schema.columns WHERE table_schema=? ORDER BY TABLE_NAME, ORDINAL_POSITION`, [cfg.database])
32
+ const byTable = {}
33
+ for (const col of cols) (byTable[col.TABLE_NAME] ||= []).push(col)
34
+
35
+ let md = `# Data Dictionary\n\nGenerated from the live database schema (\`INFORMATION_SCHEMA\`). **${tables.length} tables.**\n\n## Tables\n\n`
36
+ for (const t of tables) md += `- [${t.TABLE_NAME}](#${t.TABLE_NAME.toLowerCase()}) (${t.COLS} columns)\n`
37
+ md += `\n---\n\n`
38
+ for (const t of tables) {
39
+ md += `### ${t.TABLE_NAME}\n\n`
40
+ if (t.TABLE_COMMENT) md += `${esc(t.TABLE_COMMENT)}\n\n`
41
+ md += `| Column | Type | Null | Key | Default | Extra | Comment |\n|--------|------|------|-----|---------|-------|--------|\n`
42
+ for (const col of byTable[t.TABLE_NAME] || [])
43
+ md += `| ${esc(col.COLUMN_NAME)} | \`${esc(col.COLUMN_TYPE)}\` | ${col.IS_NULLABLE} | ${esc(col.COLUMN_KEY)} | ${col.COLUMN_DEFAULT === null ? '' : esc(col.COLUMN_DEFAULT)} | ${esc(col.EXTRA)} | ${esc(col.COLUMN_COMMENT)} |\n`
44
+ md += `\n`
45
+ }
46
+ fs.writeFileSync(OUT, md)
47
+ console.log(`wrote ${OUT} — ${tables.length} tables, ${cols.length} columns`)
48
+ await c.end()
49
+ })().catch((e) => { console.error('FAILED:', e.code || e.message); process.exit(1) })
@@ -0,0 +1,35 @@
1
+ // Export the OpenAPI/Swagger spec from a NestJS app WITHOUT starting the HTTP server.
2
+ // This is a TEMPLATE — adapt the DocumentBuilder block to match the project's main.ts.
3
+ //
4
+ // Usage: node export-openapi.cjs <output.json>
5
+ //
6
+ // IMPORTANT notes learned the hard way:
7
+ // - Run against the COMPILED build (`nest build` -> dist), not ts-node. ts-node
8
+ // transpile-only mis-emits decorator metadata and TypeORM entity validation fails.
9
+ // - App bootstrap may require real resources (DB reachable, credential files present).
10
+ // Booting connects to the DB; use a dev DB, never production.
11
+ // - Match enableVersioning() and the DocumentBuilder config to the project's main.ts.
12
+ const { NestFactory } = require('@nestjs/core')
13
+ const { SwaggerModule, DocumentBuilder } = require('@nestjs/swagger')
14
+ const { VersioningType } = require('@nestjs/common')
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ require('reflect-metadata')
18
+ try { require('dotenv').config({ path: process.env.ENV_FILE || '.env' }) } catch {}
19
+
20
+ const OUT = process.argv[2] || 'openapi.json'
21
+ // ADAPT: path to the compiled AppModule
22
+ const { AppModule } = require(path.resolve(process.cwd(), 'dist/src/app.module'))
23
+
24
+ ;(async () => {
25
+ const app = await NestFactory.create(AppModule, { logger: ['error', 'warn'] })
26
+ // ADAPT: copy enableVersioning() from the project's main.ts if it uses versioning
27
+ app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1', prefix: 'api/v' })
28
+ // ADAPT: mirror the project's DocumentBuilder (title, version, auth schemes)
29
+ const config = new DocumentBuilder().setTitle('API').setVersion('1.0').build()
30
+ const document = SwaggerModule.createDocument(app, config)
31
+ fs.writeFileSync(path.resolve(process.cwd(), OUT), JSON.stringify(document, null, 2))
32
+ console.log(`wrote ${OUT} — ${Object.keys(document.paths || {}).length} paths`)
33
+ await app.close()
34
+ process.exit(0)
35
+ })().catch((e) => { console.error('FAILED:', e?.message || e); process.exit(1) })
@@ -0,0 +1,73 @@
1
+ // Render a Markdown file to PDF via a headless Chromium browser.
2
+ // Handles Mermaid code fences (```mermaid) by rendering them to SVG client-side.
3
+ //
4
+ // Usage: node md-to-pdf.mjs <input.md> <output.pdf>
5
+ //
6
+ // No install required if a Chromium-family browser is present (Brave/Chrome/Edge/Chromium).
7
+ // Mermaid + marked load from CDN, so the machine needs internet for diagram rendering.
8
+ //
9
+ // SECURITY: input is expected to be FIRST-PARTY documentation you generated for the
10
+ // delivery (the project's own docs), not untrusted third-party Markdown. Mermaid runs with
11
+ // securityLevel:'strict' (no click handlers / inline HTML in diagrams). Markdown is still
12
+ // rendered with raw HTML allowed — do not feed this untrusted Markdown.
13
+ //
14
+ // NOTE: For very WIDE diagrams (many nodes in one row), prefer `flowchart TB` / vertical
15
+ // ER layouts in the source — wide diagrams scale down to an illegible strip at page width.
16
+ import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, basename } from 'node:path'
19
+ import { execFileSync } from 'node:child_process'
20
+
21
+ const [src, out] = process.argv.slice(2)
22
+ if (!src || !out) { console.error('usage: node md-to-pdf.mjs <input.md> <output.pdf>'); process.exit(2) }
23
+
24
+ const BROWSERS = [
25
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
26
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
27
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
28
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
29
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
30
+ ]
31
+ const browser = BROWSERS.find(existsSync)
32
+ if (!browser) { console.error('No Chromium-family browser found. Install Chrome/Brave/Chromium.'); process.exit(1) }
33
+
34
+ const md = readFileSync(src, 'utf8')
35
+ const html = `<!doctype html><html><head><meta charset="utf-8">
36
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
38
+ <style>
39
+ @page { margin: 18mm 16mm; }
40
+ body { font: 14px/1.6 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; color:#1a1a1a; max-width:820px; margin:0 auto; }
41
+ h1 { font-size:26px; border-bottom:2px solid #222; padding-bottom:6px; }
42
+ h2 { font-size:20px; margin-top:28px; border-bottom:1px solid #ddd; padding-bottom:4px; }
43
+ h3 { font-size:16px; margin-top:20px; }
44
+ table { border-collapse:collapse; width:100%; margin:12px 0; font-size:13px; }
45
+ th,td { border:1px solid #ccc; padding:6px 10px; text-align:left; vertical-align:top; }
46
+ th { background:#f4f4f4; }
47
+ code { background:#f0f0f0; padding:1px 5px; border-radius:3px; font-size:12px; }
48
+ pre { background:#f6f8fa; padding:12px; border-radius:6px; overflow:auto; }
49
+ pre code { background:none; padding:0; }
50
+ .mermaid { text-align:center; margin:16px 0; }
51
+ blockquote { border-left:4px solid #ccc; margin:12px 0; padding:4px 14px; color:#555; }
52
+ </style></head><body>
53
+ <div id="out"></div>
54
+ <script type="text/markdown" id="src">${md.replace(/<\/script>/g, '<\\/script>')}</script>
55
+ <script>
56
+ document.getElementById('out').innerHTML = marked.parse(document.getElementById('src').textContent);
57
+ document.querySelectorAll('code.language-mermaid').forEach(c => {
58
+ const d = document.createElement('div'); d.className='mermaid'; d.textContent=c.textContent;
59
+ c.closest('pre').replaceWith(d);
60
+ });
61
+ mermaid.initialize({ startOnLoad:false, securityLevel:'strict' });
62
+ mermaid.run();
63
+ </script></body></html>`
64
+
65
+ const tmp = mkdtempSync(join(tmpdir(), 'md2pdf-'))
66
+ const htmlPath = join(tmp, basename(src) + '.html')
67
+ writeFileSync(htmlPath, html)
68
+ execFileSync(browser, [
69
+ '--headless', '--disable-gpu', '--no-pdf-header-footer',
70
+ '--run-all-compositor-stages-before-draw', '--virtual-time-budget=20000',
71
+ `--print-to-pdf=${out}`, `file://${htmlPath}`,
72
+ ], { stdio: 'inherit' })
73
+ console.log('wrote', out)
@@ -0,0 +1,78 @@
1
+ // Render an OpenAPI 3 spec to a print-friendly PDF (no lazy loading — full content,
2
+ // unlike Redoc/Swagger UI which lazy-render and truncate when printed).
3
+ //
4
+ // Usage: node openapi-to-pdf.mjs <openapi.json> <output.pdf> [guide.md]
5
+ // guide.md (optional) is prepended as an intro section before the endpoint reference.
6
+ //
7
+ // SECURITY: the spec and guide.md are expected to be FIRST-PARTY artifacts you generated
8
+ // for the delivery, not untrusted input. The guide Markdown is rendered with raw HTML
9
+ // allowed — do not pass an untrusted guide file.
10
+ import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { join } from 'node:path'
13
+ import { execFileSync } from 'node:child_process'
14
+
15
+ const [specPath, out, guidePath] = process.argv.slice(2)
16
+ if (!specPath || !out) { console.error('usage: node openapi-to-pdf.mjs <openapi.json> <output.pdf> [guide.md]'); process.exit(2) }
17
+
18
+ const BROWSERS = [
19
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
20
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
21
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
22
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
23
+ ]
24
+ const browser = BROWSERS.find(existsSync)
25
+ if (!browser) { console.error('No Chromium-family browser found.'); process.exit(1) }
26
+
27
+ const spec = JSON.parse(readFileSync(specPath, 'utf8'))
28
+ const guideMd = guidePath && existsSync(guidePath) ? readFileSync(guidePath, 'utf8').replace(/^# .*\r?\n/, '') : ''
29
+ const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]))
30
+ const refName = (r) => (r?.$ref ? r.$ref.split('/').pop() : null)
31
+ const schemaType = (s) => !s ? '' : s.$ref ? refName(s) : s.type === 'array' ? `${schemaType(s.items)}[]` : s.enum ? `enum(${s.enum.map(esc).join(', ')})` : s.format ? `${s.type}<${s.format}>` : (s.type || 'object')
32
+
33
+ const methods = ['get', 'post', 'put', 'patch', 'delete']
34
+ const groups = {}
35
+ for (const [p, item] of Object.entries(spec.paths || {}))
36
+ for (const m of methods) { const op = item[m]; if (!op) continue; const tag = (op.tags && op.tags[0]) || 'default'; (groups[tag] ||= []).push({ m, p, op }) }
37
+
38
+ const pTable = (ps = []) => !ps.length ? '' : `<table class="p"><thead><tr><th>Name</th><th>In</th><th>Req</th><th>Type</th></tr></thead><tbody>${ps.map((pa) => `<tr><td><code>${esc(pa.name)}</code></td><td>${esc(pa.in)}</td><td>${pa.required ? 'yes' : 'no'}</td><td>${esc(schemaType(pa.schema))}</td></tr>`).join('')}</tbody></table>`
39
+ const bBlock = (rb) => { if (!rb) return ''; const j = rb.content && (rb.content['application/json'] || Object.values(rb.content)[0]); return `<div class="body"><span class="lbl">Request body${rb.required ? ' (required)' : ''}:</span> <code>${esc(j ? schemaType(j.schema) : 'object')}</code></div>` }
40
+ const rBlock = (r = {}) => { const rows = Object.entries(r).map(([c, v]) => `<tr><td><code>${esc(c)}</code></td><td>${esc(v.description || '')}</td></tr>`).join(''); return rows ? `<table class="p"><thead><tr><th>Status</th><th>Description</th></tr></thead><tbody>${rows}</tbody></table>` : '' }
41
+
42
+ let body = ''
43
+ for (const tag of Object.keys(groups).sort()) {
44
+ body += `<h2>${esc(tag)}</h2>`
45
+ for (const { m, p, op } of groups[tag])
46
+ body += `<div class="op"><div class="line"><span class="m ${m}">${m.toUpperCase()}</span><code class="path">${esc(p)}</code></div>${op.summary ? `<div class="sum">${esc(op.summary)}</div>` : ''}${op.description ? `<div class="desc">${esc(op.description)}</div>` : ''}${pTable(op.parameters)}${bBlock(op.requestBody)}${rBlock(op.responses)}</div>`
47
+ }
48
+ const opCount = Object.values(spec.paths || {}).reduce((n, it) => n + methods.filter((m) => it[m]).length, 0)
49
+
50
+ const html = `<!doctype html><html><head><meta charset="utf-8"><style>
51
+ @page { margin: 16mm 14mm; }
52
+ body { font: 12px/1.5 -apple-system,Segoe UI,Roboto,Arial,sans-serif; color:#1a1a1a; max-width:860px; margin:0 auto; }
53
+ h1 { font-size:24px; border-bottom:2px solid #222; padding-bottom:6px; }
54
+ h2 { font-size:18px; margin:26px 0 10px; border-bottom:1px solid #ddd; padding-bottom:4px; break-after:avoid; }
55
+ .op { border:1px solid #e3e3e3; border-radius:6px; padding:10px 12px; margin:10px 0; break-inside:avoid; }
56
+ .line { display:flex; align-items:center; gap:8px; }
57
+ .m { color:#fff; font-weight:700; font-size:11px; padding:2px 8px; border-radius:4px; }
58
+ .m.get{background:#2e8b57}.m.post{background:#1f6feb}.m.put{background:#b8860b}.m.patch{background:#8250df}.m.delete{background:#cf222e}
59
+ .sum { font-weight:600; margin:6px 0 2px; } .desc { color:#555; margin:2px 0; }
60
+ table.p { border-collapse:collapse; width:100%; margin:8px 0; font-size:11px; }
61
+ table.p th, table.p td { border:1px solid #ddd; padding:4px 8px; text-align:left; } table.p th { background:#f5f5f5; }
62
+ .body { margin:6px 0; } .lbl { color:#555; } code { background:#f0f0f0; padding:1px 4px; border-radius:3px; }
63
+ .meta { color:#555; margin-bottom:18px; } .section-break { break-before:page; }
64
+ .guide table { border-collapse:collapse; width:100%; margin:8px 0; font-size:12px; }
65
+ .guide th, .guide td { border:1px solid #ccc; padding:5px 9px; text-align:left; } .guide th { background:#f4f4f4; }
66
+ .guide blockquote { border-left:4px solid #ccc; margin:10px 0; padding:4px 14px; color:#555; }
67
+ </style>${guideMd ? '<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>' : ''}</head><body>
68
+ <h1>${esc(spec.info?.title || 'API')} — API Documentation</h1>
69
+ <div class="meta">Version ${esc(spec.info?.version || '')} · OpenAPI ${esc(spec.openapi || '')} · ${Object.keys(spec.paths || {}).length} paths · ${opCount} operations</div>
70
+ ${guideMd ? `<div class="guide" id="guide"></div><script type="text/markdown" id="g">${guideMd.replace(/<\/script>/g, '<\\/script>')}</script><script>document.getElementById('guide').innerHTML=marked.parse(document.getElementById('g').textContent)</script><h1 class="section-break">Endpoint Reference</h1>` : ''}
71
+ ${body}
72
+ </body></html>`
73
+
74
+ const tmp = mkdtempSync(join(tmpdir(), 'oa2pdf-'))
75
+ const htmlPath = join(tmp, 'api.html')
76
+ writeFileSync(htmlPath, html)
77
+ execFileSync(browser, ['--headless', '--disable-gpu', '--no-pdf-header-footer', '--run-all-compositor-stages-before-draw', '--virtual-time-budget=15000', `--print-to-pdf=${out}`, `file://${htmlPath}`], { stdio: 'inherit' })
78
+ console.log('wrote', out)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Produce a clean source-code snapshot using git archive.
3
+ # git archive only includes tracked files — node_modules, .env, dist, secrets are
4
+ # excluded automatically (assuming they are gitignored / untracked).
5
+ #
6
+ # Usage: snapshot-source.sh <repo-root> <output.zip>
7
+ set -euo pipefail
8
+
9
+ REPO_ROOT="${1:-$PWD}"
10
+ OUT="${2:?usage: snapshot-source.sh <repo-root> <output.zip>}"
11
+
12
+ # Resolve a relative <output.zip> against the caller's cwd, then use `git -C` so we
13
+ # never change the caller's working directory. (A bare relative --output under
14
+ # `git -C`/`cd` would surprisingly land inside the repo root.)
15
+ case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
16
+
17
+ if [ ! -d "$REPO_ROOT/.git" ]; then
18
+ echo "Error: $REPO_ROOT is not a git repository." >&2
19
+ exit 1
20
+ fi
21
+
22
+ git -C "$REPO_ROOT" archive --format=zip --output "$OUT" HEAD
23
+ echo "Source snapshot written to: $OUT"