@repo-os/repoos 0.5.70 → 0.5.71
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/dist/.build-info.json +2 -2
- package/dist/.build-stamp.json +1 -1
- package/dist/cli/help.js +2 -2
- package/dist/cli/task-flags.js +16 -0
- package/dist/commands/control-api.js +23 -4
- package/dist/commands/init.js +19 -0
- package/dist/commands/screenshot-flag.d.ts +36 -0
- package/dist/commands/screenshot-flag.js +99 -0
- package/dist/commands/shot.d.ts +10 -0
- package/dist/commands/shot.js +129 -32
- package/dist/commands/tasks.js +118 -5
- package/dist/core/budget-allowlist.d.ts +43 -0
- package/dist/core/budget-allowlist.js +83 -0
- package/dist/core/config.d.ts +8 -1
- package/dist/core/config.js +176 -1
- package/dist/core/review-passes.d.ts +1 -1
- package/dist/core/review-passes.js +7 -1
- package/dist/core/review-verification-config.d.ts +36 -0
- package/dist/core/review-verification-config.js +36 -0
- package/dist/core/review-verification.d.ts +59 -1
- package/dist/core/review-verification.js +132 -19
- package/dist/core/review-verification.test.js +95 -0
- package/dist/core/shot-command.d.ts +72 -0
- package/dist/core/shot-command.js +265 -0
- package/dist/core/shot-fixtures.d.ts +4 -2
- package/dist/core/shot-fixtures.js +5 -0
- package/dist/core/shot-page.d.ts +3 -1
- package/dist/core/shot-plan.d.ts +52 -3
- package/dist/core/shot-plan.js +133 -18
- package/dist/core/shot-targets.d.ts +35 -2
- package/dist/core/shot-targets.js +118 -6
- package/dist/core/types.d.ts +127 -0
- package/dist/core/types.js +2 -0
- package/dist/server/agent-model-allowlist.d.ts +30 -0
- package/dist/server/agent-model-allowlist.js +35 -0
- package/dist/server/agents.js +2 -0
- package/dist/server/local-token.d.ts +11 -0
- package/dist/server/local-token.js +22 -0
- package/dist/server/review-verification-gather.js +44 -15
- package/dist/server/review.d.ts +16 -0
- package/dist/server/review.js +140 -29
- package/dist/server/routes/config.js +43 -1
- package/dist/server/routes/tasks.js +48 -2
- package/dist/server/shot-capture.d.ts +67 -1
- package/dist/server/shot-capture.js +190 -15
- package/dist/server/shot-context.d.ts +1 -1
- package/dist/server/shot-context.js +7 -2
- package/dist/server/shots.d.ts +8 -0
- package/dist/server/shots.js +3 -0
- package/dist/server/ui-handoff-gate.d.ts +27 -2
- package/dist/server/ui-handoff-gate.js +102 -18
- package/dist/ui/assets/AgentModelControl-D_Idhaon.js +1 -0
- package/dist/ui/assets/AgentsView-Bsp_kUpY.js +1 -0
- package/dist/ui/assets/{AgentsView-kKt4CrDo.css → AgentsView-BvMAuoAh.css} +1 -1
- package/dist/ui/assets/{ChecksView-Ch8Ncat5.js → ChecksView-vW_M6sZs.js} +0 -0
- package/dist/ui/assets/ContextView-BsRhYLEM.js +4 -0
- package/dist/ui/assets/DashboardView-DLHWXZvB.js +1 -0
- package/dist/ui/assets/{DiffView-DeZzB47D.js → DiffView-CgUV2isa.js} +1 -1
- package/dist/ui/assets/InputsView-q5EU9qzR.js +1 -0
- package/dist/ui/assets/SearchOverlay-BgKVgkJE.js +1 -0
- package/dist/ui/assets/{SettingsView-BN-H2QBT.js → SettingsView-DiypqAsv.js} +2 -2
- package/dist/ui/assets/StoriesView-DM6B6MJf.js +1 -0
- package/dist/ui/assets/{WorkView-B4J9uqPn.js → WorkView-D6PNGvIY.js} +1 -1
- package/dist/ui/assets/{abnfDiagram-VCTEODGH-DrTaHBvQ.js → abnfDiagram-VCTEODGH-Bd-cBM60.js} +1 -1
- package/dist/ui/assets/{architectureDiagram-5GKGNRK7-4pbmksoa.js → architectureDiagram-5GKGNRK7-CipYgUsO.js} +1 -1
- package/dist/ui/assets/{blockDiagram-I7D4REHJ-DotXLktk.js → blockDiagram-I7D4REHJ-f_cW_0BB.js} +1 -1
- package/dist/ui/assets/{c4Diagram-7LVT6UL2-CX4XAag_.js → c4Diagram-7LVT6UL2-Br35UBf0.js} +1 -1
- package/dist/ui/assets/channel-HBsTnkcF.js +1 -0
- package/dist/ui/assets/{chunk-2E4U76K2-DBwbHQrf.js → chunk-2E4U76K2-DkDkPJbl.js} +1 -1
- package/dist/ui/assets/{chunk-4HAMMTFA-CC2YIavJ.js → chunk-4HAMMTFA-B5OLaaS5.js} +1 -1
- package/dist/ui/assets/{chunk-75Z2AOVW-GQ3dA3RS.js → chunk-75Z2AOVW-BiOKSVCI.js} +1 -1
- package/dist/ui/assets/{chunk-CLGD4ZFX-DJeOtdRW.js → chunk-CLGD4ZFX-BurZYOsg.js} +1 -1
- package/dist/ui/assets/{chunk-DU6HZSFF-Dth3AgVP.js → chunk-DU6HZSFF-BGDoa0O8.js} +1 -1
- package/dist/ui/assets/{chunk-F27PBJKO-CUKJ_1GV.js → chunk-F27PBJKO-DxIrKenk.js} +1 -1
- package/dist/ui/assets/{chunk-GMAD6QVW-DUokwNqt.js → chunk-GMAD6QVW-COGgFsG-.js} +1 -1
- package/dist/ui/assets/{chunk-GVQU2GXP--wyu3z_M.js → chunk-GVQU2GXP-ChFWBbq9.js} +1 -1
- package/dist/ui/assets/{chunk-IMKFNOWR-DKnHgEdS.js → chunk-IMKFNOWR-wsEQiYt4.js} +1 -1
- package/dist/ui/assets/{chunk-L3NEJ4N5-vio-PK5r.js → chunk-L3NEJ4N5-Cr1dKnZJ.js} +1 -1
- package/dist/ui/assets/{chunk-OSK3NFVY-BF4uJ1Wv.js → chunk-OSK3NFVY-cSWMlBxc.js} +1 -1
- package/dist/ui/assets/{chunk-P2QGCYS3-B9OHNM4g.js → chunk-P2QGCYS3-D1sKUill.js} +1 -1
- package/dist/ui/assets/{chunk-POPQ4Y6H-DfPkV4K8.js → chunk-POPQ4Y6H-DluqWxSK.js} +1 -1
- package/dist/ui/assets/{chunk-PWAF6VOD-C6pQpja9.js → chunk-PWAF6VOD-kdXLYxnu.js} +1 -1
- package/dist/ui/assets/{chunk-SHT3W25Y-D1FyTkkv.js → chunk-SHT3W25Y-Dm81UtEL.js} +1 -1
- package/dist/ui/assets/{chunk-SVP7TREG-DtQlvhPk.js → chunk-SVP7TREG-1RGCR-KK.js} +1 -1
- package/dist/ui/assets/{chunk-TICWLB2K-BpnCR3pz.js → chunk-TICWLB2K-BwkWIbAw.js} +1 -1
- package/dist/ui/assets/chunk-TLUHSLCS-CCcqBXqp.js +2 -0
- package/dist/ui/assets/classDiagram-ZZMXUADV-D_W2hGoN.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-D_W2hGoN.js +1 -0
- package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-D3SEdRnG.js → cynefinDiagram-5FMLGOSQ-BDPeRvBt.js} +1 -1
- package/dist/ui/assets/{dagre-GXQ25YYZ-C8gQG-lD.js → dagre-GXQ25YYZ-BqmYPMiI.js} +1 -1
- package/dist/ui/assets/{diagram-S7CK7UJ4-Czu_0uu0.js → diagram-S7CK7UJ4-DXQbSart.js} +1 -1
- package/dist/ui/assets/{diagram-UQ7AKVKN-ViXph8nh.js → diagram-UQ7AKVKN-8Iu6RcsI.js} +1 -1
- package/dist/ui/assets/{diagram-VSXAHHWV-C6BtTSHW.js → diagram-VSXAHHWV-D1vUZYDf.js} +1 -1
- package/dist/ui/assets/{diagram-VX7I27RA-TbbO1fXg.js → diagram-VX7I27RA-D8vq2Cmg.js} +1 -1
- package/dist/ui/assets/{diagram-Z3DM3KII-BO9R4sQs.js → diagram-Z3DM3KII-w6p5EbNr.js} +1 -1
- package/dist/ui/assets/{ebnfDiagram-PWID7BFC-aKXt7bQt.js → ebnfDiagram-PWID7BFC-BYjT4y_m.js} +1 -1
- package/dist/ui/assets/{erDiagram-RLTQ6QDP-47HSz_yo.js → erDiagram-RLTQ6QDP-CEELItZa.js} +1 -1
- package/dist/ui/assets/flowDiagram-HODETNUW-nKF0KGmt.js +1 -0
- package/dist/ui/assets/format-B31EJAKm.js +1 -0
- package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-CLH5Zx6B.js → ganttDiagram-EL5Y4UJY-j3mvcMyd.js} +1 -1
- package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-BOxdVoS7.js → gitGraphDiagram-WWUBYQGX-BfEOsCSC.js} +1 -1
- package/dist/ui/assets/{index-ChEmz2Dy.css → index-CDFBWswc.css} +1 -1
- package/dist/ui/assets/index-DKMEICXY.js +57 -0
- package/dist/ui/assets/{infoDiagram-27XIBGKW-CuAZPnDi.js → infoDiagram-27XIBGKW-CoKHZI4g.js} +1 -1
- package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-B7Eq3ePc.js → ishikawaDiagram-5VMMS53U-BzLGZ78I.js} +1 -1
- package/dist/ui/assets/{journeyDiagram-3NMN7TZE-BhW2Sqpt.js → journeyDiagram-3NMN7TZE-DasBgTnF.js} +1 -1
- package/dist/ui/assets/{kanban-definition-UXKFOSKX-Cw_klMxM.js → kanban-definition-UXKFOSKX-Bi2G6162.js} +1 -1
- package/dist/ui/assets/{line-CX-sssw_.js → line-BD89T94A.js} +1 -1
- package/dist/ui/assets/{mermaid-parser.core-DU36LHRU.js → mermaid-parser.core-CmPHHcqL.js} +1 -1
- package/dist/ui/assets/mermaid.core-B22Uko0X.js +11 -0
- package/dist/ui/assets/{mindmap-definition-YA3MSWOX-NMTZuXGJ.js → mindmap-definition-YA3MSWOX-CmkalisH.js} +1 -1
- package/dist/ui/assets/{pegDiagram-XKGWAZYB-B9DqL68I.js → pegDiagram-XKGWAZYB-B7n3kqND.js} +1 -1
- package/dist/ui/assets/{pieDiagram-E7YTZNPT-EPN4fJwb.js → pieDiagram-E7YTZNPT-Dc8m07AW.js} +1 -1
- package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-B1vP5hUF.js → quadrantDiagram-AXDQQJYC-C_-Mk5Ep.js} +1 -1
- package/dist/ui/assets/{railroadDiagram-O6MQD6OU-q57-HBoV.js → railroadDiagram-O6MQD6OU-DmT5wkuj.js} +1 -1
- package/dist/ui/assets/{requirementDiagram-BXWQKSXE-B1nzxnDE.js → requirementDiagram-BXWQKSXE-BERE-gY9.js} +1 -1
- package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-CvGzdKI2.js → sankeyDiagram-P5KCCOFB-C51xkN9v.js} +1 -1
- package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-G73REJUS.js → sequenceDiagram-WJ2MYXX4-DJYgfPQJ.js} +1 -1
- package/dist/ui/assets/{stateDiagram-D77RDMKH-CRW4gM0e.js → stateDiagram-D77RDMKH-BdMTpabl.js} +1 -1
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BWp5c8Bl.js +1 -0
- package/dist/ui/assets/{swimlanes-42K2YHIH-CE91Y-lT.js → swimlanes-42K2YHIH-By3v_NQb.js} +1 -1
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-D4WKOkfS.js +8 -0
- package/dist/ui/assets/text-insertion-sXVs8rKF.js +1 -0
- package/dist/ui/assets/{timeline-definition-24CTP7MA-DVw_tCzX.js → timeline-definition-24CTP7MA-DgqMOYEl.js} +1 -1
- package/dist/ui/assets/{vennDiagram-4TSXK5OY-gBaTlcOW.js → vennDiagram-4TSXK5OY-Cfy1llHd.js} +1 -1
- package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-B9w0GtT1.js → wardleyDiagram-VM6X3IG4-Z5UvJKtg.js} +1 -1
- package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-D2o0fTLK.js → xychartDiagram-S5SC5T6Z-D4EkuQPv.js} +1 -1
- package/dist/ui/index.html +6 -3
- package/dist/ui/sw.js +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/AgentsView-YS1AZ1v2.js +0 -1
- package/dist/ui/assets/ContextView-DGMk39_r.js +0 -4
- package/dist/ui/assets/DashboardView-B_9hD8tR.js +0 -1
- package/dist/ui/assets/InputsView-0b4Sjg0Q.js +0 -1
- package/dist/ui/assets/SearchOverlay-D44KxIbl.js +0 -1
- package/dist/ui/assets/StoriesView-CnAlxgkT.js +0 -1
- package/dist/ui/assets/channel-BrKb638S.js +0 -1
- package/dist/ui/assets/chunk-TLUHSLCS-BeK-nkSE.js +0 -2
- package/dist/ui/assets/classDiagram-ZZMXUADV-BO_a0cvs.js +0 -1
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BO_a0cvs.js +0 -1
- package/dist/ui/assets/flowDiagram-HODETNUW-DPrFUvzg.js +0 -1
- package/dist/ui/assets/index-DcF5KN6q.js +0 -57
- package/dist/ui/assets/mermaid.core-DKxErDRT.js +0 -11
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-C8VGcrPt.js +0 -1
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-CJy_fpiy.js +0 -8
package/dist/.build-info.json
CHANGED
package/dist/.build-stamp.json
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -12,7 +12,7 @@ import { c } from "./colors.js";
|
|
|
12
12
|
import { table, termWidth, visibleWidth, wrap } from "./layout.js";
|
|
13
13
|
import { NEW_FLAG_HELP, UPDATE_FLAG_HELP, flagSummary, flagUsageTokens } from "./task-flags.js";
|
|
14
14
|
/** Commands whose own argument parser already handles `--help`. */
|
|
15
|
-
export const SELF_HELP_COMMANDS = new Set(["support", "tunnel", "shot", "uninstall"]);
|
|
15
|
+
export const SELF_HELP_COMMANDS = new Set(["support", "tunnel", "shot", "uninstall", "override"]);
|
|
16
16
|
const GROUPS = [
|
|
17
17
|
{
|
|
18
18
|
name: "Setup",
|
|
@@ -107,7 +107,7 @@ const GROUPS = [
|
|
|
107
107
|
{
|
|
108
108
|
name: "override",
|
|
109
109
|
usage: "override <id> [--cli <name>] [--model <model>] [--json] [--port N]",
|
|
110
|
-
desc: "Set per-task engineer overrides (PATCH /api/tasks/:id)",
|
|
110
|
+
desc: "Set per-task engineer overrides (PATCH /api/tasks/:id). --help prints the agent-driver budget allowlist",
|
|
111
111
|
flags: "--cli · --model · --json · --port",
|
|
112
112
|
},
|
|
113
113
|
{
|
package/dist/cli/task-flags.js
CHANGED
|
@@ -33,6 +33,14 @@ export const NEW_FLAG_HELP = {
|
|
|
33
33
|
paths: { value: "a,b", desc: "Files the task touches, e.g. src/a.ts,src/b.ts" },
|
|
34
34
|
body: { value: '"..."|-', desc: "Task body; `-` reads it from stdin" },
|
|
35
35
|
shots: { value: '"<JSON list>"|-', desc: "Declare the ## Shots section (validated JSON)" },
|
|
36
|
+
screenshot: {
|
|
37
|
+
value: "path.png",
|
|
38
|
+
desc: "Attach a local image to ## Screenshots (repeatable)",
|
|
39
|
+
},
|
|
40
|
+
caption: {
|
|
41
|
+
value: '"..."',
|
|
42
|
+
desc: "Alt text for a single --screenshot image",
|
|
43
|
+
},
|
|
36
44
|
};
|
|
37
45
|
/** Every flag `repoos update` accepts, in help order. */
|
|
38
46
|
export const UPDATE_FLAG_HELP = {
|
|
@@ -52,6 +60,14 @@ export const UPDATE_FLAG_HELP = {
|
|
|
52
60
|
questions: { value: '"Q1\\nQ2"', desc: "Open questions for the human (newline or JSON list)" },
|
|
53
61
|
"clear-questions": { desc: "Clear the recorded questions" },
|
|
54
62
|
shots: { value: '"<JSON list>"|-', desc: "Declare the ## Shots section (validated JSON)" },
|
|
63
|
+
screenshot: {
|
|
64
|
+
value: "path.png",
|
|
65
|
+
desc: "Attach a local image to ## Screenshots (repeatable)",
|
|
66
|
+
},
|
|
67
|
+
caption: {
|
|
68
|
+
value: '"..."',
|
|
69
|
+
desc: "Alt text for a single --screenshot image",
|
|
70
|
+
},
|
|
55
71
|
section: { value: '"heading"', desc: "Replace/create one ## section (pair with --section-body)" },
|
|
56
72
|
"section-body": { value: '"..."|-', desc: "Body for --section; `-` reads it from stdin" },
|
|
57
73
|
force: { desc: "Allow a --body replace that drops spec headings" },
|
|
@@ -31,6 +31,21 @@ function parseCommonFlags(args) {
|
|
|
31
31
|
function printJson(data) {
|
|
32
32
|
console.log(JSON.stringify(data, null, 2));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Print the agent-driver budget allowlist so a driver can choose an allowed
|
|
36
|
+
* pair before running `repoos override` (#0764). Reads the local config file —
|
|
37
|
+
* no server round-trip.
|
|
38
|
+
*/
|
|
39
|
+
function printBudgetAllowlist(config) {
|
|
40
|
+
const rows = config.budget?.allowlist ?? [];
|
|
41
|
+
if (!rows.length) {
|
|
42
|
+
console.log(c.dim(" Budget allowlist: (empty — agent drivers are not restricted to any agent+model pair)"));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(c.dim(" Budget allowlist for agent drivers — allowed agent + model pairs:"));
|
|
46
|
+
for (const row of rows)
|
|
47
|
+
console.log(` · ${row.cli} + ${row.models.join(", ")}`);
|
|
48
|
+
}
|
|
34
49
|
function printErr(message) {
|
|
35
50
|
console.error(c.red(" ✗ ") + message);
|
|
36
51
|
}
|
|
@@ -304,10 +319,14 @@ export async function cmdOverride(args) {
|
|
|
304
319
|
printErr(error);
|
|
305
320
|
return 1;
|
|
306
321
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
const wantsHelp = rest.includes("--help") || rest.includes("-h");
|
|
323
|
+
const id = rest.find((a) => !a.startsWith("-"));
|
|
324
|
+
if (!id || wantsHelp) {
|
|
325
|
+
const { config } = resolveControlPlaneBase({ port: opts.port });
|
|
326
|
+
console.log(c.cyan("Usage:") +
|
|
327
|
+
" repoos override <id> [--cli <name>] [--model <model>] [--json] [--port N]");
|
|
328
|
+
printBudgetAllowlist(config);
|
|
329
|
+
return wantsHelp ? 0 : 1;
|
|
311
330
|
}
|
|
312
331
|
let cli;
|
|
313
332
|
let model;
|
package/dist/commands/init.js
CHANGED
|
@@ -253,6 +253,15 @@ a candidate merge. Do not erase a task's branch metadata to force this path.
|
|
|
253
253
|
must use those same RepoOS CLI commands or HTTP API endpoints for task body,
|
|
254
254
|
metadata, and status changes; it must never edit task Markdown directly.
|
|
255
255
|
- **Never** deploy to production without human sign-off.
|
|
256
|
+
- **Attach the human's screenshots to the task.** When a task comes from a
|
|
257
|
+
request that included images, pass every one with
|
|
258
|
+
\`repoos new "<title>" --screenshot path.png\` (repeat \`--screenshot\` per
|
|
259
|
+
image) or \`repoos update <id> --screenshot path.png\` (\`--caption "..."\`
|
|
260
|
+
labels a single image). The command stores the files under the gitignored
|
|
261
|
+
\`${workDir}/.attachments/<id>/\` and writes the task's \`## Screenshots\`
|
|
262
|
+
section itself — never \`git add\` the pixels, never hand-copy them, never
|
|
263
|
+
edit that section by hand. Say in your reply which images you attached.
|
|
264
|
+
An image the human gave you is an image that lands on the task.
|
|
256
265
|
- Keep frontmatter tidy; \`repoos\` will normalize key order on write.
|
|
257
266
|
- One task = one focused worktree.
|
|
258
267
|
- **Never invent evidence.** Do not fabricate device sessions, live API results,
|
|
@@ -452,6 +461,16 @@ function previewTargetRows(areas) {
|
|
|
452
461
|
`# areas = ${areaList}`,
|
|
453
462
|
`# command = "bun run dev --port {port} --host {host}"`,
|
|
454
463
|
`# readyTimeoutMs = 240000 # raise for a command that also builds`,
|
|
464
|
+
"",
|
|
465
|
+
"# No dev server (native macOS, iOS/Android, desktop)? Declare a command",
|
|
466
|
+
"# capture target instead — the command renders image files into",
|
|
467
|
+
"# REPOOS_SHOT_DIR and RepoOS collects them as the task's evidence shots.",
|
|
468
|
+
"# [[shot.targets]]",
|
|
469
|
+
`# name = "macOS app"`,
|
|
470
|
+
`# areas = ${areaList}`,
|
|
471
|
+
`# paths = ["Sources/**"] # changed files that make this the target`,
|
|
472
|
+
`# command = "swift test --filter RenderShots"`,
|
|
473
|
+
`# outputGlob = ["*.png"]`,
|
|
455
474
|
].join("\n");
|
|
456
475
|
}
|
|
457
476
|
const ENV_EXAMPLE = `# Copy to .env and fill in what you need — .env is gitignored, this file is
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RepoOSConfig, Task } from "../core/types.js";
|
|
2
|
+
import { type ScreenshotMeta } from "../server/attachments.js";
|
|
3
|
+
/** One local image read from disk, ready for `saveScreenshot`. */
|
|
4
|
+
export interface ScreenshotInput {
|
|
5
|
+
name: string;
|
|
6
|
+
mime: string;
|
|
7
|
+
/** Base64-encoded file bytes. */
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read and validate one local image file. Validation mirrors the upload route:
|
|
12
|
+
* the extension must map to an allowed MIME type, the file must exist and be a
|
|
13
|
+
* regular file, be non-empty, and be within the size limit. Never throws —
|
|
14
|
+
* problems come back as `{ error }` with the offending path named.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readScreenshotFile(filePath: string): ScreenshotInput | {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
/** Read every `--screenshot` path, failing fast on the first bad one. */
|
|
20
|
+
export declare function readScreenshotFiles(paths: string[]): {
|
|
21
|
+
inputs: ScreenshotInput[];
|
|
22
|
+
} | {
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Store every local image on a task and extend its `## Screenshots` section in
|
|
27
|
+
* one patch (numbering continues from whatever the task already has). The
|
|
28
|
+
* images are gitignored storage under `work/.attachments/<id>/`; only the
|
|
29
|
+
* markdown links land in the task file. Reports the first failure rather than
|
|
30
|
+
* leaving a partial attach silent, and never half-writes the body.
|
|
31
|
+
*/
|
|
32
|
+
export declare function attachScreenshots(config: RepoOSConfig, task: Task, paths: string[], caption?: string): {
|
|
33
|
+
metas: ScreenshotMeta[];
|
|
34
|
+
} | {
|
|
35
|
+
error: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--screenshot` support for `repoos new` / `repoos update` (#0763).
|
|
3
|
+
*
|
|
4
|
+
* An agent that is handed images in chat needs a way to attach them when it
|
|
5
|
+
* creates or updates the task, without hand-copying files into
|
|
6
|
+
* `work/.attachments/<id>/` and guessing at the `## Screenshots` markdown. This
|
|
7
|
+
* module is the one place that turns local image paths into stored attachments:
|
|
8
|
+
* it validates each file against the SAME allowlist and size limit the HTTP
|
|
9
|
+
* upload route uses ({@link saveScreenshot}), stores them through the same
|
|
10
|
+
* storage helper, and extends the task body through the same
|
|
11
|
+
* `patchTaskFile({ addScreenshots })` path the API route uses. No storage logic
|
|
12
|
+
* is duplicated here.
|
|
13
|
+
*
|
|
14
|
+
* The write is local, exactly like the rest of `repoos new` / `repoos update`:
|
|
15
|
+
* it works with no server running, and equally against a running server (which
|
|
16
|
+
* picks the file change up through its work-dir watcher). A caller that wants
|
|
17
|
+
* the server to have the task in the same request can post `attachments` to
|
|
18
|
+
* `POST /api/tasks` instead — both paths land in the same storage.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { extname } from "node:path";
|
|
22
|
+
import { MAX_SCREENSHOT_BYTES, mimeForExtension, saveScreenshot, } from "../server/attachments.js";
|
|
23
|
+
import { patchTaskFile } from "../server/write.js";
|
|
24
|
+
/** How many bytes are too many, as human words for the error message. */
|
|
25
|
+
function maxMb() {
|
|
26
|
+
return Math.round(MAX_SCREENSHOT_BYTES / 1024 / 1024);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read and validate one local image file. Validation mirrors the upload route:
|
|
30
|
+
* the extension must map to an allowed MIME type, the file must exist and be a
|
|
31
|
+
* regular file, be non-empty, and be within the size limit. Never throws —
|
|
32
|
+
* problems come back as `{ error }` with the offending path named.
|
|
33
|
+
*/
|
|
34
|
+
export function readScreenshotFile(filePath) {
|
|
35
|
+
if (!filePath || filePath.trim() === "") {
|
|
36
|
+
return { error: "empty --screenshot path" };
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(filePath)) {
|
|
39
|
+
return { error: `image not found: ${filePath}` };
|
|
40
|
+
}
|
|
41
|
+
const stat = statSync(filePath);
|
|
42
|
+
if (!stat.isFile()) {
|
|
43
|
+
return { error: `not a file: ${filePath}` };
|
|
44
|
+
}
|
|
45
|
+
const mime = mimeForExtension(filePath);
|
|
46
|
+
if (!mime) {
|
|
47
|
+
return {
|
|
48
|
+
error: `unsupported image type "${extname(filePath) || filePath}" — supported: .png, .jpg, .jpeg, .gif, .webp, .avif, .bmp`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const bytes = readFileSync(filePath);
|
|
52
|
+
if (bytes.length === 0) {
|
|
53
|
+
return { error: `image is empty: ${filePath}` };
|
|
54
|
+
}
|
|
55
|
+
if (bytes.length > MAX_SCREENSHOT_BYTES) {
|
|
56
|
+
return { error: `image is too large (max ${maxMb()} MB): ${filePath}` };
|
|
57
|
+
}
|
|
58
|
+
return { name: filePath, mime, data: bytes.toString("base64") };
|
|
59
|
+
}
|
|
60
|
+
/** Read every `--screenshot` path, failing fast on the first bad one. */
|
|
61
|
+
export function readScreenshotFiles(paths) {
|
|
62
|
+
const inputs = [];
|
|
63
|
+
for (const path of paths) {
|
|
64
|
+
const result = readScreenshotFile(path);
|
|
65
|
+
if ("error" in result)
|
|
66
|
+
return { error: result.error };
|
|
67
|
+
inputs.push(result);
|
|
68
|
+
}
|
|
69
|
+
return { inputs };
|
|
70
|
+
}
|
|
71
|
+
/** Trim a caption into safe markdown alt text, or null when blank. */
|
|
72
|
+
function normalizeCaption(caption) {
|
|
73
|
+
const trimmed = (caption ?? "").replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
|
|
74
|
+
return trimmed || null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Store every local image on a task and extend its `## Screenshots` section in
|
|
78
|
+
* one patch (numbering continues from whatever the task already has). The
|
|
79
|
+
* images are gitignored storage under `work/.attachments/<id>/`; only the
|
|
80
|
+
* markdown links land in the task file. Reports the first failure rather than
|
|
81
|
+
* leaving a partial attach silent, and never half-writes the body.
|
|
82
|
+
*/
|
|
83
|
+
export function attachScreenshots(config, task, paths, caption) {
|
|
84
|
+
const read = readScreenshotFiles(paths);
|
|
85
|
+
if ("error" in read)
|
|
86
|
+
return { error: read.error };
|
|
87
|
+
const label = normalizeCaption(caption);
|
|
88
|
+
const metas = [];
|
|
89
|
+
for (const input of read.inputs) {
|
|
90
|
+
const saved = saveScreenshot(config, task, input);
|
|
91
|
+
if ("error" in saved)
|
|
92
|
+
return { error: saved.error };
|
|
93
|
+
metas.push(label ? { ...saved, name: label } : saved);
|
|
94
|
+
}
|
|
95
|
+
if (metas.length === 0)
|
|
96
|
+
return { metas };
|
|
97
|
+
patchTaskFile(config, task.absPath, { addScreenshots: metas });
|
|
98
|
+
return { metas };
|
|
99
|
+
}
|
package/dist/commands/shot.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ interface CmdShotOptions {
|
|
|
11
11
|
height: number;
|
|
12
12
|
};
|
|
13
13
|
fullPage: boolean;
|
|
14
|
+
/** Extra env for a command capture target (#0769), from repeated `--env KEY=VAL`. */
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
/** Per-entry output glob for a command capture target (#0769), from `--glob`. */
|
|
17
|
+
glob?: string;
|
|
14
18
|
/** Set when an argument is unknown or malformed; cmdShot prints and exits. */
|
|
15
19
|
error?: string;
|
|
16
20
|
/** Set for `-h`/`--help`; cmdShot prints usage and exits 0. */
|
|
@@ -38,6 +42,12 @@ export interface CliShotPlanInput {
|
|
|
38
42
|
matchedGlobs: ReadonlyMap<string, string[]>;
|
|
39
43
|
/** Targets whose matched files are docs content only (#0603). */
|
|
40
44
|
docsContentOnly: ReadonlySet<string>;
|
|
45
|
+
/** Command capture target names (#0761) — no route, run a command instead. */
|
|
46
|
+
commandTargets?: ReadonlySet<string>;
|
|
47
|
+
/** Extra env for a command capture target (#0769), from `--env KEY=VAL`. */
|
|
48
|
+
env?: Record<string, string>;
|
|
49
|
+
/** Per-entry output glob for a command capture target (#0769), from `--glob`. */
|
|
50
|
+
glob?: string;
|
|
41
51
|
}
|
|
42
52
|
/** The plan plus the visible notes `cmdShot` prints alongside it. */
|
|
43
53
|
export interface CliShotPlan {
|
package/dist/commands/shot.js
CHANGED
|
@@ -15,15 +15,18 @@
|
|
|
15
15
|
* overrides; area resolution is the fallback. Missing Playwright is a clean
|
|
16
16
|
* skip with install advice, not a crash.
|
|
17
17
|
*/
|
|
18
|
-
import { basename } from "node:path";
|
|
18
|
+
import { basename, extname, join } from "node:path";
|
|
19
|
+
import { rmSync } from "node:fs";
|
|
19
20
|
import { c } from "../cli/colors.js";
|
|
20
21
|
import { findRepoRoot, loadConfig, mainCheckoutRoot, resolveServePort } from "../core/config.js";
|
|
21
22
|
import { changedPathsVsBase, currentBranch } from "../core/git.js";
|
|
22
23
|
import { createRepoOS } from "../core/repoos.js";
|
|
23
24
|
import { shotCapturePageUrl } from "../core/shot-fixtures.js";
|
|
24
25
|
import { captureShotPage } from "../core/shot-page.js";
|
|
26
|
+
import { runShotCommand, slugName } from "../core/shot-command.js";
|
|
27
|
+
import { shotsDir } from "../server/shots.js";
|
|
25
28
|
import { buildCapturePlan, parseShotPlan, provenanceCaption, } from "../core/shot-plan.js";
|
|
26
|
-
import { describeTargetPathMatches, formatTargetList, resolveShotTargets, } from "../core/shot-targets.js";
|
|
29
|
+
import { describeShotTargetPathMatches, describeTargetPathMatches, formatTargetList, resolveShotTargets, shotTargetByName, } from "../core/shot-targets.js";
|
|
27
30
|
import { readServeLocks } from "./status.js";
|
|
28
31
|
import { readLocalCliToken } from "../server/local-token.js";
|
|
29
32
|
import { launchWebkit } from "./ui-harness.js";
|
|
@@ -74,6 +77,24 @@ export function parseShotArgs(args) {
|
|
|
74
77
|
return opts;
|
|
75
78
|
opts.selector = v;
|
|
76
79
|
}
|
|
80
|
+
else if (a === "--env") {
|
|
81
|
+
const v = value(a);
|
|
82
|
+
if (v === undefined)
|
|
83
|
+
return opts;
|
|
84
|
+
const eq = v.indexOf("=");
|
|
85
|
+
const key = eq > 0 ? v.slice(0, eq) : "";
|
|
86
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
87
|
+
opts.error = `--env expects KEY=VALUE with a valid variable name, got "${v}"`;
|
|
88
|
+
return opts;
|
|
89
|
+
}
|
|
90
|
+
opts.env = { ...opts.env, [key]: v.slice(eq + 1) };
|
|
91
|
+
}
|
|
92
|
+
else if (a === "--glob") {
|
|
93
|
+
const v = value(a);
|
|
94
|
+
if (v === undefined)
|
|
95
|
+
return opts;
|
|
96
|
+
opts.glob = v;
|
|
97
|
+
}
|
|
77
98
|
else if (a === "--task") {
|
|
78
99
|
const v = value(a);
|
|
79
100
|
if (v === undefined)
|
|
@@ -160,8 +181,11 @@ export function buildCliShotPlan(input) {
|
|
|
160
181
|
const built = buildCapturePlan(input.targets, declared.shots, {
|
|
161
182
|
matchedGlobs: input.matchedGlobs,
|
|
162
183
|
docsContentOnly: input.docsContentOnly,
|
|
184
|
+
...(input.commandTargets ? { commandTargets: input.commandTargets } : {}),
|
|
163
185
|
route: input.route,
|
|
164
186
|
selector: input.selector,
|
|
187
|
+
...(input.env ? { env: input.env } : {}),
|
|
188
|
+
...(input.glob ? { glob: input.glob } : {}),
|
|
165
189
|
});
|
|
166
190
|
return {
|
|
167
191
|
entries: built.entries,
|
|
@@ -239,11 +263,12 @@ async function uploadShot(port, taskId, token, input) {
|
|
|
239
263
|
headers: cliHeaders(token, true),
|
|
240
264
|
body: JSON.stringify({
|
|
241
265
|
target: input.target,
|
|
242
|
-
route: input.route,
|
|
266
|
+
...(input.route ? { route: input.route } : {}),
|
|
267
|
+
...(input.command ? { command: input.command } : {}),
|
|
243
268
|
...(input.label ? { label: input.label } : {}),
|
|
244
269
|
...(input.provenance ? { provenance: input.provenance } : {}),
|
|
245
|
-
name: input.target,
|
|
246
|
-
mime: "image/png",
|
|
270
|
+
name: input.name ?? input.target,
|
|
271
|
+
mime: input.mime ?? "image/png",
|
|
247
272
|
data: input.data,
|
|
248
273
|
...(input.warnings?.length ? { warnings: input.warnings } : {}),
|
|
249
274
|
}),
|
|
@@ -287,6 +312,14 @@ export async function cmdShot(args) {
|
|
|
287
312
|
target's paths; diffs touching just tests or task notes stand down with a
|
|
288
313
|
visible "shots: skipped — no UI change to capture" note instead.
|
|
289
314
|
|
|
315
|
+
A repo with no dev server (a native macOS, iOS/Android or desktop app) can
|
|
316
|
+
declare a command capture target in repoos.toml as a [[shot.targets]] row —
|
|
317
|
+
a command that renders image files into REPOOS_SHOT_DIR. --target <name>
|
|
318
|
+
selects one, and its images are stored like any other shot; no browser is
|
|
319
|
+
needed. --env KEY=VALUE and --glob narrow what such a target renders and which
|
|
320
|
+
produced files are kept (a declared ## Shots entry can carry the same as
|
|
321
|
+
"env"/"args"/"glob"). See user-docs/configuration.md.
|
|
322
|
+
|
|
290
323
|
An explicit <route> or --selector replaces the declared list outright, and
|
|
291
324
|
applies to every resolved target: declared entries carry steps, a highlight
|
|
292
325
|
and a label written for their own route, so they are never re-pointed at a
|
|
@@ -300,6 +333,9 @@ export async function cmdShot(args) {
|
|
|
300
333
|
Flags:
|
|
301
334
|
--target <name> Preview target to capture (overrides path/area resolution)
|
|
302
335
|
--selector <css> Capture one element instead of the whole page
|
|
336
|
+
--env KEY=VAL (command target) pass an env var to the capture command;
|
|
337
|
+
repeat per variable, e.g. --env DIVOT_SNAPSHOT_ONLY=worldclock
|
|
338
|
+
--glob <glob> (command target) collect only produced files matching this glob
|
|
303
339
|
--task <id> Task to shoot (defaults to the current worktree's branch)
|
|
304
340
|
--base <ref> Base branch for changed-path resolution (default "main")
|
|
305
341
|
--wait <ms> Settle time after load before capturing (default 900)
|
|
@@ -309,7 +345,7 @@ export async function cmdShot(args) {
|
|
|
309
345
|
}
|
|
310
346
|
if (opts.error) {
|
|
311
347
|
console.error(c.red(" ✗ ") + opts.error);
|
|
312
|
-
console.error(c.dim(" usage: repoos shot [<route|url>] [--target <name>] [--selector <css>]"));
|
|
348
|
+
console.error(c.dim(" usage: repoos shot [<route|url>] [--target <name>] [--selector <css>] [--env K=V] [--glob <glob>]"));
|
|
313
349
|
return 1;
|
|
314
350
|
}
|
|
315
351
|
const cwdRoot = findRepoRoot();
|
|
@@ -355,7 +391,7 @@ export async function cmdShot(args) {
|
|
|
355
391
|
console.error(c.red(" ✗ ") + `Base ref "${opts.base}" is not a commit — cannot resolve changed paths.`);
|
|
356
392
|
return 1;
|
|
357
393
|
}
|
|
358
|
-
const resolution = resolveShotTargets(worktreeConfig.preview, task.area, changed, opts.target);
|
|
394
|
+
const resolution = resolveShotTargets(worktreeConfig.preview, task.area, changed, opts.target, worktreeConfig.shot);
|
|
359
395
|
if (resolution.names.length === 0) {
|
|
360
396
|
console.error(c.red(" ✗ ") + (resolution.reason ?? "No preview target resolved."));
|
|
361
397
|
if (resolution.detected.length > 0) {
|
|
@@ -386,6 +422,12 @@ export async function cmdShot(args) {
|
|
|
386
422
|
if (match.contentOnly)
|
|
387
423
|
docsContentOnly.add(match.target);
|
|
388
424
|
}
|
|
425
|
+
for (const match of describeShotTargetPathMatches(worktreeConfig.shot, changed)) {
|
|
426
|
+
matchedGlobs.set(match.target, match.globs);
|
|
427
|
+
if (match.contentOnly)
|
|
428
|
+
docsContentOnly.add(match.target);
|
|
429
|
+
}
|
|
430
|
+
const commandTargets = new Set((worktreeConfig.shot?.targets ?? []).map((t) => t.name));
|
|
389
431
|
// #0610: the requested route/selector is part of the plan, not a separate
|
|
390
432
|
// candidate that lost to the fallback's `/`.
|
|
391
433
|
const built = buildCliShotPlan({
|
|
@@ -395,6 +437,9 @@ export async function cmdShot(args) {
|
|
|
395
437
|
targets,
|
|
396
438
|
matchedGlobs,
|
|
397
439
|
docsContentOnly,
|
|
440
|
+
commandTargets,
|
|
441
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
442
|
+
...(opts.glob ? { glob: opts.glob } : {}),
|
|
398
443
|
});
|
|
399
444
|
for (const error of built.declaredErrors) {
|
|
400
445
|
console.error(c.yellow(" · ") + `declared shot list: ${error}`);
|
|
@@ -431,29 +476,34 @@ export async function cmdShot(args) {
|
|
|
431
476
|
`RepoOS server is not reachable on ${baseUrl}. Start it with \`repoos serve\`.`);
|
|
432
477
|
return 1;
|
|
433
478
|
}
|
|
479
|
+
const commandTargets = new Set((worktreeConfig.shot?.targets ?? []).map((t) => t.name));
|
|
480
|
+
const webPlan = plan.filter((e) => !commandTargets.has(e.target));
|
|
481
|
+
const commandPlan = plan.filter((e) => commandTargets.has(e.target));
|
|
434
482
|
let browser;
|
|
435
483
|
let context;
|
|
436
484
|
const saved = [];
|
|
437
485
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
catch (err) {
|
|
443
|
-
const msg = err.message;
|
|
444
|
-
if (isShotCaptureUnavailable(err)) {
|
|
445
|
-
console.error(c.yellow(" · ") + "Screenshot capture skipped — Playwright/WebKit unavailable");
|
|
446
|
-
console.error(c.dim(` ${msg.split("\n")[0]}`));
|
|
447
|
-
console.error(c.dim(` ${INSTALL_ADVICE}`));
|
|
486
|
+
if (webPlan.length > 0) {
|
|
487
|
+
try {
|
|
488
|
+
browser = await launchWebkit();
|
|
489
|
+
context = await browser.newContext({ serviceWorkers: "block" });
|
|
448
490
|
}
|
|
449
|
-
|
|
450
|
-
|
|
491
|
+
catch (err) {
|
|
492
|
+
const msg = err.message;
|
|
493
|
+
if (isShotCaptureUnavailable(err)) {
|
|
494
|
+
console.error(c.yellow(" · ") + "Screenshot capture skipped — Playwright/WebKit unavailable");
|
|
495
|
+
console.error(c.dim(` ${msg.split("\n")[0]}`));
|
|
496
|
+
console.error(c.dim(` ${INSTALL_ADVICE}`));
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
console.error(c.red(" ✗ ") + `could not launch the browser: ${msg.split("\n")[0]}`);
|
|
500
|
+
}
|
|
501
|
+
return 1;
|
|
451
502
|
}
|
|
452
|
-
return 1;
|
|
453
503
|
}
|
|
454
504
|
let currentTarget;
|
|
455
505
|
let currentPreviewUrl = "";
|
|
456
|
-
for (const entry of
|
|
506
|
+
for (const entry of webPlan) {
|
|
457
507
|
// Only one preview runs per task, so a different target must replace
|
|
458
508
|
// the running one before it can start (#0379/#0271). Same-target
|
|
459
509
|
// entries reuse the running preview — no restart, no churn.
|
|
@@ -467,26 +517,27 @@ export async function cmdShot(args) {
|
|
|
467
517
|
currentPreviewUrl = started.url.replace(/\/$/, "");
|
|
468
518
|
currentTarget = entry.target;
|
|
469
519
|
}
|
|
470
|
-
const
|
|
520
|
+
const route = entry.route ?? "/";
|
|
521
|
+
const pageUrl = absolute ? route : shotCapturePageUrl(currentPreviewUrl, { ...entry, route });
|
|
471
522
|
const page = (await context.newPage());
|
|
472
523
|
let png;
|
|
473
524
|
const warnings = [];
|
|
474
525
|
try {
|
|
475
526
|
await page.setViewportSize(opts.viewport);
|
|
476
527
|
png = await captureShotPage(page, pageUrl, entry, { waitMs: opts.waitMs, fullPage: opts.fullPage }, {
|
|
477
|
-
onHighlightMiss: (selector,
|
|
478
|
-
warnings.push(`highlight ${selector} matched nothing on ${
|
|
528
|
+
onHighlightMiss: (selector, r) => {
|
|
529
|
+
warnings.push(`highlight ${selector} matched nothing on ${r}`);
|
|
479
530
|
console.error(c.yellow(" · ") +
|
|
480
|
-
`highlight "${selector}" matched nothing on ${
|
|
531
|
+
`highlight "${selector}" matched nothing on ${r} — capture went ahead unhighlighted`);
|
|
481
532
|
},
|
|
482
|
-
onSelectorMiss: (selector,
|
|
483
|
-
warnings.push(`selector ${selector} matched nothing on ${
|
|
533
|
+
onSelectorMiss: (selector, r) => {
|
|
534
|
+
warnings.push(`selector ${selector} matched nothing on ${r}`);
|
|
484
535
|
console.error(c.yellow(" · ") +
|
|
485
|
-
`selector "${selector}" matched nothing on ${
|
|
536
|
+
`selector "${selector}" matched nothing on ${r} — capture used the whole window`);
|
|
486
537
|
},
|
|
487
|
-
onStepMiss: (message,
|
|
488
|
-
warnings.push(`${message} on ${
|
|
489
|
-
console.error(c.yellow(" · ") + `${message} on ${
|
|
538
|
+
onStepMiss: (message, r) => {
|
|
539
|
+
warnings.push(`${message} on ${r}`);
|
|
540
|
+
console.error(c.yellow(" · ") + `${message} on ${r} — capture went ahead`);
|
|
490
541
|
},
|
|
491
542
|
});
|
|
492
543
|
}
|
|
@@ -499,7 +550,7 @@ export async function cmdShot(args) {
|
|
|
499
550
|
await page.close();
|
|
500
551
|
const uploaded = await uploadShot(port, task.id, token, {
|
|
501
552
|
target: entry.target,
|
|
502
|
-
route:
|
|
553
|
+
route: route.startsWith("/") ? normalizedRoute(route) : route,
|
|
503
554
|
...(entry.label ? { label: entry.label } : {}),
|
|
504
555
|
provenance: provenanceCaption(entry.provenance),
|
|
505
556
|
data: png.toString("base64"),
|
|
@@ -519,6 +570,52 @@ export async function cmdShot(args) {
|
|
|
519
570
|
console.log(c.green(" ✔ ") +
|
|
520
571
|
`${entry.target}${entry.highlight ? " · highlighted" : ""}${entry.label ? ` – ${entry.label}` : ""} → ${c.cyan(uploaded.shot.path)} ${c.dim(`(${Math.round(png.length / 1024)} KB)`)}`);
|
|
521
572
|
}
|
|
573
|
+
for (const entry of commandPlan) {
|
|
574
|
+
const target = shotTargetByName(worktreeConfig.shot, entry.target);
|
|
575
|
+
if (!target) {
|
|
576
|
+
console.error(c.red(" ✗ ") + `command capture target "${entry.target}" is not configured`);
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
const outputDir = join(shotsDir(worktreeRoot, worktreeConfig.workDir, task.id), "cmd", slugName(entry.target));
|
|
580
|
+
const outcome = runShotCommand(target, worktreeRoot, outputDir, {
|
|
581
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
582
|
+
...(entry.args ? { args: entry.args } : {}),
|
|
583
|
+
...(entry.glob ? { outputGlob: [entry.glob] } : {}),
|
|
584
|
+
});
|
|
585
|
+
if (outcome.status !== "captured") {
|
|
586
|
+
console.error(c.yellow(" · ") + `shots: ${outcome.status} — ${outcome.reason}`);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
for (const msg of outcome.warnings) {
|
|
590
|
+
console.error(c.yellow(" · ") + msg);
|
|
591
|
+
}
|
|
592
|
+
for (const file of outcome.files) {
|
|
593
|
+
const uploaded = await uploadShot(port, task.id, token, {
|
|
594
|
+
target: entry.target,
|
|
595
|
+
command: "command",
|
|
596
|
+
...(entry.label ? { label: entry.label } : {}),
|
|
597
|
+
provenance: provenanceCaption(entry.provenance),
|
|
598
|
+
name: `${entry.target}-${basename(file.name, extname(file.name))}`,
|
|
599
|
+
mime: file.mime,
|
|
600
|
+
data: file.data.toString("base64"),
|
|
601
|
+
});
|
|
602
|
+
if (!uploaded.ok) {
|
|
603
|
+
console.error(c.red(" ✗ ") + `storing the shot failed: ${uploaded.error}`);
|
|
604
|
+
return 1;
|
|
605
|
+
}
|
|
606
|
+
saved.push({
|
|
607
|
+
target: entry.target,
|
|
608
|
+
...(entry.label ? { label: entry.label } : {}),
|
|
609
|
+
file: uploaded.shot.name,
|
|
610
|
+
path: uploaded.shot.path,
|
|
611
|
+
url: uploaded.shot.url,
|
|
612
|
+
});
|
|
613
|
+
console.log(c.green(" ✔ ") +
|
|
614
|
+
`${entry.target}${entry.label ? ` – ${entry.label}` : ""} → ${c.cyan(uploaded.shot.path)} ${c.dim(`(${Math.round(file.data.length / 1024)} KB)`)}`);
|
|
615
|
+
}
|
|
616
|
+
// The raw command output is transient — the server now owns the bytes.
|
|
617
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
618
|
+
}
|
|
522
619
|
if (saved.length === 0) {
|
|
523
620
|
console.error(c.yellow(" · ") + "No shots captured.");
|
|
524
621
|
return 1;
|