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 +8 -6
- package/README.md +8 -6
- package/bin/ppt-agent.js +86 -1
- package/package.json +6 -2
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/design-gate.js +241 -0
- package/scripts/html2png.js +246 -0
- package/scripts/install-runtime.js +216 -0
- package/skills/slides-grab/SKILL.md +14 -12
- package/skills/slides-grab/references/presentation-workflow-reference.md +1 -1
- package/skills/slides-grab-card-news/SKILL.md +1 -1
- package/skills/slides-grab-design/SKILL.md +10 -6
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +3 -3
- package/skills/slides-grab-design/references/design-system-full.md +4 -4
- package/skills/slides-grab-export/SKILL.md +5 -4
- package/skills/slides-grab-export/references/pptx-skill-reference.md +7 -42
- package/skills/slides-grab-plan/SKILL.md +3 -3
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +14 -14
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +294 -0
- package/src/design-styles.js +19 -2
- package/src/editor/codex-edit.js +26 -1
- package/src/editor/editor.html +1 -1
- package/src/editor/js/model-registry.js +1 -1
- package/templates/design-styles/README.md +2 -1
- package/templates/design-styles/preview.html +1088 -6
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
|
|
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`, `
|
|
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 # 번들된
|
|
94
|
-
slides-grab preview-styles #
|
|
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개
|
|
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
|
|
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`, `
|
|
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
|
|
102
|
-
slides-grab preview-styles # Open the
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|