@erclx/aitk 0.16.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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents.md +24 -0
- package/package.json +2 -1
- package/src/cli.ts +4 -0
- package/src/commands/capture.ts +133 -0
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.
|
|
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
|
+
}
|