@erclx/aitk 0.15.0 → 0.17.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.15.0",
4
+ "version": "0.17.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
package/docs/agents.md CHANGED
@@ -80,6 +80,7 @@ Full help: `aitk <command> --help`.
80
80
  | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
81
81
  | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
82
82
  | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
83
+ | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
83
84
 
84
85
  ### Domain commands
85
86
 
@@ -301,6 +302,29 @@ aitk tasks archive --pull-request 673 --json | jq -r 'if .ok then .task else .re
301
302
 
302
303
  For the board format, the `Pull request:` line, and the archive rules, see `.claude/standards/tasks.md`.
303
304
 
305
+ ## Capture
306
+
307
+ `aitk capture [source]` renders HTML capture sources to PNG, which is how a committed documentation image regenerates from its committed source. The source defaults to `assets/`, where a directory expands to every `.html` directly inside it, so adding a capture means dropping a file beside the first one and running the same bare command.
308
+
309
+ ```bash
310
+ aitk capture
311
+ aitk capture assets/install.html
312
+ aitk capture assets --out .claude/review/captures
313
+ ```
314
+
315
+ | Option | Behavior |
316
+ | ------------------ | ------------------------------------------------- |
317
+ | `--out <dir>` | Write every PNG here instead of beside its source |
318
+ | `--selector <sel>` | Element to capture (default: `.window`) |
319
+
320
+ Each source renders at `deviceScaleFactor` 2 with a transparent background, and the success line reports the pixel dimensions the element wrapped to. Size is reported and never asserted. The height of a terminal frame is whatever its text wrapped to at a fixed width, so pinning that number would harden an accident.
321
+
322
+ What is asserted is the font. The command reads the first family the captured element declares and fails when the browser did not resolve it, because a fallback face rewraps the block and silently changes the output. Sources therefore name a real font rather than relying on `monospace`. A source that cannot render reports its own line and exits 1 without dropping the rest of the batch.
323
+
324
+ The browser binary installs separately from the package. A fresh clone runs `bunx playwright install chromium` once, and a run that cannot launch one reports the engine's own remediation inside the frame and exits 1 rather than escaping as a stack trace.
325
+
326
+ The command is toolkit-only. Its render module holds every browser reference in the toolkit and `files` in `package.json` excludes it, so an installed `aitk` carries the command, reports it as absent on one line, and exits 1. Every other command is unaffected, which is the reason the browser import sits behind a dynamic import rather than at a command's top level.
327
+
304
328
  ## Comments
305
329
 
306
330
  `aitk comments scan [path]` reports comment density for a tree, split by language and by comment kind. It is the only command that parses the target's own source, so its scope stays deliberately narrow: TypeScript and bash, line-oriented, no AST.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.15.0",
4
+ "version": "0.17.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "tsconfig.json",
20
20
  "!scripts/sandbox",
21
21
  "!scripts/eval",
22
+ "!src/capture",
22
23
  "!**/*.test.ts"
23
24
  ],
24
25
  "publishConfig": {
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import { register as indexes } from '@/commands/indexes'
14
14
  import { register as docs } from '@/commands/docs'
15
15
  import { register as design } from '@/commands/design'
16
16
  import { register as slides } from '@/commands/slides'
17
+ import { register as capture } from '@/commands/capture'
17
18
  import { register as feedback } from '@/commands/feedback'
18
19
  import { register as transcripts } from '@/commands/transcripts'
19
20
  import { register as tasks } from '@/commands/tasks'
@@ -42,6 +43,7 @@ function showHelp(): void {
42
43
  `${GREY}│${NC} docs [cmd|topic] ${GREY}# Emit toolkit reference docs (list, <topic>)${NC}`,
43
44
  `${GREY}│${NC} design [cmd] ${GREY}# Design system commands (render)${NC}`,
44
45
  `${GREY}│${NC} slides [cmd] ${GREY}# Slide deck commands (render, list)${NC}`,
46
+ `${GREY}│${NC} capture [source] ${GREY}# Render HTML capture sources to PNG${NC}`,
45
47
  `${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .claude/review/${NC}`,
46
48
  `${GREY}│${NC} transcripts <url> ${GREY}# Fetch a YouTube transcript with metadata frontmatter${NC}`,
47
49
  `${GREY}│${NC} tasks [cmd] ${GREY}# Task board commands (archive)${NC}`,
@@ -72,6 +74,7 @@ function showHelp(): void {
72
74
  `${GREY}│${NC} aitk design render`,
73
75
  `${GREY}│${NC} aitk slides render`,
74
76
  `${GREY}│${NC} aitk slides list --json`,
77
+ `${GREY}│${NC} aitk capture assets/install.html`,
75
78
  `${GREY}│${NC} pbpaste | aitk feedback`,
76
79
  `${GREY}│${NC} aitk transcripts https://youtu.be/VIDEO_ID`,
77
80
  `${GREY}│${NC} aitk tasks archive --pull-request 673 --json`,
@@ -107,6 +110,7 @@ indexes(program)
107
110
  docs(program)
108
111
  design(program)
109
112
  slides(program)
113
+ capture(program)
110
114
  feedback(program)
111
115
  transcripts(program)
112
116
  tasks(program)
@@ -0,0 +1,133 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { relative, resolve } from 'node:path'
3
+ import type { Command } from 'commander'
4
+ import { frameError, intro, logError, logInfo, outro, pipeOutput } from '@/ui'
5
+
6
+ const DEFAULT_SOURCE = 'assets'
7
+ const DEFAULT_SELECTOR = '.window'
8
+
9
+ /**
10
+ * Both aliases are type queries rather than imports, so `@/capture/render`
11
+ * never appears in this module's runtime import list.
12
+ */
13
+ type Renderer = typeof import('@/capture/render')
14
+ type CaptureResult = Awaited<ReturnType<Renderer['captureSources']>>[number]
15
+
16
+ /**
17
+ * Holds wiring only. Every browser reference sits behind `loadRenderer`,
18
+ * because `src/cli.ts` imports this module at startup and the render module is
19
+ * excluded from the published package.
20
+ */
21
+ export function register(program: Command): void {
22
+ program
23
+ .command('capture')
24
+ .description('Render HTML capture sources to PNG')
25
+ .argument('[source]', 'HTML file or a directory of them', DEFAULT_SOURCE)
26
+ .option('-o, --out <dir>', 'Output directory, defaults beside the source')
27
+ .option('-s, --selector <selector>', 'Element to capture', DEFAULT_SELECTOR)
28
+ .action(
29
+ async (
30
+ source: string,
31
+ opts: { out?: string; selector: string },
32
+ ): Promise<void> => {
33
+ const renderer = await loadRenderer()
34
+ if (!renderer) {
35
+ frameError(
36
+ 'capture is toolkit-only and is absent from an installed aitk',
37
+ )
38
+ process.exitCode = 1
39
+ return
40
+ }
41
+
42
+ const sourcePath = resolve(process.cwd(), source)
43
+ if (!existsSync(sourcePath)) {
44
+ frameError(`${source} not found`)
45
+ process.exitCode = 1
46
+ return
47
+ }
48
+
49
+ intro('Capture')
50
+ let results: CaptureResult[]
51
+ try {
52
+ results = await renderer.captureSources(sourcePath, {
53
+ selector: opts.selector,
54
+ outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
55
+ })
56
+ } catch (error) {
57
+ reportInFrame(error)
58
+ process.exitCode = 1
59
+ return
60
+ }
61
+
62
+ if (!results.length) {
63
+ logError(`no .html source under ${source}`)
64
+ outro()
65
+ process.exitCode = 1
66
+ return
67
+ }
68
+
69
+ for (const result of results) {
70
+ if (result.status === 'rendered') {
71
+ logInfo(
72
+ `${displayPath(result.pngPath)} ${result.width}x${result.height}`,
73
+ )
74
+ } else {
75
+ logError(`${displayPath(result.htmlPath)}: ${result.reason}`)
76
+ }
77
+ }
78
+ outro()
79
+
80
+ if (results.some((result) => result.status === 'failed')) {
81
+ process.exitCode = 1
82
+ }
83
+ },
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Closes an open frame around a failure the render module raised for the whole
89
+ * run rather than for one source, such as a browser binary that is not
90
+ * installed. The engine's own message is readable, so it is passed through
91
+ * intact instead of being summarized.
92
+ */
93
+ function reportInFrame(error: unknown): void {
94
+ const message = error instanceof Error ? error.message : String(error)
95
+ const [first, ...rest] = message.split('\n')
96
+ logError(first ?? 'capture failed')
97
+ if (rest.length) pipeOutput(rest.join('\n'))
98
+ outro()
99
+ }
100
+
101
+ /**
102
+ * Keeps a path clickable in the operator's terminal. A source outside the
103
+ * project reports absolute, since a relative path to it is a run of `..`
104
+ * segments no editor resolves.
105
+ */
106
+ function displayPath(path: string): string {
107
+ const fromCwd = relative(process.cwd(), path)
108
+ return fromCwd.startsWith('..') ? path : fromCwd
109
+ }
110
+
111
+ /**
112
+ * Reports absence only when the module or its engine cannot be resolved, which
113
+ * is the published-package case. Any other import failure is a defect inside
114
+ * the render module and propagates, rather than being reported as a feature
115
+ * the package left out.
116
+ */
117
+ async function loadRenderer(): Promise<Renderer | undefined> {
118
+ try {
119
+ return await import('@/capture/render')
120
+ } catch (error) {
121
+ if (isModuleNotFound(error)) return undefined
122
+ throw error
123
+ }
124
+ }
125
+
126
+ function isModuleNotFound(error: unknown): boolean {
127
+ return (
128
+ typeof error === 'object' &&
129
+ error !== null &&
130
+ 'code' in error &&
131
+ error.code === 'ERR_MODULE_NOT_FOUND'
132
+ )
133
+ }