slides-grab 1.3.0 → 1.3.1

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/README-ko.md CHANGED
@@ -54,11 +54,13 @@ CLI와 공유 에이전트 스킬만 사용하려면 npm 패키지를 설치하
54
54
  ```bash
55
55
  npm install slides-grab
56
56
  npx playwright install chromium
57
- npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
57
+ npx slides-grab install-skills --target all --scope user
58
58
  ```
59
59
 
60
60
  이 방법은 일반적인 사용에 충분합니다. slides-grab 자체를 수정하거나 기여하려는 경우에만 저장소를 클론하세요.
61
61
 
62
+ 이 명령은 같은 공유 Agent Skills와 얇은 런타임 adapter를 Codex 런타임과 Claude Code 런타임 위치에 모두 설치합니다. 패키지된 워크플로우는 번들된 `SKILL.md`, `references/`, `slides-grab` CLI만 사용합니다. `slides-grab pdf`, `slides-grab convert`, `slides-grab figma` export는 현재 슬라이드 파일에 대한 최신 `slides-grab design-gate` `proceed` 기록이 없으면 차단됩니다.
63
+
62
64
  ## 왜 slides-grab인가요?
63
65
 
64
66
  많은 AI 도구가 슬라이드 HTML을 생성하지만, 사용자가 화면에서 **수정하고 싶은 부분을 직접 가리키고** 그 자리에서 반복 편집할 수 있게 해 주는 도구는 드뭅니다. slides-grab은 다음 흐름을 제공합니다.
@@ -72,7 +74,7 @@ npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --cop
72
74
 
73
75
  워크플로 명령은 `--slides-dir <path>`를 지원하며 기본값은 `slides`입니다.
74
76
 
75
- 새 클론에서는 `--help`, `list-templates`, `list-styles`, `preview-styles` 같은 탐색 명령은 덱 없이도 동작합니다. `edit`, `build-viewer`, `validate`, `convert`, `pdf`는 `slide-*.html` 파일이 들어 있는 슬라이드 작업공간이 필요합니다.
77
+ 새 클론에서는 `--help`, `list-templates`, `list-styles`, `preview-styles` 같은 탐색 명령은 덱 없이도 동작합니다. `edit`, `build-viewer`, `validate`, `png`, `design-gate`는 `slide-*.html` 파일이 들어 있는 슬라이드 작업공간이 필요하고, `convert`, `pdf`, `figma`는 추가로 최신 `Proceed` design gate가 필요합니다.
76
78
 
77
79
  ```bash
78
80
  slides-grab edit # 시각적 슬라이드 편집기 실행
@@ -90,13 +92,13 @@ slides-grab image --prompt "..." # 로컬 슬라이드 이미지 생성
90
92
  slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # yt-dlp로 동영상 에셋 다운로드
91
93
  slides-grab tldraw # .tldr 다이어그램을 슬라이드 크기의 로컬 SVG로 렌더링
92
94
  slides-grab list-templates # 사용 가능한 슬라이드 템플릿 표시
93
- slides-grab list-styles # 번들된 35개 디자인 스타일 표시
94
- slides-grab preview-styles # 35개 스타일 미리보기 갤러리를 브라우저에서 열기
95
+ slides-grab list-styles # 번들된 95개 디자인 스타일 표시
96
+ slides-grab preview-styles # 95개 스타일 미리보기 갤러리를 브라우저에서 열기
95
97
  ```
96
98
 
97
99
  ## 디자인 스타일 모음
98
100
 
99
- slides-grab은 [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles)에서 파생된 30개 스타일과 slides-grab 고유 스타일 5개, 총 35개 디자인 스타일을 제공합니다. 에이전트에게 특정 스타일을 요청하거나 완전히 커스텀 디자인을 요청할 수 있습니다.
101
+ slides-grab은 [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles)에서 파생된 30개 스타일, slides-grab 고유 스타일 5개, [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity)에서 파생된 PPT 팩 60개를 포함해 95개 디자인 스타일을 제공합니다. 에이전트에게 특정 스타일을 요청하거나 완전히 커스텀 디자인을 요청할 수 있습니다.
100
102
 
101
103
  ```bash
102
104
  slides-grab list-styles
@@ -229,7 +231,7 @@ npm install slides-grab
229
231
  Vercel Agent Skills로 공유 에이전트 스킬을 설치하려면 다음을 실행하세요.
230
232
 
231
233
  ```bash
232
- npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
234
+ npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy --full-depth
233
235
  ```
234
236
 
235
237
  이 npm 설치 경로는 일반적인 사용에 충분합니다. slides-grab 자체를 수정하거나 기여하려는 경우에만 저장소를 클론하세요.
package/README.md CHANGED
@@ -64,9 +64,11 @@ npm ci && npx playwright install chromium
64
64
  ```bash
65
65
  npm install slides-grab
66
66
  npx playwright install chromium
67
- npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
67
+ npx slides-grab install-skills --target all --scope user
68
68
  ```
69
69
 
70
+ This installs the same shared Agent Skills plus lightweight runtime adapters into Codex and Claude Code locations. The packaged workflow relies on bundled `SKILL.md` files, bundled `references/`, and the `slides-grab` CLI. Exports through `slides-grab pdf`, `slides-grab convert`, and `slides-grab figma` are blocked until `slides-grab design-gate` records a fresh `proceed` receipt for the current slide files.
71
+
70
72
  ## Why This Project?
71
73
 
72
74
  There are many AI tools that generate slide HTML. Almost none let you **visually point at what you want changed** and iterate in-place. slides-grab fills that gap:
@@ -80,7 +82,7 @@ There are many AI tools that generate slide HTML. Almost none let you **visually
80
82
 
81
83
  Workflow commands support `--slides-dir <path>` (default: `slides`).
82
84
 
83
- On a fresh clone, the discovery commands (`--help`, `list-templates`, `list-styles`, and `preview-styles`) work without a deck. `edit`, `build-viewer`, `validate`, `convert`, and `pdf` require an existing slides workspace containing `slide-*.html`.
85
+ On a fresh clone, the discovery commands (`--help`, `list-templates`, `list-styles`, and `preview-styles`) work without a deck. `edit`, `build-viewer`, `validate`, `png`, and `design-gate` require an existing slides workspace containing `slide-*.html`; `convert`, `pdf`, and `figma` additionally require a fresh `Proceed` design gate.
84
86
 
85
87
  ```bash
86
88
  slides-grab edit # Launch visual slide editor
@@ -98,13 +100,13 @@ slides-grab image --prompt "..." # Generate a local slide image with god-tibo
98
100
  slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # Download a local video asset with yt-dlp
99
101
  slides-grab tldraw # Render a .tldr diagram into a slide-sized local SVG asset
100
102
  slides-grab list-templates # Show available slide templates
101
- slides-grab list-styles # Show 35 bundled design styles (browse, preview, select)
102
- slides-grab preview-styles # Open the 35-style visual gallery in browser
103
+ slides-grab list-styles # Show 95 bundled design styles (browse, preview, select)
104
+ slides-grab preview-styles # Open the 95-style visual gallery in browser
103
105
  ```
104
106
 
105
107
  ## Design Style Collections
106
108
 
107
- slides-grab bundles 35 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) plus 5 slides-grab originals. Agents can also create fully custom designs beyond the bundled collection.
109
+ slides-grab bundles 95 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles), 5 slides-grab originals, and 60 PPT packs derived from [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity). Agents can also create fully custom designs beyond the bundled collection.
108
110
 
109
111
  ```bash
110
112
  slides-grab list-styles # Browse the catalog
@@ -237,7 +239,7 @@ npm install slides-grab
237
239
  Install shared agent skills with Vercel Agent Skills:
238
240
 
239
241
  ```bash
240
- npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy
242
+ npx skills add ./node_modules/slides-grab -g -a codex -a claude-code --yes --copy --full-depth
241
243
  ```
242
244
 
243
245
  This npm-install path is enough for normal usage. Clone the repo only when you want to modify or contribute to `slides-grab` itself.
package/bin/ppt-agent.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getFigmaImportCaveats,
10
10
  getFigmaManualImportInstructions,
11
11
  } from '../src/figma.js';
12
+ import { assertDesignGateReady } from '../src/design-gate-state.js';
12
13
 
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
15
  const packageRoot = resolve(__dirname, '..');
@@ -64,6 +65,17 @@ async function runCommand(relativePath, args = []) {
64
65
  }
65
66
  }
66
67
 
68
+ async function ensureDesignGateForExport(slidesDir, label) {
69
+ try {
70
+ await assertDesignGateReady(resolve(process.cwd(), slidesDir), { label });
71
+ } catch (error) {
72
+ console.error(`[slides-grab] ${error.message}`);
73
+ process.exitCode = 1;
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+
67
79
  function collectRepeatedOption(value, previous = []) {
68
80
  return [...previous, value];
69
81
  }
@@ -109,6 +121,66 @@ program
109
121
  await runCommand('scripts/validate-slides.js', args);
110
122
  });
111
123
 
124
+ program
125
+ .command('design-gate')
126
+ .description('Record the required visual QA gate evidence before export')
127
+ .option('--slides-dir <path>', 'Slide directory', 'slides')
128
+ .option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
129
+ .option('--resolution <preset>', 'PNG evidence resolution preset: 720p, 1080p, 1440p, 2160p, or 4k', '2160p')
130
+ .requiredOption('--verdict <verdict>', 'Gate verdict: proceed, revise, or rethink')
131
+ .requiredOption('--pass-a-report <path>', 'Pass A review report file')
132
+ .requiredOption('--pass-b-report <path>', 'Pass B review report file')
133
+ .option('--output-dir <path>', 'PNG evidence directory (default: <slides-dir>/.slides-grab/gate-preview)')
134
+ .action(async (options = {}) => {
135
+ const args = [
136
+ '--slides-dir',
137
+ options.slidesDir,
138
+ '--slide-mode',
139
+ options.slideMode,
140
+ '--resolution',
141
+ options.resolution,
142
+ '--verdict',
143
+ options.verdict,
144
+ '--pass-a-report',
145
+ options.passAReport,
146
+ '--pass-b-report',
147
+ options.passBReport,
148
+ ];
149
+ if (options.outputDir) {
150
+ args.push('--output-dir', String(options.outputDir));
151
+ }
152
+ await runCommand('scripts/design-gate.js', args);
153
+ });
154
+
155
+ function registerInstallSkillsCommand(commandName) {
156
+ program
157
+ .command(commandName)
158
+ .description('Install slides-grab skills and lightweight runtime adapters for Codex and Claude Code')
159
+ .option('--target <target>', 'Runtime target: all, codex, or claude-code', 'all')
160
+ .option('--runtime <target>', 'Alias for --target')
161
+ .option('--scope <scope>', 'Install scope: project or user', 'project')
162
+ .option('--project-dir <path>', 'Project directory for project scope')
163
+ .option('--target-root <path>', 'Root directory for user-style installs')
164
+ .option('--dry-run', 'Print planned writes without copying')
165
+ .option('--json', 'Print JSON result')
166
+ .action(async (options = {}) => {
167
+ const args = [
168
+ '--target',
169
+ String(options.runtime || options.target),
170
+ '--scope',
171
+ String(options.scope),
172
+ ];
173
+ if (options.projectDir) args.push('--project-dir', String(options.projectDir));
174
+ if (options.targetRoot) args.push('--target-root', String(options.targetRoot));
175
+ if (options.dryRun) args.push('--dry-run');
176
+ if (options.json) args.push('--json');
177
+ await runCommand('scripts/install-runtime.js', args);
178
+ });
179
+ }
180
+
181
+ registerInstallSkillsCommand('install-skills');
182
+ registerInstallSkillsCommand('install-runtime');
183
+
112
184
  program
113
185
  .command('convert')
114
186
  .description('Convert slide HTML files to experimental / unstable PPTX')
@@ -117,6 +189,7 @@ program
117
189
  .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
118
190
  .option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
119
191
  .action(async (options = {}) => {
192
+ if (!(await ensureDesignGateForExport(options.slidesDir, 'PPTX export'))) return;
120
193
  const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
121
194
  if (options.output) {
122
195
  args.push('--output', String(options.output));
@@ -136,6 +209,7 @@ program
136
209
  .option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
137
210
  .option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
138
211
  .action(async (options = {}) => {
212
+ if (!(await ensureDesignGateForExport(options.slidesDir, 'PDF export'))) return;
139
213
  const args = ['--slides-dir', options.slidesDir];
140
214
  if (options.output) {
141
215
  args.push('--output', String(options.output));
@@ -196,6 +270,7 @@ program
196
270
  .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
197
271
  .addHelpText('after', figmaHelpText)
198
272
  .action(async (options = {}) => {
273
+ if (!(await ensureDesignGateForExport(options.slidesDir, 'Figma export'))) return;
199
274
  const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
200
275
  if (options.output) {
201
276
  args.push('--output', String(options.output));
@@ -320,7 +395,7 @@ program
320
395
 
321
396
  program
322
397
  .command('preview-styles')
323
- .description('Print the path to the bundled 35-style visual preview gallery')
398
+ .description('Print the path to the bundled 95-style visual preview gallery')
324
399
  .action(async () => {
325
400
  try {
326
401
  const { getPreviewHtmlPath } = await import('../src/design-styles.js');
@@ -418,6 +493,8 @@ program
418
493
  console.log(`# Bundled style: ${style.title} (${style.id})`);
419
494
  console.log(`Mood: ${style.mood}`);
420
495
  console.log(`Best for: ${style.bestFor}`);
496
+ if (style.source?.repo) console.log(`Source: ${style.source.repo}`);
497
+ if (style.source?.url) console.log(`Source URL: ${style.source.url}`);
421
498
  if (Array.isArray(style.background)) {
422
499
  console.log('\n## Background');
423
500
  for (const b of style.background) console.log(`- ${b}`);
@@ -434,6 +511,14 @@ program
434
511
  console.log('\n## Layout');
435
512
  for (const l of style.layout) console.log(`- ${l}`);
436
513
  }
514
+ if (Array.isArray(style.signature)) {
515
+ console.log('\n## Signature');
516
+ for (const s of style.signature) console.log(`- ${s}`);
517
+ }
518
+ if (Array.isArray(style.avoid)) {
519
+ console.log('\n## Avoid');
520
+ for (const a of style.avoid) console.log(`- ${a}`);
521
+ }
437
522
  }
438
523
  } catch (error) {
439
524
  reportCliError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
5
5
  "license": "MIT",
6
6
  "author": "vkehfdl1",
@@ -39,10 +39,14 @@
39
39
  "scripts/generate-image.js",
40
40
  "src/god-tibo-imagen.js",
41
41
  "scripts/figma-export.js",
42
+ "scripts/design-gate.js",
42
43
  "scripts/html2pdf.js",
44
+ "scripts/html2png.js",
43
45
  "scripts/html2pptx.js",
46
+ "scripts/install-runtime.js",
44
47
  "scripts/render-tldraw.js",
45
48
  "scripts/validate-slides.js",
49
+ "runtimes/",
46
50
  "skills/",
47
51
  "src/",
48
52
  "templates/",
@@ -56,7 +60,7 @@
56
60
  "build:showcase": "node showcase/scripts/build-manifest.js",
57
61
  "validate": "node scripts/validate-slides.js",
58
62
  "convert": "node convert.cjs",
59
- "test": "node --test --test-concurrency=1 tests/docs/readme-ko.test.js tests/design/design-styles.test.js tests/design/design-md-parser.test.js tests/editor/editor-codex-edit.test.js tests/editor/edit-subprocess-abort.test.js tests/editor/editor-server.test.js tests/editor/editor-server-orphan-prevention.test.js tests/editor/editor-model-dispatch.test.js tests/god-tibo/god-tibo.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/viewer/build-viewer.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
63
+ "test": "node --test --test-concurrency=1 tests/docs/readme-ko.test.js tests/design/design-styles.test.js tests/design/design-md-parser.test.js tests/design-gate/design-gate.test.js tests/runtime/install-runtime.test.js tests/editor/editor-codex-edit.test.js tests/editor/edit-subprocess-abort.test.js tests/editor/editor-server.test.js tests/editor/editor-server-orphan-prevention.test.js tests/editor/editor-model-dispatch.test.js tests/god-tibo/god-tibo.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/viewer/build-viewer.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
60
64
  "test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
61
65
  },
62
66
  "dependencies": {
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: design-critic-agent
3
+ description: Run the slides-grab design gate before export.
4
+ tools: Read, Grep, Glob, Bash, Task
5
+ ---
6
+
7
+ Use the canonical gate in `skills/slides-grab-design/references/design-gate.md`.
8
+
9
+ Required workflow:
10
+
11
+ 1. Run `slides-grab validate --slides-dir <slides-dir>`.
12
+ 2. Render evidence with `slides-grab png --slides-dir <slides-dir> --output-dir <slides-dir>/.slides-grab/gate-preview`.
13
+ 3. Produce two read-only review reports:
14
+ - Pass A: System Contract / Constraint Integrity.
15
+ - Pass B: Audience Impact / Expressive Readability.
16
+ Each `Proceed` report must use the CLI-enforced structure from `skills/slides-grab-design/references/design-gate.md`: role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, findings table, and all required checks marked `PASS`.
17
+ 4. If both passes conclude Proceed, record the gate with:
18
+
19
+ ```bash
20
+ slides-grab design-gate --slides-dir <slides-dir> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>
21
+ ```
22
+
23
+ If either pass finds blocking issues, or if `slides-grab design-gate` rejects the reports, fix the slides/reports and repeat from validation and fresh rendered evidence. Do not run `slides-grab pdf`, `slides-grab convert`, or `slides-grab figma` until the CLI gate records `proceed`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: slides-grab-design-critic
3
+ description: Run the slides-grab design gate before export.
4
+ ---
5
+
6
+ Use the canonical gate in `skills/slides-grab-design/references/design-gate.md`.
7
+
8
+ Required workflow:
9
+
10
+ 1. Run `slides-grab validate --slides-dir <slides-dir>`.
11
+ 2. Render evidence with `slides-grab png --slides-dir <slides-dir> --output-dir <slides-dir>/.slides-grab/gate-preview`.
12
+ 3. Produce two read-only review reports:
13
+ - Pass A: System Contract / Constraint Integrity.
14
+ - Pass B: Audience Impact / Expressive Readability.
15
+ Each `Proceed` report must use the CLI-enforced structure from `skills/slides-grab-design/references/design-gate.md`: role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, findings table, and all required checks marked `PASS`.
16
+ 4. If both passes conclude Proceed, record the gate with:
17
+
18
+ ```bash
19
+ slides-grab design-gate --slides-dir <slides-dir> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>
20
+ ```
21
+
22
+ If either pass finds blocking issues, or if `slides-grab design-gate` rejects the reports, fix the slides/reports and repeat from validation and fresh rendered evidence. Do not run `slides-grab pdf`, `slides-grab convert`, or `slides-grab figma` until the CLI gate records `proceed`.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { mkdir, readFile } from 'node:fs/promises';
6
+ import { dirname, join, relative, resolve } from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+
9
+ import { ensureSlidesPassValidation } from './validate-slides.js';
10
+ import { assertProceedReportsComplete } from '../src/design-gate-report.js';
11
+ import {
12
+ buildDesignGatePaths,
13
+ buildDesignGateReport,
14
+ collectFileFingerprints,
15
+ collectSlideFingerprints,
16
+ createDesignGateState,
17
+ findGateSlideFiles,
18
+ normalizeGateVerdict,
19
+ writeDesignGateState,
20
+ } from '../src/design-gate-state.js';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const packageRoot = resolve(__dirname, '..');
24
+ const DEFAULT_SLIDES_DIR = 'slides';
25
+ const DEFAULT_SLIDE_MODE = 'presentation';
26
+ const DEFAULT_RESOLUTION = '2160p';
27
+
28
+ function printUsage() {
29
+ process.stdout.write(
30
+ [
31
+ 'Usage: slides-grab design-gate [options]',
32
+ '',
33
+ 'Options:',
34
+ ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
35
+ ` --slide-mode <mode> Slide mode: presentation|card-news (default: ${DEFAULT_SLIDE_MODE})`,
36
+ ` --resolution <preset> PNG evidence resolution preset (default: ${DEFAULT_RESOLUTION})`,
37
+ ' --verdict <verdict> Gate verdict: proceed|revise|rethink',
38
+ ' --pass-a-report <path> Pass A review report file',
39
+ ' --pass-b-report <path> Pass B review report file',
40
+ ' --output-dir <path> PNG evidence directory (default: <slides-dir>/.slides-grab/gate-preview)',
41
+ ' -h, --help Show this help message',
42
+ ].join('\n'),
43
+ );
44
+ process.stdout.write('\n');
45
+ }
46
+
47
+ function parseArgs(args) {
48
+ const options = {
49
+ slidesDir: DEFAULT_SLIDES_DIR,
50
+ slideMode: DEFAULT_SLIDE_MODE,
51
+ resolution: DEFAULT_RESOLUTION,
52
+ verdict: '',
53
+ passAReport: '',
54
+ passBReport: '',
55
+ outputDir: '',
56
+ help: false,
57
+ };
58
+
59
+ for (let index = 0; index < args.length; index += 1) {
60
+ const arg = args[index];
61
+ if (arg === '-h' || arg === '--help') {
62
+ options.help = true;
63
+ continue;
64
+ }
65
+
66
+ if (ARGUMENT_READERS.has(arg)) {
67
+ options[ARGUMENT_READERS.get(arg)] = readOptionValue(args, index, arg);
68
+ index += 1;
69
+ continue;
70
+ }
71
+
72
+ const equalSignIndex = arg.indexOf('=');
73
+ if (equalSignIndex > -1) {
74
+ const name = arg.slice(0, equalSignIndex);
75
+ const value = arg.slice(equalSignIndex + 1);
76
+ if (ARGUMENT_READERS.has(name)) {
77
+ options[ARGUMENT_READERS.get(name)] = value;
78
+ continue;
79
+ }
80
+ }
81
+
82
+ throw new Error(`Unknown option: ${arg}`);
83
+ }
84
+
85
+ if (options.help) return options;
86
+
87
+ options.slidesDir = requireNonEmpty(options.slidesDir, '--slides-dir');
88
+ options.slideMode = requireNonEmpty(options.slideMode, '--slide-mode');
89
+ options.resolution = requireNonEmpty(options.resolution, '--resolution');
90
+ options.verdict = normalizeGateVerdict(requireNonEmpty(options.verdict, '--verdict'));
91
+ options.passAReport = requireNonEmpty(options.passAReport, '--pass-a-report');
92
+ options.passBReport = requireNonEmpty(options.passBReport, '--pass-b-report');
93
+ if (options.outputDir) options.outputDir = options.outputDir.trim();
94
+ return options;
95
+ }
96
+
97
+ const ARGUMENT_READERS = new Map([
98
+ ['--slides-dir', 'slidesDir'],
99
+ ['--slide-mode', 'slideMode'],
100
+ ['--resolution', 'resolution'],
101
+ ['--verdict', 'verdict'],
102
+ ['--pass-a-report', 'passAReport'],
103
+ ['--pass-b-report', 'passBReport'],
104
+ ['--output-dir', 'outputDir'],
105
+ ]);
106
+
107
+ function readOptionValue(args, index, optionName) {
108
+ const value = args[index + 1];
109
+ if (!value || value.startsWith('-')) {
110
+ throw new Error(`Missing value for ${optionName}.`);
111
+ }
112
+ return value;
113
+ }
114
+
115
+ function requireNonEmpty(value, optionName) {
116
+ const trimmed = String(value || '').trim();
117
+ if (!trimmed) throw new Error(`${optionName} must be a non-empty string.`);
118
+ return trimmed;
119
+ }
120
+
121
+ async function renderGateEvidence(options, outputDir) {
122
+ await mkdir(outputDir, { recursive: true });
123
+ const scriptPath = resolve(packageRoot, 'scripts/html2png.js');
124
+ const args = [
125
+ scriptPath,
126
+ '--slides-dir',
127
+ options.slidesDir,
128
+ '--output-dir',
129
+ outputDir,
130
+ '--slide-mode',
131
+ options.slideMode,
132
+ '--resolution',
133
+ options.resolution,
134
+ ];
135
+
136
+ await new Promise((resolvePromise, rejectPromise) => {
137
+ const child = spawn(process.execPath, args, {
138
+ cwd: process.cwd(),
139
+ stdio: 'inherit',
140
+ env: { ...process.env, PPT_AGENT_PACKAGE_ROOT: packageRoot },
141
+ });
142
+ child.on('error', rejectPromise);
143
+ child.on('close', (code, signal) => {
144
+ if (signal) {
145
+ rejectPromise(new Error(`PNG evidence render terminated by signal ${signal}.`));
146
+ return;
147
+ }
148
+ if (code !== 0) {
149
+ rejectPromise(new Error(`PNG evidence render failed with exit code ${code}.`));
150
+ return;
151
+ }
152
+ resolvePromise();
153
+ });
154
+ });
155
+ }
156
+
157
+ async function readEvidenceReport(filePath, label) {
158
+ const resolvedPath = resolve(process.cwd(), filePath);
159
+ if (!existsSync(resolvedPath)) {
160
+ throw new Error(`${label} report not found: ${resolvedPath}`);
161
+ }
162
+ const report = (await readFile(resolvedPath, 'utf-8')).trim();
163
+ if (!report) {
164
+ throw new Error(`${label} report is empty: ${resolvedPath}`);
165
+ }
166
+ return { resolvedPath, report };
167
+ }
168
+
169
+ export async function main(argv = process.argv.slice(2)) {
170
+ const options = parseArgs(argv);
171
+ if (options.help) {
172
+ printUsage();
173
+ return;
174
+ }
175
+
176
+ const paths = buildDesignGatePaths(options.slidesDir);
177
+ const previewDir = options.outputDir ? resolve(process.cwd(), options.outputDir) : paths.previewDir;
178
+ await ensureSlidesPassValidation(paths.slidesDir, {
179
+ exportLabel: 'design gate',
180
+ slideMode: options.slideMode,
181
+ shouldBlockIssue: () => true,
182
+ });
183
+ await renderGateEvidence(options, previewDir);
184
+
185
+ const passA = await readEvidenceReport(options.passAReport, 'Pass A');
186
+ const passB = await readEvidenceReport(options.passBReport, 'Pass B');
187
+ let gateValidation = { status: options.verdict === 'proceed' ? 'passed' : 'not-run' };
188
+ const slideFiles = await findGateSlideFiles(paths.slidesDir);
189
+ const slideFingerprints = await collectSlideFingerprints(paths.slidesDir);
190
+ const evidenceFiles = slideFiles.map((fileName) => fileName.replace(/\.html$/i, '.png'));
191
+ const previewRelativeDir = relative(paths.slidesDir, previewDir);
192
+ const previewFingerprintFiles = evidenceFiles.map((fileName) => join(previewRelativeDir, fileName));
193
+ const passReportFingerprintFiles = [
194
+ relative(paths.slidesDir, passA.resolvedPath),
195
+ relative(paths.slidesDir, passB.resolvedPath),
196
+ ];
197
+
198
+ if (options.verdict === 'proceed') {
199
+ gateValidation = assertProceedReportsComplete({
200
+ passAReport: passA.report,
201
+ passBReport: passB.report,
202
+ evidenceFiles,
203
+ slideFingerprints,
204
+ });
205
+ }
206
+ const state = await createDesignGateState({
207
+ slidesDir: paths.slidesDir,
208
+ slideMode: options.slideMode,
209
+ resolution: options.resolution,
210
+ verdict: options.verdict,
211
+ previewDir,
212
+ reportPath: paths.reportPath,
213
+ passA: { reportPath: passA.resolvedPath, summary: firstLine(passA.report), ...gateValidation.passA },
214
+ passB: { reportPath: passB.resolvedPath, summary: firstLine(passB.report), ...gateValidation.passB },
215
+ gateValidation,
216
+ passReportFingerprints: await collectFileFingerprints(paths.slidesDir, passReportFingerprintFiles),
217
+ previewFingerprints: await collectFileFingerprints(paths.slidesDir, previewFingerprintFiles),
218
+ });
219
+ const report = buildDesignGateReport(state, passA.report, passB.report);
220
+ await writeDesignGateState(paths.slidesDir, state, report);
221
+
222
+ process.stdout.write(`Design gate recorded: ${state.verdict}\n`);
223
+ process.stdout.write(`Evidence PNGs: ${previewDir}\n`);
224
+ process.stdout.write(`Gate report: ${paths.reportPath}\n`);
225
+ process.stdout.write(`Gate state: ${paths.statePath}\n`);
226
+
227
+ if (state.verdict !== 'proceed') {
228
+ process.exitCode = 1;
229
+ }
230
+ }
231
+
232
+ function firstLine(value) {
233
+ return value.split(/\r?\n/).find((line) => line.trim())?.trim() || '';
234
+ }
235
+
236
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
237
+ main().catch((error) => {
238
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
239
+ process.exit(1);
240
+ });
241
+ }