@erclx/canon 5.66.0 → 5.68.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/CHANGELOG.md +23 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +35 -0
- package/claude/skills/app-drive/SKILL.md +112 -0
- package/claude/skills/app-drive/references/config.md +38 -0
- package/claude/skills/app-drive/references/drive-record.md +70 -0
- package/claude/skills/app-drive/references/isolate.md +46 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/plan-feature/references/plan.md +1 -1
- package/claude/skills/plan-intake/references/intake.md +12 -2
- package/claude/skills/pr-evidence/REQUIREMENT.md +1 -1
- package/claude/skills/pr-evidence/SKILL.md +7 -1
- package/claude/skills/pr-evidence/references/evidence.md +33 -3
- package/claude/skills/review-craft/references/ui.md +30 -10
- package/claude/skills/review-ui/SKILL.md +5 -4
- package/claude/skills/review-ui/references/drive-copy.md +4 -0
- package/claude/skills/review-ui/references/driving.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +15 -2
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-worker/SKILL.md +4 -6
- package/claude/skills/session-map/REQUIREMENT.md +1 -1
- package/claude/skills/session-map/SKILL.md +8 -1
- package/claude/skills/session-map/references/session.md +19 -5
- package/claude/skills/ui-record/REQUIREMENT.md +36 -0
- package/claude/skills/ui-record/SKILL.md +114 -0
- package/claude/skills/ui-record/references/config.md +40 -0
- package/claude/skills/ui-record/references/handoff.md +67 -0
- package/claude/skills/ui-record/references/pr-clip.md +84 -0
- package/claude/skills/ui-record/references/take-craft.md +99 -0
- package/docs/agents/records.md +2 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +1 -0
- package/docs/workflow/skill-map.md +1 -0
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/package.json +5 -2
- package/src/audits/catalog.ts +5 -1
- package/src/browser/agent-cli.ts +68 -0
- package/src/claude/cases/workflow.ts +20 -0
- package/src/cli/help.ts +3 -0
- package/src/cli.ts +4 -0
- package/src/commands/browser.ts +102 -0
- package/src/commands/intake.ts +181 -1
- package/src/commands/pr.ts +252 -85
- package/src/commands/records.ts +18 -1
- package/src/commands/take.ts +302 -0
- package/src/context/requirements.ts +1 -11
- package/src/git/status.ts +73 -0
- package/src/intake/drive-check.ts +355 -0
- package/src/intake/folder.ts +1 -1
- package/src/markdown/scan.ts +10 -0
- package/src/pr/drive-copy.ts +97 -84
- package/src/pr/evidence.ts +208 -78
- package/src/pr/frames.ts +42 -4
- package/src/pr/pixels.ts +44 -0
- package/src/records/backup.ts +28 -4
- package/src/records/session-map.ts +75 -0
- package/src/records/validate.ts +64 -8
- package/src/take/camera.ts +309 -0
- package/src/take/encode.ts +207 -0
- package/src/take/film.ts +211 -0
- package/src/take/fixtures/fade.html +49 -0
- package/src/take/fixtures/fetch.html +38 -0
- package/src/take/log.ts +109 -0
- package/src/take/perform.ts +200 -0
- package/src/take/plan.ts +193 -0
- package/src/take/pointer.ts +168 -0
- package/src/take/reel.ts +215 -0
- package/src/take/screencast.ts +97 -0
- package/src/take/shell.ts +287 -0
- package/src/take/stepper.ts +255 -0
- package/src/take/zoom/NOTICE.md +45 -0
- package/src/take/zoom/math-utils.ts +69 -0
- package/src/take/zoom/region-utils.ts +55 -0
- package/src/take/zoom/spring.ts +133 -0
- package/src/tasks/archive.ts +1 -3
- package/claude/skills/review-ui/scripts/pw.sh +0 -60
package/src/commands/pr.ts
CHANGED
|
@@ -32,17 +32,22 @@ import {
|
|
|
32
32
|
findEvidenceHead,
|
|
33
33
|
findEvidenceLocal,
|
|
34
34
|
findEvidencePreview,
|
|
35
|
+
findEvidenceReel,
|
|
35
36
|
type EvidenceItem,
|
|
36
37
|
type EvidenceReading,
|
|
37
38
|
groupEvidence,
|
|
38
39
|
hasMarkedEvidenceComment,
|
|
40
|
+
isEvidencePath,
|
|
39
41
|
PNG_HEADER_BYTES,
|
|
40
42
|
readChecklistBoxes,
|
|
41
43
|
readOwed,
|
|
42
44
|
readPngWidth,
|
|
45
|
+
readUncaptured,
|
|
46
|
+
nextEvidenceReel,
|
|
43
47
|
renderEvidenceBody,
|
|
44
48
|
settleChecklist,
|
|
45
49
|
} from '@/pr/evidence'
|
|
50
|
+
import { isWithinTolerance } from '@/pr/pixels'
|
|
46
51
|
import {
|
|
47
52
|
dropFramesWithLegacy,
|
|
48
53
|
FRAMES_BRANCH,
|
|
@@ -56,7 +61,9 @@ import {
|
|
|
56
61
|
LEGACY_FRAMES_BRANCH,
|
|
57
62
|
pruneFramesWithLegacy,
|
|
58
63
|
pushFrame,
|
|
64
|
+
REEL_MAX_BYTES,
|
|
59
65
|
readFrame,
|
|
66
|
+
readReel,
|
|
60
67
|
isDesignReference,
|
|
61
68
|
referenceSlug,
|
|
62
69
|
type TreeEntry,
|
|
@@ -83,14 +90,12 @@ import {
|
|
|
83
90
|
type MergeRequest,
|
|
84
91
|
} from '@/pr/merge'
|
|
85
92
|
import { listWorktrees } from '@/git/worktree'
|
|
93
|
+
import { branchSlug, buildDriveCopy, loadPreviewConfig } from '@/pr/drive-copy'
|
|
86
94
|
import {
|
|
87
|
-
branchSlug,
|
|
88
|
-
buildDriveCopy,
|
|
89
95
|
changedSince,
|
|
90
|
-
loadPreviewConfig,
|
|
91
96
|
readWorktreeStatus,
|
|
92
97
|
type StatusEntry,
|
|
93
|
-
} from '@/
|
|
98
|
+
} from '@/git/status'
|
|
94
99
|
import { machineListenerReader } from '@/listeners/machine'
|
|
95
100
|
import { loadPaintedPatterns } from '@/pr/painted'
|
|
96
101
|
import { LOCAL_CONFIG_REL, readHealthPaths } from '@/pr/health'
|
|
@@ -164,6 +169,7 @@ interface EvidenceOptions extends ReadOptions {
|
|
|
164
169
|
readonly preview?: string
|
|
165
170
|
readonly checklist?: string
|
|
166
171
|
readonly local?: string
|
|
172
|
+
readonly reel?: string
|
|
167
173
|
readonly check?: boolean
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -274,6 +280,7 @@ interface FramesOptions extends ReadOptions {
|
|
|
274
280
|
readonly head?: string
|
|
275
281
|
readonly pass?: string
|
|
276
282
|
readonly theme?: string
|
|
283
|
+
readonly reel?: string
|
|
277
284
|
readonly drop?: boolean
|
|
278
285
|
readonly prune?: string
|
|
279
286
|
}
|
|
@@ -289,6 +296,8 @@ type FramesCommandRefusal =
|
|
|
289
296
|
| 'bad-theme'
|
|
290
297
|
| 'bad-days'
|
|
291
298
|
| 'unreadable-frame'
|
|
299
|
+
| 'unreadable-reel'
|
|
300
|
+
| 'reel-too-large'
|
|
292
301
|
| 'read-only'
|
|
293
302
|
| 'gh-missing'
|
|
294
303
|
| 'gh-failed'
|
|
@@ -296,22 +305,25 @@ type FramesCommandRefusal =
|
|
|
296
305
|
|
|
297
306
|
const FRAMES_REFUSALS: Record<FramesCommandRefusal, string> = {
|
|
298
307
|
'bad-mode':
|
|
299
|
-
'Pass exactly one of --add, --drop, or --prune. --prune reads every pull request on the branch, so it takes no number.',
|
|
308
|
+
'Pass exactly one of --add, --reel, --drop, or --prune. --prune reads every pull request on the branch, so it takes no number.',
|
|
300
309
|
'no-number':
|
|
301
|
-
'--add and --drop write for one pull request, so name its number.',
|
|
310
|
+
'--add, --reel, and --drop write for one pull request, so name its number.',
|
|
302
311
|
'bad-box':
|
|
303
|
-
'--add takes --box <n>, a positive whole number naming the checklist box, or --reference, as <page>/<frame> or a tracked canon/design/<surface>/<name> path, never both.',
|
|
312
|
+
'--add takes --box <n>, a positive whole number naming the checklist box, or --reference, as <page>/<frame> or a tracked canon/design/<surface>/<name> path, never both. --reel takes neither, since a pass has one reel.',
|
|
304
313
|
'bad-reference':
|
|
305
314
|
'--reference takes <page>/<frame> or a tracked canon/design/<surface>/<name> path, each half opening on a letter or digit and holding only letters, digits, dots, underscores, and dashes, with no `--` in either, so the capture files under a name that reads back as the frame.',
|
|
306
315
|
'bad-head':
|
|
307
316
|
'--head takes a commit sha, 7 to 40 lowercase hex characters. A branch name or a path would file the frame under a folder that names no commit.',
|
|
308
317
|
'bad-pass':
|
|
309
|
-
'--add
|
|
318
|
+
'--add and --reel take --pass <stamp>, the instant the pass started in compact UTC such as 20261002T154450Z, so a second pass never replaces an earlier one.',
|
|
310
319
|
'bad-theme':
|
|
311
320
|
'--theme takes dark or light and goes with --add --box, so a second shot of one box never replaces the first.',
|
|
312
321
|
'bad-days': '--prune takes a positive whole number of days.',
|
|
313
322
|
'unreadable-frame':
|
|
314
323
|
'The file named by --add is missing, empty, or not a PNG, so it would embed as a broken image. Nothing is pushed.',
|
|
324
|
+
'unreadable-reel':
|
|
325
|
+
'The file named by --reel is missing, empty, or not a WebP, so it would not play in the comment. Nothing is pushed.',
|
|
326
|
+
'reel-too-large': `The file named by --reel is past ${REEL_MAX_BYTES / 1_000_000} MB, the most GitHub renders as an image in a comment. Film fewer or shorter beats. Nothing is pushed.`,
|
|
315
327
|
'read-only': `GitHub refused the write to ${FRAMES_BRANCH}. The token cannot write this repository, which is what a fork pull request's workflow token gets, so describe the frame in words instead.`,
|
|
316
328
|
'gh-missing':
|
|
317
329
|
'gh is not on the path, so the branch could not be read or written.',
|
|
@@ -417,7 +429,7 @@ type EvidenceRefusal =
|
|
|
417
429
|
|
|
418
430
|
const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
419
431
|
'check-writes':
|
|
420
|
-
'--check is a read and renders no body, so it takes none of --preview, --local, or --
|
|
432
|
+
'--check is a read and renders no body, so it takes none of --preview, --local, --checklist, or --reel. Run the check and the render as two calls.',
|
|
421
433
|
'gh-failed':
|
|
422
434
|
'gh could not answer for this repository. Name the pull request number.',
|
|
423
435
|
'unreadable-checklist':
|
|
@@ -754,6 +766,10 @@ export function register(program: Command): void {
|
|
|
754
766
|
'--local <url>',
|
|
755
767
|
"Add this worktree's running server to the address line, from canon pr local",
|
|
756
768
|
)
|
|
769
|
+
.option(
|
|
770
|
+
'--reel <link>',
|
|
771
|
+
'Embed this reel under the address line, filmed at the head, from canon pr frames --reel',
|
|
772
|
+
)
|
|
757
773
|
.option(
|
|
758
774
|
'--check',
|
|
759
775
|
'Report which post-pull-request steps the thread is still owed, rendering nothing',
|
|
@@ -784,6 +800,19 @@ export function register(program: Command): void {
|
|
|
784
800
|
'the hosted and local previews with " · ", carrying whichever apply,',
|
|
785
801
|
'then the Base and Head line when there are screenshots.',
|
|
786
802
|
'',
|
|
803
|
+
'Each case shows its head image at the comment width under a bold',
|
|
804
|
+
'label, with its base folded beneath in a <details> block, and an',
|
|
805
|
+
'added case shows its head alone. Every state opens while the comment',
|
|
806
|
+
'carries at most six head images and closes past that.',
|
|
807
|
+
'',
|
|
808
|
+
'Each modified PNG is compared with its base pixel by pixel. A case',
|
|
809
|
+
'whose densest 16px square holds 6 or fewer differing pixels moves to',
|
|
810
|
+
'one closed "Within tolerance" block after the states, still named and',
|
|
811
|
+
'embedded, and stops counting toward the six. Its item on the record',
|
|
812
|
+
'carries `withinTolerance`, which is absent for an added case, a',
|
|
813
|
+
'format other than PNG, or a pair with a failed read. None of those',
|
|
814
|
+
'folds, and neither does a pair that differs in size or will not decode.',
|
|
815
|
+
'',
|
|
787
816
|
'--preview puts the hosted address on that line. With no evidence in',
|
|
788
817
|
'the diff, the body is the heading, that line, and the marker alone,',
|
|
789
818
|
'reported as ok, so the preview still lands in one comment a later',
|
|
@@ -813,6 +842,13 @@ export function register(program: Command): void {
|
|
|
813
842
|
'comment. Together with --checklist the link and the checklist land in',
|
|
814
843
|
'one comment.',
|
|
815
844
|
'',
|
|
845
|
+
'--reel embeds an animated reel on its own line under the addresses,',
|
|
846
|
+
'labeled with the head this render describes as the one it was filmed',
|
|
847
|
+
'at. Without --reel, a reel the marked comment already embeds is carried',
|
|
848
|
+
'forward under the head it names, so a stale reel never reads as',
|
|
849
|
+
'current, and a passed one replaces it. Like --local it rides on a',
|
|
850
|
+
'checklist or a comparison and never opens a comment alone.',
|
|
851
|
+
'',
|
|
816
852
|
"A render at a new head also resets the pull request body's UI pass",
|
|
817
853
|
'line, the one `canon pr tick` writes under ## Testing, to unchecked',
|
|
818
854
|
'naming that head. It touches no other body text, leaves a line already',
|
|
@@ -822,6 +858,7 @@ export function register(program: Command): void {
|
|
|
822
858
|
'present only when that comment holds it:',
|
|
823
859
|
' preview the hosted **Preview:** address the comment carries',
|
|
824
860
|
' local the **Local preview:** address beside it',
|
|
861
|
+
' reel the embedded reel, as `link` and the `head` it was filmed at',
|
|
825
862
|
' checklist the checklist between its delimiters, ticks included',
|
|
826
863
|
' boxes each checklist box with its number, tick, stamp, and taste',
|
|
827
864
|
' mark, the numbering `canon pr tick` takes',
|
|
@@ -831,6 +868,16 @@ export function register(program: Command): void {
|
|
|
831
868
|
'unread thread refuses as gh-failed on both reasons rather than',
|
|
832
869
|
'reporting the fields absent.',
|
|
833
870
|
'',
|
|
871
|
+
'The ok, no-evidence, and --check records carry `uncaptured`, the',
|
|
872
|
+
'painted paths the pull request changed while no evidence image did, in',
|
|
873
|
+
"a project whose base branch's tree holds one. Painted is the set",
|
|
874
|
+
'`canon/config/painted.json` tunes, minus any path under an evidence/',
|
|
875
|
+
'segment or canon/design/, and any changed evidence image empties the',
|
|
876
|
+
'list. The field is absent when the base tree holds no capture or could',
|
|
877
|
+
'not be read, a cut-short listing showing none included, so absent means',
|
|
878
|
+
'unchecked rather than clean. It never adds to `owed`, since no step of',
|
|
879
|
+
'the ship chain can take a capture.',
|
|
880
|
+
'',
|
|
834
881
|
'--check reads the same changed set and thread, renders no body, and',
|
|
835
882
|
'reports what the ship chain still owes the pull request:',
|
|
836
883
|
' settled nothing is owed',
|
|
@@ -841,7 +888,7 @@ export function register(program: Command): void {
|
|
|
841
888
|
'request changed, and the marked comment carries no **Preview:**',
|
|
842
889
|
'address. A failed deploy reads the same as a skipped one.',
|
|
843
890
|
'The record carries `owed` and the same marked fields. --check refuses',
|
|
844
|
-
'with --preview, --local, or --
|
|
891
|
+
'with --preview, --local, --checklist, or --reel as check-writes.',
|
|
845
892
|
'',
|
|
846
893
|
'Refusals, read off `reason`:',
|
|
847
894
|
' unreadable-checklist --checklist named a path that could not be read,',
|
|
@@ -864,6 +911,7 @@ export function register(program: Command): void {
|
|
|
864
911
|
' canon pr evidence 1341 --preview https://feat-x.site.pages.dev --json',
|
|
865
912
|
' canon pr evidence 1341 --checklist .canon/tmp/handoff/ui-checklist/x.md --json',
|
|
866
913
|
' canon pr evidence 1341 --local http://localhost:5173 --json',
|
|
914
|
+
' canon pr evidence 1341 --reel <link from canon pr frames --reel> --json',
|
|
867
915
|
' canon pr evidence 1341 --check --json',
|
|
868
916
|
'',
|
|
869
917
|
].join('\n'),
|
|
@@ -1306,18 +1354,28 @@ export function register(program: Command): void {
|
|
|
1306
1354
|
[
|
|
1307
1355
|
'',
|
|
1308
1356
|
'A project names what its preview writes in',
|
|
1309
|
-
'`canon/config/preview.json` as
|
|
1357
|
+
'`canon/config/preview.json` as',
|
|
1358
|
+
'`{ "writes": [path], "keep": [path], "env": "<NAME>" }`.',
|
|
1310
1359
|
'Each `writes` path is relative to the worktree, and `env`, defaulting',
|
|
1311
1360
|
'to `CANON_PREVIEW_ROOT`, is the variable the server reads for the copy',
|
|
1312
1361
|
'root. The server resolves each declared path under that root.',
|
|
1313
1362
|
'',
|
|
1363
|
+
'Each `keep` path sits below a `writes` path, never equal to one, and',
|
|
1364
|
+
'names state the running server owns there, such as a run file. A',
|
|
1365
|
+
"rebuild leaves it in the copy and never copies the worktree's own, so",
|
|
1366
|
+
'list it for a server that writes under a declared path. The key is',
|
|
1367
|
+
'optional.',
|
|
1368
|
+
'',
|
|
1314
1369
|
'The copy lives at `.canon/tmp/pr/review-ui/<slug>/board/` at the main',
|
|
1315
1370
|
'worktree root, where `<slug>` is the branch with a leading type',
|
|
1316
1371
|
'segment dropped and each `/` made `-`. With a number the branch is the',
|
|
1317
1372
|
"pull request's head, and without one it is the checkout's, so the",
|
|
1318
1373
|
'worker starting its server before a pull request exists and the',
|
|
1319
1374
|
'reviewer naming the number land on one copy. Every run but --check',
|
|
1320
|
-
"
|
|
1375
|
+
"resets each declared path from the worktree, dropping the last pass's",
|
|
1376
|
+
'drives, and leaves the rest of the copy root and each `keep` path as',
|
|
1377
|
+
'they are. A page added only to the copy, inside a declared path, is',
|
|
1378
|
+
'wiped.',
|
|
1321
1379
|
'',
|
|
1322
1380
|
'--baseline records `git status` for the worktree under',
|
|
1323
1381
|
'`.canon/tmp/pr/review-ui/<number>/`, and --check reports each path',
|
|
@@ -1388,6 +1446,10 @@ export function register(program: Command): void {
|
|
|
1388
1446
|
'--theme <theme>',
|
|
1389
1447
|
'The theme the frame was shot at, dark or light, with --add, keeping two shots of one box apart',
|
|
1390
1448
|
)
|
|
1449
|
+
.option(
|
|
1450
|
+
'--reel <file>',
|
|
1451
|
+
"Push this animated WebP as the pass's reel, in place of --add",
|
|
1452
|
+
)
|
|
1391
1453
|
.option('--drop', "Rewrite the branch without this pull request's frames")
|
|
1392
1454
|
.option(
|
|
1393
1455
|
'--prune <days>',
|
|
@@ -1409,6 +1471,11 @@ export function register(program: Command): void {
|
|
|
1409
1471
|
'segment keeps a second pass at one head and box from replacing an image',
|
|
1410
1472
|
'an earlier comment shows. The first --add creates the branch.',
|
|
1411
1473
|
'',
|
|
1474
|
+
`--reel <file> pushes one animated WebP of at most ${REEL_MAX_BYTES / 1_000_000} MB to`,
|
|
1475
|
+
'pr-<number>/<short-head>/<pass>/reel.webp beside the box frames, taking',
|
|
1476
|
+
'--pass and --head the way --add does, and carries `reel: true` in place of',
|
|
1477
|
+
'a box. A drop or a prune clears it with the rest of the folder.',
|
|
1478
|
+
'',
|
|
1412
1479
|
'--drop and --prune write one root commit and force-move the ref,',
|
|
1413
1480
|
'since a delete commit would keep every image reachable through history.',
|
|
1414
1481
|
'A rewrite that empties the branch deletes it. --prune keeps a pull',
|
|
@@ -1422,15 +1489,17 @@ export function register(program: Command): void {
|
|
|
1422
1489
|
'',
|
|
1423
1490
|
'Read `reason` on the JSON record:',
|
|
1424
1491
|
' ok an add pushed, or a drop or prune finished',
|
|
1425
|
-
' bad-mode not exactly one of --add, --drop, --prune',
|
|
1426
|
-
' no-number --add or --drop named no pull request',
|
|
1427
|
-
' bad-box --add with both --box and --reference, or neither',
|
|
1492
|
+
' bad-mode not exactly one of --add, --reel, --drop, --prune',
|
|
1493
|
+
' no-number --add, --reel, or --drop named no pull request',
|
|
1494
|
+
' bad-box --add with both --box and --reference, or neither, or --reel with either',
|
|
1428
1495
|
' bad-reference --reference is not <page>/<frame> or canon/design/<surface>/<name> in the safe name form, or the tracked path is no file',
|
|
1429
1496
|
' bad-head --head is not 7 to 40 lowercase hex characters',
|
|
1430
|
-
' bad-pass --add without a compact UTC --pass stamp',
|
|
1497
|
+
' bad-pass --add or --reel without a compact UTC --pass stamp',
|
|
1431
1498
|
' bad-theme --theme is not dark or light, or no --add, or rides a --reference',
|
|
1432
1499
|
' bad-days --prune without a positive number of days',
|
|
1433
1500
|
' unreadable-frame the --add file is missing, empty, or not a PNG',
|
|
1501
|
+
' unreadable-reel the --reel file is missing, empty, or not a WebP',
|
|
1502
|
+
` reel-too-large the --reel file is past ${REEL_MAX_BYTES / 1_000_000} MB`,
|
|
1434
1503
|
' read-only GitHub refused the write, as on a fork token',
|
|
1435
1504
|
' unreadable-tip the branch could not be read',
|
|
1436
1505
|
' push-failed a write failed and the ref did not move',
|
|
@@ -1445,6 +1514,7 @@ export function register(program: Command): void {
|
|
|
1445
1514
|
'',
|
|
1446
1515
|
'Examples:',
|
|
1447
1516
|
' canon pr frames 1341 --add frames/3.png --box 3 --pass 20261002T154450Z --json',
|
|
1517
|
+
' canon pr frames 1341 --reel reel.webp --pass 20261002T154450Z --json',
|
|
1448
1518
|
' canon pr frames 1341 --drop --json',
|
|
1449
1519
|
' canon pr frames --prune 30 --json',
|
|
1450
1520
|
'',
|
|
@@ -2358,6 +2428,31 @@ function everyChangedPath(files: PullFiles): string[] {
|
|
|
2358
2428
|
return [...files.present.keys(), ...files.removed]
|
|
2359
2429
|
}
|
|
2360
2430
|
|
|
2431
|
+
/**
|
|
2432
|
+
* Whether the base branch's tree holds an evidence image, read recursively
|
|
2433
|
+
* over the API so the answer is the same from any checkout. GitHub cuts a
|
|
2434
|
+
* large listing short, and a cut listing proves captures only when one
|
|
2435
|
+
* arrived before the cut, so it reads as unread otherwise. Undefined on any
|
|
2436
|
+
* failed read.
|
|
2437
|
+
*/
|
|
2438
|
+
async function readBaseKeepsCaptures(
|
|
2439
|
+
cwd: string,
|
|
2440
|
+
baseRef: string | undefined,
|
|
2441
|
+
): Promise<boolean | undefined> {
|
|
2442
|
+
if (baseRef === undefined || baseRef === '') return undefined
|
|
2443
|
+
const stdout = await gh(cwd, [
|
|
2444
|
+
'api',
|
|
2445
|
+
`repos/{owner}/{repo}/git/trees/${baseRef}?recursive=1`,
|
|
2446
|
+
'--jq',
|
|
2447
|
+
'.truncated, (.tree[] | select(.type == "blob") | .path)',
|
|
2448
|
+
])
|
|
2449
|
+
if (stdout === null) return undefined
|
|
2450
|
+
const [truncated, ...paths] = stdout.split('\n').filter(Boolean)
|
|
2451
|
+
if (truncated !== 'true' && truncated !== 'false') return undefined
|
|
2452
|
+
if (paths.some(isEvidencePath)) return true
|
|
2453
|
+
return truncated === 'true' ? undefined : false
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2361
2456
|
/**
|
|
2362
2457
|
* The commit the pull request diverged from its base at, from the compare
|
|
2363
2458
|
* endpoint, since a checkout on another branch resolves its own trunk tip
|
|
@@ -2378,25 +2473,30 @@ async function readPullMergeBase(
|
|
|
2378
2473
|
return sha === null || sha.trim() === '' ? undefined : sha.trim()
|
|
2379
2474
|
}
|
|
2380
2475
|
|
|
2381
|
-
/** How many
|
|
2382
|
-
const
|
|
2476
|
+
/** How many cases are read at once. Each reads its images one after another, so this bounds both the downloads in flight and the pairs held for comparison. */
|
|
2477
|
+
const IMAGE_READ_CONCURRENCY = 4
|
|
2383
2478
|
|
|
2384
|
-
type
|
|
2385
|
-
| {
|
|
2479
|
+
type ImageRead =
|
|
2480
|
+
| {
|
|
2481
|
+
readonly kind: 'read'
|
|
2482
|
+
readonly width?: number
|
|
2483
|
+
readonly bytes?: Uint8Array
|
|
2484
|
+
}
|
|
2386
2485
|
| { readonly kind: 'failed' }
|
|
2387
2486
|
|
|
2388
2487
|
/**
|
|
2389
|
-
*
|
|
2390
|
-
*
|
|
2391
|
-
* file arrives and
|
|
2392
|
-
* reads
|
|
2393
|
-
* the caller can keep the ceiling width and still report
|
|
2488
|
+
* A PNG at `sha`, read over the contents API like every other evidence read,
|
|
2489
|
+
* with its pixel width from the header. The endpoint ignores a `Range` header,
|
|
2490
|
+
* so the whole file arrives, and its bytes are kept for the pixel comparison.
|
|
2491
|
+
* A format other than PNG reads with neither, and a refused or timed-out call
|
|
2492
|
+
* reads as failed, so the caller can keep the ceiling width and still report
|
|
2493
|
+
* the cause.
|
|
2394
2494
|
*/
|
|
2395
|
-
async function
|
|
2495
|
+
async function readImage(
|
|
2396
2496
|
cwd: string,
|
|
2397
2497
|
path: string,
|
|
2398
2498
|
sha: string,
|
|
2399
|
-
): Promise<
|
|
2499
|
+
): Promise<ImageRead> {
|
|
2400
2500
|
if (!/\.png$/i.test(path)) return { kind: 'read' }
|
|
2401
2501
|
try {
|
|
2402
2502
|
const result = await execa(
|
|
@@ -2415,8 +2515,11 @@ async function readImageWidth(
|
|
|
2415
2515
|
encoding: 'buffer',
|
|
2416
2516
|
},
|
|
2417
2517
|
)
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2518
|
+
const bytes = result.stdout
|
|
2519
|
+
const width = readPngWidth(bytes.subarray(0, PNG_HEADER_BYTES))
|
|
2520
|
+
return width === undefined
|
|
2521
|
+
? { kind: 'read', bytes }
|
|
2522
|
+
: { kind: 'read', width, bytes }
|
|
2420
2523
|
} catch {
|
|
2421
2524
|
return { kind: 'failed' }
|
|
2422
2525
|
}
|
|
@@ -2444,66 +2547,79 @@ async function mapPool<T, R>(
|
|
|
2444
2547
|
|
|
2445
2548
|
interface SizedEvidence {
|
|
2446
2549
|
readonly reading: EvidenceReading
|
|
2447
|
-
/** Each image whose
|
|
2550
|
+
/** Each image whose read failed, as `head:<path>` or `base:<path>`, which rendered at the ceiling and left its case uncompared. */
|
|
2448
2551
|
readonly unread: readonly string[]
|
|
2449
2552
|
}
|
|
2450
2553
|
|
|
2451
|
-
|
|
2452
|
-
|
|
2554
|
+
interface MeasuredItem {
|
|
2555
|
+
readonly item: EvidenceItem
|
|
2556
|
+
readonly unread: readonly string[]
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
/**
|
|
2560
|
+
* One case with the pixel width of its head image and, when it has one, its
|
|
2561
|
+
* base image, and whether the two match within tolerance. The pair is compared
|
|
2562
|
+
* as soon as both reads land, so its bytes are dropped before the pool takes
|
|
2563
|
+
* the next case. A case missing either PNG carries no verdict.
|
|
2564
|
+
*/
|
|
2565
|
+
async function measureItem(
|
|
2566
|
+
cwd: string,
|
|
2567
|
+
item: EvidenceItem,
|
|
2568
|
+
base: string | undefined,
|
|
2569
|
+
head: string,
|
|
2570
|
+
): Promise<MeasuredItem> {
|
|
2571
|
+
const headRead = await readImage(cwd, item.path, head)
|
|
2572
|
+
const baseRead =
|
|
2573
|
+
item.added || base === undefined
|
|
2574
|
+
? undefined
|
|
2575
|
+
: await readImage(cwd, item.path, base)
|
|
2576
|
+
const unread = [
|
|
2577
|
+
...(headRead.kind === 'failed' ? [`head:${item.path}`] : []),
|
|
2578
|
+
...(baseRead?.kind === 'failed' ? [`base:${item.path}`] : []),
|
|
2579
|
+
]
|
|
2580
|
+
const headBytes = headRead.kind === 'read' ? headRead.bytes : undefined
|
|
2581
|
+
const baseBytes = baseRead?.kind === 'read' ? baseRead.bytes : undefined
|
|
2582
|
+
return {
|
|
2583
|
+
item: {
|
|
2584
|
+
...item,
|
|
2585
|
+
...(headRead.kind === 'read' &&
|
|
2586
|
+
headRead.width !== undefined && { width: headRead.width }),
|
|
2587
|
+
...(baseRead?.kind === 'read' &&
|
|
2588
|
+
baseRead.width !== undefined && { baseWidth: baseRead.width }),
|
|
2589
|
+
...(headBytes !== undefined &&
|
|
2590
|
+
baseBytes !== undefined && {
|
|
2591
|
+
withinTolerance: isWithinTolerance(baseBytes, headBytes),
|
|
2592
|
+
}),
|
|
2593
|
+
},
|
|
2594
|
+
unread,
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/** Each item measured by `measureItem`, in the reading's order. */
|
|
2599
|
+
async function withImageReads(
|
|
2453
2600
|
cwd: string,
|
|
2454
2601
|
reading: EvidenceReading,
|
|
2455
2602
|
base: string | undefined,
|
|
2456
2603
|
head: string,
|
|
2457
2604
|
): Promise<SizedEvidence> {
|
|
2458
2605
|
if (reading.kind === 'refused') return { reading, unread: [] }
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
itemIndex,
|
|
2464
|
-
side: 'head' as const,
|
|
2465
|
-
path: item.path,
|
|
2466
|
-
sha: head,
|
|
2467
|
-
},
|
|
2468
|
-
...(item.added || base === undefined
|
|
2469
|
-
? []
|
|
2470
|
-
: [
|
|
2471
|
-
{
|
|
2472
|
-
stateIndex,
|
|
2473
|
-
itemIndex,
|
|
2474
|
-
side: 'base' as const,
|
|
2475
|
-
path: item.path,
|
|
2476
|
-
sha: base,
|
|
2477
|
-
},
|
|
2478
|
-
]),
|
|
2479
|
-
]),
|
|
2480
|
-
)
|
|
2481
|
-
const reads = await mapPool(jobs, WIDTH_READ_CONCURRENCY, (job) =>
|
|
2482
|
-
readImageWidth(cwd, job.path, job.sha),
|
|
2606
|
+
const measured = await mapPool(
|
|
2607
|
+
reading.states.flatMap((entry) => entry.items),
|
|
2608
|
+
IMAGE_READ_CONCURRENCY,
|
|
2609
|
+
(item) => measureItem(cwd, item, base, head),
|
|
2483
2610
|
)
|
|
2484
|
-
|
|
2485
|
-
const states = reading.states.map((entry
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
return
|
|
2497
|
-
}
|
|
2498
|
-
if (read.kind === 'failed') unread.push(`${job.side}:${job.path}`)
|
|
2499
|
-
else if (read.width !== undefined) {
|
|
2500
|
-
widths[job.side === 'head' ? 'width' : 'baseWidth'] = read.width
|
|
2501
|
-
}
|
|
2502
|
-
})
|
|
2503
|
-
return { ...item, ...widths }
|
|
2504
|
-
}),
|
|
2505
|
-
}))
|
|
2506
|
-
return { reading: { kind: 'read', states }, unread }
|
|
2611
|
+
let offset = 0
|
|
2612
|
+
const states = reading.states.map((entry) => {
|
|
2613
|
+
const items = measured
|
|
2614
|
+
.slice(offset, offset + entry.items.length)
|
|
2615
|
+
.map((read) => read.item)
|
|
2616
|
+
offset += entry.items.length
|
|
2617
|
+
return { ...entry, items }
|
|
2618
|
+
})
|
|
2619
|
+
return {
|
|
2620
|
+
reading: { kind: 'read', states },
|
|
2621
|
+
unread: measured.flatMap((entry) => entry.unread),
|
|
2622
|
+
}
|
|
2507
2623
|
}
|
|
2508
2624
|
|
|
2509
2625
|
async function runEvidence(
|
|
@@ -2520,7 +2636,8 @@ async function runEvidence(
|
|
|
2520
2636
|
isCheck &&
|
|
2521
2637
|
(opts.preview !== undefined ||
|
|
2522
2638
|
opts.local !== undefined ||
|
|
2523
|
-
opts.checklist !== undefined
|
|
2639
|
+
opts.checklist !== undefined ||
|
|
2640
|
+
opts.reel !== undefined)
|
|
2524
2641
|
) {
|
|
2525
2642
|
return refuseWith(
|
|
2526
2643
|
'check-writes',
|
|
@@ -2573,9 +2690,10 @@ async function runEvidence(
|
|
|
2573
2690
|
|
|
2574
2691
|
// The check never reads the merge base, so it skips the compare call and
|
|
2575
2692
|
// a failed one cannot hold a draft lift for a reason the check never had.
|
|
2576
|
-
const [pullFiles, base] = await Promise.all([
|
|
2693
|
+
const [pullFiles, base, keepsCaptures] = await Promise.all([
|
|
2577
2694
|
listPullFiles(root, identity.number),
|
|
2578
2695
|
isCheck ? undefined : readPullMergeBase(root, identity.base, identity.head),
|
|
2696
|
+
readBaseKeepsCaptures(root, identity.base),
|
|
2579
2697
|
])
|
|
2580
2698
|
if (pullFiles === undefined) {
|
|
2581
2699
|
return refuseWith(
|
|
@@ -2590,6 +2708,11 @@ async function runEvidence(
|
|
|
2590
2708
|
[...pullFiles.present.keys()],
|
|
2591
2709
|
async (path) => pullFiles.present.get(path) === false,
|
|
2592
2710
|
)
|
|
2711
|
+
const uncaptured = readUncaptured(
|
|
2712
|
+
everyChangedPath(pullFiles),
|
|
2713
|
+
loadPaintedPatterns(root),
|
|
2714
|
+
keepsCaptures,
|
|
2715
|
+
)
|
|
2593
2716
|
|
|
2594
2717
|
let existingCases = 0
|
|
2595
2718
|
let commentId: number | undefined
|
|
@@ -2615,12 +2738,14 @@ async function runEvidence(
|
|
|
2615
2738
|
carriedPreview = findEvidencePreview(comments)
|
|
2616
2739
|
carriedChecklist = findEvidenceChecklist(comments)
|
|
2617
2740
|
carriedLocal = findEvidenceLocal(comments)
|
|
2741
|
+
const carriedReel = findEvidenceReel(comments)
|
|
2618
2742
|
|
|
2619
2743
|
// What the marked comment already shows a reviewer, kept apart from the
|
|
2620
2744
|
// flags this call passed so a reader learns what is posted.
|
|
2621
2745
|
const carried = {
|
|
2622
2746
|
...(carriedPreview !== undefined && { preview: carriedPreview }),
|
|
2623
2747
|
...(carriedLocal !== undefined && { local: carriedLocal }),
|
|
2748
|
+
...(carriedReel !== undefined && { reel: carriedReel }),
|
|
2624
2749
|
...(carriedChecklist !== undefined && {
|
|
2625
2750
|
checklist: carriedChecklist,
|
|
2626
2751
|
boxes: readChecklistBoxes(carriedChecklist),
|
|
@@ -2635,6 +2760,16 @@ async function runEvidence(
|
|
|
2635
2760
|
servesChange(deploy.filter, everyChangedPath(pullFiles))
|
|
2636
2761
|
const isUnserved = deploy.kind === 'found' && !deployServesChange
|
|
2637
2762
|
|
|
2763
|
+
// Reported on every record that read the changed set, and never owed,
|
|
2764
|
+
// since no step of the ship chain can take the capture a skip left out.
|
|
2765
|
+
const uncapturedField = uncaptured !== undefined && { uncaptured }
|
|
2766
|
+
const warnUncaptured = (): void => {
|
|
2767
|
+
if (uncaptured === undefined || uncaptured.length === 0) return
|
|
2768
|
+
logWarn(
|
|
2769
|
+
`${plural(uncaptured.length, 'painted path')} changed with no evidence capture: ${uncaptured.join(', ')}`,
|
|
2770
|
+
)
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2638
2773
|
if (isCheck) {
|
|
2639
2774
|
const owed = readOwed({
|
|
2640
2775
|
hasEvidenceChange: grouped.kind === 'read',
|
|
@@ -2648,6 +2783,7 @@ async function runEvidence(
|
|
|
2648
2783
|
? 'Nothing the ship chain posts after the pull request is missing.'
|
|
2649
2784
|
: `missing ${owed.join(', ')}`,
|
|
2650
2785
|
)
|
|
2786
|
+
warnUncaptured()
|
|
2651
2787
|
outro()
|
|
2652
2788
|
if (emitJson) {
|
|
2653
2789
|
process.stdout.write(
|
|
@@ -2656,6 +2792,7 @@ async function runEvidence(
|
|
|
2656
2792
|
number: identity.number,
|
|
2657
2793
|
reason: owed.length === 0 ? 'settled' : 'owed',
|
|
2658
2794
|
owed,
|
|
2795
|
+
...uncapturedField,
|
|
2659
2796
|
...(commentId !== undefined && { commentId }),
|
|
2660
2797
|
...carried,
|
|
2661
2798
|
})}\n`,
|
|
@@ -2683,6 +2820,7 @@ async function runEvidence(
|
|
|
2683
2820
|
logInfo(
|
|
2684
2821
|
'No changed path carries an evidence/ segment, so there is nothing to post.',
|
|
2685
2822
|
)
|
|
2823
|
+
warnUncaptured()
|
|
2686
2824
|
outro()
|
|
2687
2825
|
if (emitJson) {
|
|
2688
2826
|
process.stdout.write(
|
|
@@ -2690,6 +2828,7 @@ async function runEvidence(
|
|
|
2690
2828
|
root,
|
|
2691
2829
|
...(identity.number !== undefined && { number: identity.number }),
|
|
2692
2830
|
reason: 'no-evidence',
|
|
2831
|
+
...uncapturedField,
|
|
2693
2832
|
...carried,
|
|
2694
2833
|
})}\n`,
|
|
2695
2834
|
)
|
|
@@ -2709,12 +2848,13 @@ async function runEvidence(
|
|
|
2709
2848
|
|
|
2710
2849
|
const preview = opts.preview ?? (isUnserved ? undefined : carriedPreview)
|
|
2711
2850
|
const local = opts.local ?? carriedLocal
|
|
2851
|
+
const reel = nextEvidenceReel(opts.reel, identity.head, comments)
|
|
2712
2852
|
const unsettled = suppliedChecklist ?? carriedChecklist
|
|
2713
2853
|
const checklist =
|
|
2714
2854
|
unsettled === undefined
|
|
2715
2855
|
? undefined
|
|
2716
2856
|
: settleChecklist(unsettled, identity.head, findEvidenceHead(comments))
|
|
2717
|
-
const sized = await
|
|
2857
|
+
const sized = await withImageReads(root, grouped, base, identity.head)
|
|
2718
2858
|
const states = sized.reading.kind === 'read' ? sized.reading.states : []
|
|
2719
2859
|
const body = renderEvidenceBody(
|
|
2720
2860
|
states,
|
|
@@ -2724,6 +2864,7 @@ async function runEvidence(
|
|
|
2724
2864
|
preview,
|
|
2725
2865
|
checklist,
|
|
2726
2866
|
local,
|
|
2867
|
+
reel,
|
|
2727
2868
|
)
|
|
2728
2869
|
|
|
2729
2870
|
const caseCount = states.reduce((n, s) => n + s.items.length, 0)
|
|
@@ -2781,6 +2922,13 @@ async function runEvidence(
|
|
|
2781
2922
|
}
|
|
2782
2923
|
if (preview !== undefined) logInfo(`preview ${preview}`)
|
|
2783
2924
|
if (local !== undefined) logInfo(`local preview ${local}`)
|
|
2925
|
+
if (reel !== undefined) {
|
|
2926
|
+
logInfo(
|
|
2927
|
+
opts.reel === undefined
|
|
2928
|
+
? `reel carried forward, filmed at ${reel.head.slice(0, 7)}`
|
|
2929
|
+
: 'reel embedded under the addresses',
|
|
2930
|
+
)
|
|
2931
|
+
}
|
|
2784
2932
|
if (checklist !== undefined) {
|
|
2785
2933
|
logInfo(
|
|
2786
2934
|
suppliedChecklist === undefined
|
|
@@ -2788,6 +2936,7 @@ async function runEvidence(
|
|
|
2788
2936
|
: 'checklist folded in below the comparison',
|
|
2789
2937
|
)
|
|
2790
2938
|
}
|
|
2939
|
+
warnUncaptured()
|
|
2791
2940
|
|
|
2792
2941
|
outro()
|
|
2793
2942
|
|
|
@@ -2803,6 +2952,7 @@ async function runEvidence(
|
|
|
2803
2952
|
states,
|
|
2804
2953
|
...(sized.unread.length > 0 && { widthUnread: sized.unread }),
|
|
2805
2954
|
...(uiPass !== undefined && { uiPass }),
|
|
2955
|
+
...uncapturedField,
|
|
2806
2956
|
...(commentId !== undefined && { commentId }),
|
|
2807
2957
|
...carried,
|
|
2808
2958
|
})}\n`,
|
|
@@ -3664,6 +3814,7 @@ function framesArgumentRefusal(
|
|
|
3664
3814
|
): FramesCommandRefusal | undefined {
|
|
3665
3815
|
const modes = [
|
|
3666
3816
|
opts.add !== undefined,
|
|
3817
|
+
opts.reel !== undefined,
|
|
3667
3818
|
opts.drop === true,
|
|
3668
3819
|
opts.prune !== undefined,
|
|
3669
3820
|
]
|
|
@@ -3673,6 +3824,12 @@ function framesArgumentRefusal(
|
|
|
3673
3824
|
return POSITIVE_WHOLE.test(opts.prune) ? undefined : 'bad-days'
|
|
3674
3825
|
}
|
|
3675
3826
|
if (number === undefined || !POSITIVE_WHOLE.test(number)) return 'no-number'
|
|
3827
|
+
if (
|
|
3828
|
+
opts.reel !== undefined &&
|
|
3829
|
+
(opts.box !== undefined || opts.reference !== undefined)
|
|
3830
|
+
) {
|
|
3831
|
+
return 'bad-box'
|
|
3832
|
+
}
|
|
3676
3833
|
if (
|
|
3677
3834
|
opts.add !== undefined &&
|
|
3678
3835
|
(opts.box === undefined) === (opts.reference === undefined)
|
|
@@ -3689,7 +3846,7 @@ function framesArgumentRefusal(
|
|
|
3689
3846
|
}
|
|
3690
3847
|
if (opts.head !== undefined && !isFrameHead(opts.head)) return 'bad-head'
|
|
3691
3848
|
if (
|
|
3692
|
-
opts.add !== undefined &&
|
|
3849
|
+
(opts.add !== undefined || opts.reel !== undefined) &&
|
|
3693
3850
|
(opts.pass === undefined || !isFramePass(opts.pass))
|
|
3694
3851
|
) {
|
|
3695
3852
|
return 'bad-pass'
|
|
@@ -3757,6 +3914,10 @@ async function runFrames(
|
|
|
3757
3914
|
if (opts.add !== undefined && bytes === undefined) {
|
|
3758
3915
|
return refuseFrames('unreadable-frame')
|
|
3759
3916
|
}
|
|
3917
|
+
const reel =
|
|
3918
|
+
opts.reel === undefined ? undefined : await readReel(resolve(opts.reel))
|
|
3919
|
+
if (reel?.kind === 'unreadable') return refuseFrames('unreadable-reel')
|
|
3920
|
+
if (reel?.kind === 'too-large') return refuseFrames('reel-too-large')
|
|
3760
3921
|
|
|
3761
3922
|
if (Bun.which('gh') === null) return refuseFrames('gh-missing')
|
|
3762
3923
|
const runner = ghApiRunner(root)
|
|
@@ -3832,14 +3993,19 @@ async function runFrames(
|
|
|
3832
3993
|
box,
|
|
3833
3994
|
reference,
|
|
3834
3995
|
theme,
|
|
3835
|
-
|
|
3996
|
+
reel: reel !== undefined,
|
|
3997
|
+
bytes: reel?.bytes ?? bytes ?? new Uint8Array(),
|
|
3836
3998
|
})
|
|
3837
3999
|
if (result.kind === 'refused') return refuseWrite(result.reason)
|
|
3838
4000
|
const link = frameLink(repo, result.path)
|
|
3839
4001
|
return finish(
|
|
3840
4002
|
{
|
|
3841
4003
|
number: pull,
|
|
3842
|
-
...(
|
|
4004
|
+
...(reel !== undefined
|
|
4005
|
+
? { reel: true }
|
|
4006
|
+
: box === undefined
|
|
4007
|
+
? { reference }
|
|
4008
|
+
: { box }),
|
|
3843
4009
|
...(theme === undefined ? {} : { theme }),
|
|
3844
4010
|
head,
|
|
3845
4011
|
pass,
|
|
@@ -4485,6 +4651,7 @@ async function runDriveCopy(
|
|
|
4485
4651
|
target.worktree,
|
|
4486
4652
|
copyRoot,
|
|
4487
4653
|
config.config.writes,
|
|
4654
|
+
config.config.keep,
|
|
4488
4655
|
)
|
|
4489
4656
|
logStep('Copied')
|
|
4490
4657
|
logInfo(`${config.config.env}=${copyRoot}`)
|