@shiyuphan/follow-me 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/src/cli.js +204 -53
- package/dist/src/comments.js +95 -7
- package/dist/src/coverage.js +173 -38
- package/dist/src/host.js +1 -0
- package/dist/src/plan.js +44 -22
- package/dist/src/prompt.js +58 -17
- package/dist/src/refs.js +239 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-Bp14ygmz.js → abnfDiagram-WEYOIA3C-BWgk01ve.js} +1 -1
- package/dist/ui/dist/assets/{arc-DUR6uzhI.js → arc-Cg_Qb5c4.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-CyfnP65O.js → architecture-WOLXFQ4H-EdfV1etv.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-dJTicIk2.js → architectureDiagram-ZHVYKT53-Dz-gytQU.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BYiqh5Ib.js → blockDiagram-RJAE7QA3-B7Q5KhYC.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-DHHPQ7xE.js → c4Diagram-XDPPPE54-Cq9IvGR1.js} +1 -1
- package/dist/ui/dist/assets/channel-Da0YvuQF.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BODm_awL.js → chunk-2E7WHVMD-BTJB-4C8.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-CpZfjf2T.js → chunk-4EMNKR2T-BILthc2j.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-CoP2pJTQ.js → chunk-55XCU76K-DH3fYrJH.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-CaQcUvXY.js → chunk-7S6XSTFO-DDIiBvc2.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-BZ9Czvbu.js → chunk-AQDTSKZB-b1EeJ3gb.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-CNjUo110.js → chunk-DSNLSHJZ-DZZ635s5.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-BTmBE_D3.js → chunk-HAGFOTED-DhhrHTIh.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-D2cDuT0C.js → chunk-HNE4Q36P-B8wvhc2e.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-AZcemFgG.js → chunk-NQOKRVIF-CLtGMwDx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-BszwjUqG.js → chunk-UHW7PFBP-B9fVLGl0.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-vFSenYSe.js → chunk-VQ5W62QI-CgPvvMAv.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-DZR6is1Q.js → chunk-WQQL4CDK-DdqKu6R9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-CACFh-si.js → chunk-XXDRQBXY-GlM07vbp.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-DRRTjGuW.js → chunk-YZO67HFG-YqT6M0W9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Bxuy-Roa.js → chunk-ZAUX2LTX-DqHK51Gc.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-rYXk__1o.js → chunk-ZCHMQSOR-DEeXkjWz.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-RiF44tTz.js → classDiagram-v2-4MYWR3UA-BcKsX4up.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-0oiQoThS.js → cose-bilkent-JH36ORCC-fu9djgYc.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BAssFK5K.js → cynefin-EF2NZ3EQ-CFJASBsJ.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-Bmd05bak.js → cynefinDiagram-WF24UDRP-BVJD_n0g.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-QJkLjPxj.js → dagre-NK4GIZVX-Cx8pjz9O.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-PSaSiYXz.js → diagram-CBK3MWTC-CVIkEZ1X.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-BmU5X_hr.js → diagram-I5XIIV2D-2rxpF8BZ.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-AAV39kUu.js → diagram-ODIL2UCW-NJfGHnLk.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-DWlV-oxd.js → diagram-PFBEJUXE-DZHVJSeM.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-D8wHRCNX.js → diagram-RJA3I4VV-CQAfOObI.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-B_JQEsNY.js → diagram-TYUNFSNR-DGGaMOl6.js} +1 -1
- package/dist/ui/dist/assets/{dist-DxQIK_Ay.js → dist-CW-4NrUq.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-D83qV8mv.js → ebnfDiagram-V4P3TTPT-92pb2Fcg.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BKFAKVSD.js → elk-QONTBCTQ-vu0Z4rYG.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-CHYRd4aM.js → erDiagram-KYQIGQAA-BXnsRzCQ.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BDJDv3Zf.js → eventmodeling-K75KTNOO-bSr88MT1.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CUH45H9n.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-gTHjYkFj.js → ganttDiagram-R7GPNTJY-BQzVkGbj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-D4cWkxKB.js → gitGraph-VSP46ZUC-BegblPQi.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-D3M9wYMr.js → gitGraphDiagram-PDYL7XHY-BaGlGpEX.js} +1 -1
- package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
- package/dist/ui/dist/assets/index-D8ErIacA.js +136 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-B9y0ksVq.js → info-OHQRW6UA-D3HVGJDc.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-s4FMwb8r.js → infoDiagram-MVDRKAX5-Ckcp-D0C.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-CnQdxybf.js → ishikawaDiagram-HPEHV2GE-CWhd0CTS.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-I-9OpDWY.js → journeyDiagram-BOSHW7O7-CrH5cFXY.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DqUbIdH4.js → kanban-definition-FNSA4HC5-CpORMTDZ.js} +1 -1
- package/dist/ui/dist/assets/{line-BELSui4d.js → line-B3El9R0Z.js} +1 -1
- package/dist/ui/dist/assets/{linear-77hmn2iQ.js → linear-DLfthkbB.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-Bxa71wW-.js → mermaid-parser.core-QtQjLaoK.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-DBI9F4bh.js → mermaid.core-C2DGgMSc.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BxXMOapP.js → mindmap-definition-6OY57UG3-CW1lSXnX.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-DTT77cQh.js → packet-JDAUHWVQ-Dhu_m_17.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-HzrOQPT5.js → pegDiagram-ECIUJNBX-BGOhbzN4.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-iHwL1Qw-.js → pie-XZMESJXO-Citt3cMX.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-DZ1tLVjw.js → pieDiagram-GNMPY74T-DPVZikvz.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-NtO0YF9C.js → prism-core-YgwsYh9d.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BjTi9H5L.js → quadrantDiagram-D57K4MC3-Bwye1B9e.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-D2hYgXSB.js → radar-ABXABTNO-DRUb4G6V.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-DpakXN74.js → railroad-I3PHUGI6-x3Ug2mxX.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-B8ZFR3iy.js → railroad-abnf-LNEFI6M7-Dw14XhkZ.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-S1XUyTsC.js → railroad-ebnf-DWYD2UWJ-AwoWTWgL.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-BbeHC_6K.js → railroad-peg-JR7BVNR7-U7MXfciE.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-fd-2iyBo.js → railroadDiagram-Y72NRBL7-BxWlCZYs.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-B9-HHKH-.js → requirementDiagram-EQYSW5RA-TPmnZguV.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-CeQN1ZEG.js → sankeyDiagram-SY24JHO5-nPGftpl5.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-Vff7tx4t.js → sequenceDiagram-BLEHNHQU-BdctgaWa.js} +1 -1
- package/dist/ui/dist/assets/{src-D5-ZzLwt.js → src-zq235FjH.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js → stateDiagram-v2-A6IESKWZ-BK_E6kPg.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-CevqLchV.js → swimlanes-B3ENRKFQ-BY50Ne4l.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Ctw23I3i.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-D9jSDGJI.js → timeline-definition-JXYKGLGB-Bg8AXJ2P.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-CoXKj11m.js → treeView-D4JQ5SDB-B4hz6M4l.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-cHsansGe.js → treemap-SAJKECNS-e_Vc97Ox.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-CW9i3bW6.js → usecaseDiagram-WPOLBNNX-Butu_LCX.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-DuU3qEKJ.js → vennDiagram-Q3Y3IDCW-BGIyxlbf.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-dl9RgHUI.js → wardley-7MLQ67FV-Btv95mqq.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BOXhrK-a.js → wardleyDiagram-IVAIDKWA-DwTKExJ4.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-CLQNGrh4.js → xychartDiagram-4URNHDU4-UmlA5uxc.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +8 -8
- package/dist/ui/dist/assets/channel-CY2cQobm.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Bke3CfUH.js +0 -1
- package/dist/ui/dist/assets/index-BUOFihod.js +0 -134
- package/dist/ui/dist/assets/index-DLNJT0VS.css +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Bkv8hAmN.js +0 -8
package/README.md
CHANGED
|
@@ -75,11 +75,16 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
75
75
|

|
|
76
76
|
*Cmd-click a name to jump to where it is defined, even in a later commit, and Alt+Left to come back. A definition the branch does not change opens as a peek.*
|
|
77
77
|
|
|
78
|
+
When your agent's answer names code, like `src/lib.js:4-5`, each place becomes a numbered chip. Hover the answer to tint those lines, click a chip to jump there and Alt+Left to come back. An answer that names several places is a walkthrough: press Enter on it, then `.` and `,` to step.
|
|
79
|
+
|
|
80
|
+

|
|
81
|
+
*A walkthrough. Each place the agent names is tinted and numbered on the code, the current step strongest, and the bar at the bottom says where you are.*
|
|
82
|
+
|
|
78
83
|

|
|
79
84
|
*Coverage while reading. Your agent runs the tests with coverage and passes the lcov to `follow-me coverage`; follow-me never runs anything. Bars show covered, partly covered and uncovered lines, `x` jumps to the next uncovered line, and clicking a bar lists the tests that ran it.*
|
|
80
85
|
|
|
81
|
-

|
|
87
|
+
*Your agent offers what it can draw, like "a state machine for this guard", as chips under the commit. Tap one and it answers with the diagram, each state marked by whether a test reached it. A transition is green with the number of tests that take it, or red and dashed when none does. When the tests run one at a time, the table names them, and a list names the sequences each step of which has a test but no one test drives. Click a state or a transition to jump to its code.*
|
|
83
88
|
|
|
84
89
|

|
|
85
90
|
*Diagrams sit full width under the commit or reply they explain, and open full size on click.*
|
|
@@ -87,7 +92,7 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
|
|
|
87
92
|

|
|
88
93
|
*Submit review. Each item carries the original branch's path and line, so your agent can post it to the merge request as is.*
|
|
89
94
|
|
|
90
|
-
Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `g` shows or hides coverage and `x` jumps to the next uncovered line, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
95
|
+
Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `g` shows or hides coverage and `x` jumps to the next uncovered line, `.` and `,` step through the agent's walkthrough, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
|
|
91
96
|
|
|
92
97
|
### Reviewing someone else's branch
|
|
93
98
|
|
package/dist/src/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { mapOffTip, parseStateMap } from "./comments.js";
|
|
7
|
-
import { changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
|
|
7
|
+
import { changedBranchesTaken, changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
|
|
8
8
|
import { definitionIndex, isIdentifier } from "./definition.js";
|
|
9
9
|
import { DiffParseError, formatUnitRef, isRepoPath, isUnitId, splitLines } from "./diff.js";
|
|
10
10
|
import { BranchRefusedError, blobsAt, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
|
|
@@ -13,12 +13,17 @@ import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } fro
|
|
|
13
13
|
import { naiveDraft } from "./naive.js";
|
|
14
14
|
import { isCommentId, isConcernId, isFields, OFFERS_PER_COMMIT, OFFERS_PER_PLAN, serializePlanFile, validatePlan, } from "./plan.js";
|
|
15
15
|
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
|
|
16
|
+
import { parseRefText, refsIn, repoPathOfTyped, resolveRefs } from "./refs.js";
|
|
16
17
|
import { RegistrationError, sessionIdOf } from "./server.js";
|
|
17
18
|
import { BranchMovedError, cleanSessions, coverageAt, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
|
|
18
19
|
import { checkSyntax } from "./syntax.js";
|
|
19
20
|
class UsageError extends Error {
|
|
20
21
|
name = "UsageError";
|
|
21
22
|
}
|
|
23
|
+
/** `--help` or `-h` anywhere a command reads its options; main prints that command's usage. */
|
|
24
|
+
class HelpRequested extends Error {
|
|
25
|
+
name = "HelpRequested";
|
|
26
|
+
}
|
|
22
27
|
/** A test run follow-me will not record, with the reason the agent can act on. */
|
|
23
28
|
class CoverageError extends Error {
|
|
24
29
|
name = "CoverageError";
|
|
@@ -38,15 +43,16 @@ Other commands:
|
|
|
38
43
|
tip lines
|
|
39
44
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
40
45
|
start a thread on the page without being asked
|
|
41
|
-
show <h_…|c_…|k_…|path:
|
|
46
|
+
show <h_…|c_…|k_…|path:a[-b][@n|@base]|name>
|
|
42
47
|
print a unit, a comment's thread, a concern, the
|
|
43
|
-
lines around path:
|
|
48
|
+
lines around path:a-b in commit n, or where a name
|
|
44
49
|
is defined, instead of reading files
|
|
45
50
|
report [--json] [--markdown] print the report of the latest review; --json lists
|
|
46
51
|
the items to post to a merge request
|
|
47
52
|
coverage <lcov> [--test <id> [--at <path:line>]] [--reset]
|
|
48
53
|
record a test run taken at the tip for the page;
|
|
49
|
-
--
|
|
54
|
+
--at is where the test is declared; --reset alone
|
|
55
|
+
clears it before per-test runs
|
|
50
56
|
|
|
51
57
|
help --all lists every command.`;
|
|
52
58
|
const USAGE = `usage: follow-me <command> [options]
|
|
@@ -92,16 +98,17 @@ Interactive loop (while the review page is open):
|
|
|
92
98
|
reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
93
99
|
answer under a comment on the page; the diagram is
|
|
94
100
|
Mermaid source, the map JSON tying its states to
|
|
95
|
-
tip lines
|
|
101
|
+
tip lines. \`path:a-b[@n]\` in backticks in the body
|
|
102
|
+
points at code, and is refused if it does not exist
|
|
96
103
|
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
97
104
|
start a thread on the page without being asked;
|
|
98
105
|
--lines are change lines of the unit, commits count
|
|
99
106
|
from 1; with no target it is about the whole plan.
|
|
100
107
|
watch leaves these out like your replies
|
|
101
|
-
show <h_…|c_…|k_…|path:
|
|
108
|
+
show <h_…|c_…|k_…|path:a[-b][@n|@base]|name> [--branch <name>]
|
|
102
109
|
print a unit with the commits that take it, a
|
|
103
110
|
comment with its rows and thread, a concern with its
|
|
104
|
-
lines, the lines around path:
|
|
111
|
+
lines, the lines around path:a-b at the tip, in
|
|
105
112
|
commit n of the plan, or in the base, or each place
|
|
106
113
|
a name is defined at the tip with the commit it
|
|
107
114
|
lands in
|
|
@@ -115,7 +122,8 @@ Interactive loop (while the review page is open):
|
|
|
115
122
|
record the lcov of a test run at the branch tip;
|
|
116
123
|
refuses when a covered file in the working tree
|
|
117
124
|
differs from the tip. A plain run replaces the last;
|
|
118
|
-
--test runs add one test each, --at is
|
|
125
|
+
--test runs add one test each, --at is the line
|
|
126
|
+
where that test is declared.
|
|
119
127
|
--reset starts fresh; alone it clears the last run
|
|
120
128
|
apply, while the page is open, swaps it to the new plan version in place, same URL.
|
|
121
129
|
|
|
@@ -155,6 +163,8 @@ function parseArgs(argv, spec) {
|
|
|
155
163
|
const flags = new Set();
|
|
156
164
|
for (let i = 0; i < argv.length; i++) {
|
|
157
165
|
const arg = argv[i] ?? "";
|
|
166
|
+
if (arg === "--help" || arg === "-h")
|
|
167
|
+
throw new HelpRequested();
|
|
158
168
|
if (!arg.startsWith("--")) {
|
|
159
169
|
positionals.push(arg);
|
|
160
170
|
continue;
|
|
@@ -174,6 +184,43 @@ function parseArgs(argv, spec) {
|
|
|
174
184
|
}
|
|
175
185
|
return { positionals, values, flags };
|
|
176
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The usage of one command, read from its entries in USAGE: the synopsis, then what it does.
|
|
189
|
+
* Null for a command USAGE does not list.
|
|
190
|
+
*/
|
|
191
|
+
function usageOf(command) {
|
|
192
|
+
const blocks = [];
|
|
193
|
+
let block = null;
|
|
194
|
+
for (const line of USAGE.split("\n")) {
|
|
195
|
+
if (line.startsWith(` ${command} `) || line === ` ${command}`) {
|
|
196
|
+
block = [line];
|
|
197
|
+
blocks.push(block);
|
|
198
|
+
}
|
|
199
|
+
else if (block !== null && line.startsWith(" "))
|
|
200
|
+
block.push(line);
|
|
201
|
+
else
|
|
202
|
+
block = null;
|
|
203
|
+
}
|
|
204
|
+
if (blocks.length === 0)
|
|
205
|
+
return null;
|
|
206
|
+
return blocks
|
|
207
|
+
.map((lines) => {
|
|
208
|
+
const synopsis = [];
|
|
209
|
+
const what = [];
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
const [left = "", right = ""] = line.trim().split(/\s{3,}/, 2);
|
|
212
|
+
if (/^\s{20,}/.test(line))
|
|
213
|
+
what.push(left);
|
|
214
|
+
else {
|
|
215
|
+
synopsis.push(left);
|
|
216
|
+
if (right !== "")
|
|
217
|
+
what.push(right);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [`usage: follow-me ${synopsis.join(" ")}`, ...what.map((w) => ` ${w}`)].join("\n");
|
|
221
|
+
})
|
|
222
|
+
.join("\n");
|
|
223
|
+
}
|
|
177
224
|
function say(line = "") {
|
|
178
225
|
process.stdout.write(`${line}\n`);
|
|
179
226
|
}
|
|
@@ -263,45 +310,59 @@ function summarize(session, cwd) {
|
|
|
263
310
|
}
|
|
264
311
|
say(`session ${relative(cwd, session.dir) || "."}`);
|
|
265
312
|
}
|
|
313
|
+
/** Commits are numbered from 1 in everything the agent reads; issues carry 0-based indices. */
|
|
314
|
+
function commitName(k) {
|
|
315
|
+
return `commit ${k + 1}`;
|
|
316
|
+
}
|
|
317
|
+
/** A JSON path into the draft with its commit named from 1: `$.commits[2].units[0]` reads `commit 3, units[0]`. */
|
|
318
|
+
function jsonPlace(at) {
|
|
319
|
+
const m = /^\$\.commits\[(\d+)\]\.?(.*)$/.exec(at);
|
|
320
|
+
if (m === null)
|
|
321
|
+
return at;
|
|
322
|
+
const [, k = "0", rest = ""] = m;
|
|
323
|
+
return rest === "" ? commitName(Number(k)) : `${commitName(Number(k))}, ${rest}`;
|
|
324
|
+
}
|
|
266
325
|
function formatIssue(issue) {
|
|
267
326
|
switch (issue.kind) {
|
|
268
327
|
case "malformed":
|
|
269
|
-
return `malformed ${issue.at}: ${issue.reason}`;
|
|
328
|
+
return `malformed ${jsonPlace(issue.at)}: ${issue.reason}`;
|
|
270
329
|
case "stale":
|
|
271
330
|
return `stale: the plan is for tip ${short(issue.planTip)} but the session tip is ${short(issue.sessionTip)}`;
|
|
272
331
|
case "empty-message":
|
|
273
|
-
return
|
|
332
|
+
return `${commitName(issue.commit)}: empty message`;
|
|
274
333
|
case "empty-commit":
|
|
275
|
-
return
|
|
334
|
+
return `${commitName(issue.commit)}: no units`;
|
|
276
335
|
case "unknown-unit":
|
|
277
|
-
return
|
|
336
|
+
return `${commitName(issue.commit)}: unknown unit ${issue.ref}`;
|
|
278
337
|
case "bad-selection":
|
|
279
|
-
return
|
|
338
|
+
return `${commitName(issue.commit)}: ${issue.unit}: ${issue.reason}`;
|
|
280
339
|
case "duplicate": {
|
|
281
340
|
const [a, b] = issue.commits;
|
|
282
341
|
const what = issue.changes.length > 0 ? lines(issue.unit, issue.changes) : issue.unit;
|
|
283
|
-
return `${what} is claimed by commits ${a} and ${b}`;
|
|
342
|
+
return `${what} is claimed by commits ${a + 1} and ${b + 1}`;
|
|
284
343
|
}
|
|
285
344
|
case "unassigned": {
|
|
286
345
|
const what = issue.changes.length > 0 ? lines(issue.unit, issue.changes) : issue.unit;
|
|
287
346
|
return `${what} (${show(issue.path)}) is in no commit`;
|
|
288
347
|
}
|
|
289
348
|
case "path-conflict":
|
|
290
|
-
return
|
|
349
|
+
return `${commitName(issue.commit)}: ${show(issue.file)} is a file while ${show(issue.under)} exists`;
|
|
291
350
|
case "unknown-response":
|
|
292
351
|
return `response to unknown comment ${issue.comment}`;
|
|
293
352
|
case "bad-answer":
|
|
294
353
|
return `response to ${issue.comment}: ${issue.reason}`;
|
|
354
|
+
case "section-order":
|
|
355
|
+
return `${commitName(issue.commit)}: section ${issue.section} comes after ${commitName(issue.before)} in ${issue.beforeSection}; sections read story, steps, details, wiring, so move ${commitName(issue.commit)} before ${commitName(issue.before)}, or change its section`;
|
|
295
356
|
case "bad-concern":
|
|
296
|
-
return
|
|
357
|
+
return `${commitName(issue.commit)}: concern: ${issue.reason}`;
|
|
297
358
|
case "too-many-concerns":
|
|
298
|
-
return `${issue.unit} has
|
|
359
|
+
return `${commitName(issue.commit)}: ${issue.unit} has ${issue.count} concerns; a unit takes one per commit (each slice of a split hunk may carry its own), so keep the one that matters`;
|
|
299
360
|
case "bad-offer":
|
|
300
|
-
return
|
|
361
|
+
return `${commitName(issue.commit)}: offer: ${issue.reason}`;
|
|
301
362
|
case "too-many-offers":
|
|
302
363
|
return issue.commit === null
|
|
303
364
|
? `the plan has ${issue.count} offers; at most ${OFFERS_PER_PLAN}, and most plans need none`
|
|
304
|
-
:
|
|
365
|
+
: `${commitName(issue.commit)} has ${issue.count} offers; at most ${OFFERS_PER_COMMIT} per commit`;
|
|
305
366
|
case "final-mismatch": {
|
|
306
367
|
const tip = issue.expected ? `${issue.expected.mode} ${issue.expected.blob}` : "absent";
|
|
307
368
|
return `${show(issue.path)}: the last commit has ${issue.got}, the tip has ${tip}`;
|
|
@@ -314,10 +375,7 @@ function formatIssue(issue) {
|
|
|
314
375
|
}
|
|
315
376
|
function formatWarning(w) {
|
|
316
377
|
if (w.kind === "long-title") {
|
|
317
|
-
return `warning:
|
|
318
|
-
}
|
|
319
|
-
if (w.kind === "section-order") {
|
|
320
|
-
return `warning: $.commits[${w.commit}]: section ${w.section} comes after ${w.after}; sections read story, steps, details, wiring`;
|
|
378
|
+
return `warning: ${commitName(w.commit)}: title is ${w.length} characters; keep it to 50`;
|
|
321
379
|
}
|
|
322
380
|
if (w.kind === "question-addressed") {
|
|
323
381
|
return `warning: question ${w.comment} is marked addressed; explain it with "answered" and "at" unless answering it changed the plan`;
|
|
@@ -401,7 +459,7 @@ function syntaxOverride(args) {
|
|
|
401
459
|
return { override };
|
|
402
460
|
}
|
|
403
461
|
function formatSyntax(report, mark = "") {
|
|
404
|
-
const out = report.issues.map((i) => `${mark}
|
|
462
|
+
const out = report.issues.map((i) => `${mark}${commitName(i.commit)}: ${show(i.path)}: ${i.message}`);
|
|
405
463
|
for (const i of report.tip) {
|
|
406
464
|
out.push(`${mark}${show(i.path)}: ${i.message} (the tip itself does not parse here, so earlier commits are not checked for it)`);
|
|
407
465
|
}
|
|
@@ -557,7 +615,7 @@ function projectedCommits(session, version) {
|
|
|
557
615
|
const plan = session.plans().find((p) => p.version === version);
|
|
558
616
|
if (plan === undefined)
|
|
559
617
|
return [];
|
|
560
|
-
const verdict = validatePlan(session.diff, serializePlanFile(plan.file));
|
|
618
|
+
const verdict = validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true });
|
|
561
619
|
return verdict.ok ? verdict.plan.commits : [];
|
|
562
620
|
}
|
|
563
621
|
/** Writes the one-commit-per-file draft with no agent. */
|
|
@@ -928,7 +986,8 @@ function reviewedPlan(session) {
|
|
|
928
986
|
const reviews = reviewsOf(session);
|
|
929
987
|
for (const stored of reviews.toReversed()) {
|
|
930
988
|
const plan = session.plans().find((p) => p.version === stored.version);
|
|
931
|
-
if (plan !== undefined &&
|
|
989
|
+
if (plan !== undefined &&
|
|
990
|
+
validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true }).ok) {
|
|
932
991
|
return stored;
|
|
933
992
|
}
|
|
934
993
|
}
|
|
@@ -1074,11 +1133,42 @@ async function offerStep(port, summary) {
|
|
|
1074
1133
|
return null;
|
|
1075
1134
|
return `${STILL_WORKING}; offer ${first.offer} waits for your diagram: follow-me reply ${first.comment} --diagram <file> [--map <file>], then run follow-me again`;
|
|
1076
1135
|
}
|
|
1136
|
+
/** The per-test step while a state map waits for it, said as a `next:` of the listening loop. */
|
|
1137
|
+
async function testsStep(session, open, cwd) {
|
|
1138
|
+
if (!(await mapWantsTests(session, open)))
|
|
1139
|
+
return null;
|
|
1140
|
+
return `${STILL_WORKING}; ${perTestStep(session, cwd)}; then run follow-me again`;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Where the agent writes lcov files, relative to cwd: inside the session, which `.follow-me/`'s
|
|
1144
|
+
* own .gitignore hides and which goes when the session goes, so no run leaves a file in the repo.
|
|
1145
|
+
*/
|
|
1146
|
+
function lcovDir(session, cwd) {
|
|
1147
|
+
const dir = join(session.dir, "lcov");
|
|
1148
|
+
mkdirSync(dir, { recursive: true });
|
|
1149
|
+
return relative(cwd, dir);
|
|
1150
|
+
}
|
|
1151
|
+
/** The aggregate run to take when a review opens. */
|
|
1152
|
+
function coverageStep(session, cwd) {
|
|
1153
|
+
return `if the project has tests, run them with coverage at the tip, lcov to ${lcovDir(session, cwd)}/all.info, and pass it to follow-me coverage`;
|
|
1154
|
+
}
|
|
1155
|
+
/** The per-test runs a state map needs for its badges and its "Not driven" list. */
|
|
1156
|
+
function perTestStep(session, cwd) {
|
|
1157
|
+
const dir = lcovDir(session, cwd);
|
|
1158
|
+
return `the state machine needs one coverage run per test: follow-me coverage --reset, then for each test run it alone with lcov to ${dir}/<n>.info and follow-me coverage ${dir}/<n>.info --test "<test name>" --at <path:line where it is declared>`;
|
|
1159
|
+
}
|
|
1160
|
+
/** Whether a reply of the hosted review carries a state map while no run has per-test detail. */
|
|
1161
|
+
async function mapWantsTests(session, open) {
|
|
1162
|
+
if ((session.coverage()?.tests.size ?? 0) > 0)
|
|
1163
|
+
return false;
|
|
1164
|
+
const said = await fetchComments(open.port, open.summary);
|
|
1165
|
+
return said.some(({ comments }) => comments.some((c) => (c.replies ?? []).some((r) => r.map !== undefined)));
|
|
1166
|
+
}
|
|
1077
1167
|
/**
|
|
1078
1168
|
* Listens to the hosted review for up to --wait seconds and prints what the reviewer did. It
|
|
1079
1169
|
* returns at once with news, and with the report when the reviewer submitted or ended the review.
|
|
1080
1170
|
*/
|
|
1081
|
-
async function listen(session, open, args) {
|
|
1171
|
+
async function listen(session, open, args, cwd) {
|
|
1082
1172
|
const { port, summary } = open;
|
|
1083
1173
|
const waitMs = waitOf(args) * 1000;
|
|
1084
1174
|
const deadline = Date.now() + waitMs;
|
|
@@ -1134,11 +1224,13 @@ async function listen(session, open, args) {
|
|
|
1134
1224
|
return 0;
|
|
1135
1225
|
}
|
|
1136
1226
|
next((await offerStep(port, summary)) ??
|
|
1227
|
+
(await testsStep(session, open, cwd)) ??
|
|
1137
1228
|
`${STILL_WORKING}; answer with follow-me reply <id> --body "<markdown>" [--diagram file], then run follow-me again`);
|
|
1138
1229
|
return 0;
|
|
1139
1230
|
}
|
|
1140
1231
|
if (left <= 0) {
|
|
1141
1232
|
next((await offerStep(port, summary)) ??
|
|
1233
|
+
(await testsStep(session, open, cwd)) ??
|
|
1142
1234
|
`${STILL_WORKING}; run follow-me again to keep listening`);
|
|
1143
1235
|
return 0;
|
|
1144
1236
|
}
|
|
@@ -1163,7 +1255,7 @@ async function entry(cwd, args) {
|
|
|
1163
1255
|
return swapOpenPage(session, hosted, args);
|
|
1164
1256
|
}
|
|
1165
1257
|
if (hosted !== null)
|
|
1166
|
-
return listen(session, hosted, args);
|
|
1258
|
+
return listen(session, hosted, args, cwd);
|
|
1167
1259
|
const state = draftState(session);
|
|
1168
1260
|
if (state.kind === "invalid") {
|
|
1169
1261
|
next("fix the draft, then run follow-me again");
|
|
@@ -1208,11 +1300,10 @@ async function entry(cwd, args) {
|
|
|
1208
1300
|
const result = await hostReview(session, mode, detached, async () => prep, reportSaved);
|
|
1209
1301
|
if (result.kind !== "elsewhere")
|
|
1210
1302
|
return exitCode(result);
|
|
1211
|
-
if (session.coverage() === null)
|
|
1212
|
-
next(
|
|
1213
|
-
}
|
|
1303
|
+
if (session.coverage() === null)
|
|
1304
|
+
next(coverageStep(session, cwd));
|
|
1214
1305
|
say("follow-me is interactive: keep running it until it prints the report");
|
|
1215
|
-
return listen(session, result, args);
|
|
1306
|
+
return listen(session, result, args, cwd);
|
|
1216
1307
|
}
|
|
1217
1308
|
/** Opens the session, re-extracting instead of failing when the branch moved or --base changed. */
|
|
1218
1309
|
async function openFresh(cwd, args) {
|
|
@@ -1235,7 +1326,10 @@ function walkablePlan(session) {
|
|
|
1235
1326
|
const lastReview = session.lastReview();
|
|
1236
1327
|
const stored = session.plans().at(-1);
|
|
1237
1328
|
if (stored !== undefined) {
|
|
1238
|
-
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
1329
|
+
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
1330
|
+
lastReview,
|
|
1331
|
+
frozen: true,
|
|
1332
|
+
});
|
|
1239
1333
|
if (verdict.ok)
|
|
1240
1334
|
return { version: stored.version, plan: verdict.plan };
|
|
1241
1335
|
}
|
|
@@ -1274,7 +1368,7 @@ async function renderStored(session, stored, format = "markdown") {
|
|
|
1274
1368
|
console.error(`follow-me: review v${stored.version} refers to a plan that is missing`);
|
|
1275
1369
|
return null;
|
|
1276
1370
|
}
|
|
1277
|
-
const verdict = validatePlan(session.diff, serializePlanFile(plan.file));
|
|
1371
|
+
const verdict = validatePlan(session.diff, serializePlanFile(plan.file), { frozen: true });
|
|
1278
1372
|
if (!verdict.ok) {
|
|
1279
1373
|
console.error(`follow-me: plan v${plan.version} no longer fits ${session.anchor.branch}`);
|
|
1280
1374
|
return null;
|
|
@@ -1338,7 +1432,7 @@ async function report(cwd, args, walkOnly = false) {
|
|
|
1338
1432
|
process.stdout.write(text);
|
|
1339
1433
|
return 0;
|
|
1340
1434
|
}
|
|
1341
|
-
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset";
|
|
1435
|
+
const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset\n--at is the path:line where the test is declared";
|
|
1342
1436
|
/**
|
|
1343
1437
|
* Records the lcov of a test run the agent made at the tip in the session's coverage.json and
|
|
1344
1438
|
* prints how much of the branch's changed code it ran. follow-me never runs the tests itself.
|
|
@@ -1402,7 +1496,19 @@ async function coverage(cwd, args) {
|
|
|
1402
1496
|
if (open !== null)
|
|
1403
1497
|
await postCoverage(open.port, open.summary);
|
|
1404
1498
|
const { covered, executable } = totalOf(changedLines(session.diff.units, recorded.files).values());
|
|
1405
|
-
|
|
1499
|
+
const total = `coverage for ${plural(recorded.files.size, "file")} at ${short(tip)}; ${covered} of ${executable} changed lines covered`;
|
|
1500
|
+
if (test === undefined)
|
|
1501
|
+
say(total);
|
|
1502
|
+
else {
|
|
1503
|
+
// A per-test run adds to what is on record, so its own share is said apart from the total.
|
|
1504
|
+
const own = totalOf(changedLines(session.diff.units, run.files).values()).covered;
|
|
1505
|
+
const branches = changedBranchesTaken(session.diff.units, run.files);
|
|
1506
|
+
say(`recorded test ${JSON.stringify(test.id)}: ${plural(own, "changed line")}, ${branches === 1 ? "1 branch" : `${branches} branches`}`);
|
|
1507
|
+
say(`running total of ${plural(recorded.tests.size, "test")}: ${total}`);
|
|
1508
|
+
}
|
|
1509
|
+
if (test === undefined && open !== null && (await mapWantsTests(session, open))) {
|
|
1510
|
+
next(`${perTestStep(session, cwd)}; then run follow-me again`);
|
|
1511
|
+
}
|
|
1406
1512
|
return 0;
|
|
1407
1513
|
}
|
|
1408
1514
|
/**
|
|
@@ -1529,14 +1635,20 @@ async function reply(cwd, args) {
|
|
|
1529
1635
|
const open = await openOnServer(session, null);
|
|
1530
1636
|
if (open === null)
|
|
1531
1637
|
return notOpen(session);
|
|
1638
|
+
const refs = await bodyRefs(session, open.summary.version, body);
|
|
1532
1639
|
const id = await postReply(open.port, open.summary, commentId, {
|
|
1533
1640
|
author: "agent",
|
|
1534
1641
|
body,
|
|
1535
1642
|
...(diagram === undefined ? {} : { diagram }),
|
|
1536
1643
|
...(map === undefined ? {} : { map }),
|
|
1644
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
1537
1645
|
});
|
|
1538
1646
|
say(`replied ${id} to ${commentId}`);
|
|
1539
|
-
|
|
1647
|
+
if (map !== undefined && (session.coverage()?.tests.size ?? 0) === 0) {
|
|
1648
|
+
next(`${perTestStep(session, cwd)}; then run follow-me again; ${STILL_WORKING}`);
|
|
1649
|
+
}
|
|
1650
|
+
else
|
|
1651
|
+
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1540
1652
|
return 0;
|
|
1541
1653
|
}
|
|
1542
1654
|
const REPLY_USAGE = `usage: follow-me reply <commentId> --body <text> [--diagram <file> [--map <file>]]
|
|
@@ -1586,6 +1698,33 @@ async function readMap(session, file, diagram) {
|
|
|
1586
1698
|
throw new UsageError(`${file}:\n ${problems.join("\n ")}`);
|
|
1587
1699
|
return map;
|
|
1588
1700
|
}
|
|
1701
|
+
/**
|
|
1702
|
+
* The refs --body writes, resolved against plan `version` and the tip. Every one that does not
|
|
1703
|
+
* resolve is named at once, as --map's are.
|
|
1704
|
+
*/
|
|
1705
|
+
async function bodyRefs(session, version, body) {
|
|
1706
|
+
const specs = refsIn(body);
|
|
1707
|
+
if (specs.length === 0)
|
|
1708
|
+
return [];
|
|
1709
|
+
const { root, diff } = session;
|
|
1710
|
+
const otherChanged = new Set([...diff.units.values()].flatMap((u) => (diff.textFiles.has(u.path) ? [] : [u.path])));
|
|
1711
|
+
const asked = [...new Set(specs.map((s) => repoPathOfTyped(s.path)))].filter((p) => isRepoPath(p) && !diff.textFiles.has(p) && !otherChanged.has(p));
|
|
1712
|
+
const tip = asked.length === 0 ? new Set() : await pathsAt(root, diff.tip);
|
|
1713
|
+
const present = asked.filter((p) => tip.has(p));
|
|
1714
|
+
const unchanged = new Map([...(await blobsAt(root, diff.tip, present))].map(([p, bytes]) => [p, splitLines(bytes)]));
|
|
1715
|
+
const { refs, problems } = resolveRefs(specs, {
|
|
1716
|
+
tip: diff.tip,
|
|
1717
|
+
textFiles: diff.textFiles,
|
|
1718
|
+
otherChanged,
|
|
1719
|
+
unchanged,
|
|
1720
|
+
commits: projectedCommits(session, version),
|
|
1721
|
+
version,
|
|
1722
|
+
});
|
|
1723
|
+
if (problems.length > 0) {
|
|
1724
|
+
throw new UsageError(`--body points at code that does not resolve:\n ${problems.join("\n ")}`);
|
|
1725
|
+
}
|
|
1726
|
+
return refs;
|
|
1727
|
+
}
|
|
1589
1728
|
/** The Mermaid source in --diagram's file, or undefined without the flag. */
|
|
1590
1729
|
function diagramOf(args) {
|
|
1591
1730
|
const file = args.values.get("diagram");
|
|
@@ -1693,17 +1832,19 @@ async function explain(cwd, args) {
|
|
|
1693
1832
|
if (open === null)
|
|
1694
1833
|
return notOpen(session);
|
|
1695
1834
|
const target = explainTarget(session, open.summary.version, args);
|
|
1835
|
+
const refs = await bodyRefs(session, open.summary.version, body);
|
|
1696
1836
|
const id = await postComment(open.port, open.summary, {
|
|
1697
1837
|
...target,
|
|
1698
1838
|
body,
|
|
1699
1839
|
author: "agent",
|
|
1700
1840
|
...(diagram === undefined ? {} : { diagram }),
|
|
1841
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
1701
1842
|
});
|
|
1702
1843
|
say(`explained ${id}`);
|
|
1703
1844
|
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1704
1845
|
return 0;
|
|
1705
1846
|
}
|
|
1706
|
-
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:
|
|
1847
|
+
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:a[-b][@<commit>|@base]|name>";
|
|
1707
1848
|
/** Every comment the session has, with the plan version it was made on: live ones first. */
|
|
1708
1849
|
async function knownComments(session) {
|
|
1709
1850
|
const open = await openOnServer(session, null);
|
|
@@ -1717,14 +1858,16 @@ async function knownComments(session) {
|
|
|
1717
1858
|
}
|
|
1718
1859
|
const SHOW_MARGIN = 3;
|
|
1719
1860
|
/**
|
|
1720
|
-
* The lines around path:
|
|
1721
|
-
* base, or the tip when `at` is
|
|
1861
|
+
* The lines around path:a-b as commit `at` of the latest plan writes the file (1-based), the
|
|
1862
|
+
* base, or the tip when `at` is null. Read from the plan, so it works before any apply.
|
|
1722
1863
|
*/
|
|
1723
1864
|
function showLines(session, raw) {
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
if (typed === undefined || lineRaw === undefined)
|
|
1865
|
+
const spec = parseRefText(raw);
|
|
1866
|
+
if (spec === null)
|
|
1727
1867
|
throw new UsageError(SHOW_USAGE);
|
|
1868
|
+
const { path: typed, start, end, at } = spec;
|
|
1869
|
+
if (end < start)
|
|
1870
|
+
throw new UsageError(`${raw} ends before it starts`);
|
|
1728
1871
|
const path = Buffer.from(typed, "utf8").toString("latin1");
|
|
1729
1872
|
const file = [...session.diff.textFiles.values()].find((f) => f.path === path);
|
|
1730
1873
|
if (file === undefined) {
|
|
@@ -1732,7 +1875,7 @@ function showLines(session, raw) {
|
|
|
1732
1875
|
}
|
|
1733
1876
|
let text;
|
|
1734
1877
|
let label;
|
|
1735
|
-
if (at ===
|
|
1878
|
+
if (at === null) {
|
|
1736
1879
|
text = file.tip?.bytes ?? null;
|
|
1737
1880
|
label = "the tip";
|
|
1738
1881
|
}
|
|
@@ -1743,7 +1886,7 @@ function showLines(session, raw) {
|
|
|
1743
1886
|
else {
|
|
1744
1887
|
const latest = session.plans().at(-1);
|
|
1745
1888
|
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1746
|
-
const n =
|
|
1889
|
+
const n = at;
|
|
1747
1890
|
if (n < 1 || n > commits.length) {
|
|
1748
1891
|
throw new UsageError(`@${at} is not one of the ${commits.length} commits of the plan`);
|
|
1749
1892
|
}
|
|
@@ -1762,18 +1905,18 @@ function showLines(session, raw) {
|
|
|
1762
1905
|
const lines = Buffer.from(text, "latin1").toString("utf8").split("\n");
|
|
1763
1906
|
if (lines.at(-1) === "")
|
|
1764
1907
|
lines.pop();
|
|
1765
|
-
|
|
1766
|
-
if (line < 1 || line > lines.length) {
|
|
1908
|
+
if (start < 1 || end > lines.length) {
|
|
1767
1909
|
return `${typed} has ${plural(lines.length, "line")} in ${label}\n`;
|
|
1768
1910
|
}
|
|
1769
|
-
const from = Math.max(1,
|
|
1770
|
-
const to = Math.min(lines.length,
|
|
1911
|
+
const from = Math.max(1, start - SHOW_MARGIN);
|
|
1912
|
+
const to = Math.min(lines.length, end + SHOW_MARGIN);
|
|
1771
1913
|
const width = String(to).length;
|
|
1772
1914
|
const rows = lines.slice(from - 1, to).map((l, i) => {
|
|
1773
1915
|
const n = from + i;
|
|
1774
|
-
return `${n
|
|
1916
|
+
return `${n >= start && n <= end ? ">" : " "} ${String(n).padStart(width)} ${l}`;
|
|
1775
1917
|
});
|
|
1776
|
-
|
|
1918
|
+
const span = end > start ? `${start}-${end}` : `${start}`;
|
|
1919
|
+
return `${typed}:${span} in ${label}\n${rows.join("\n")}\n`;
|
|
1777
1920
|
}
|
|
1778
1921
|
/** Expands a ref the agent was given to the lines and thread it names, so no file read is needed. */
|
|
1779
1922
|
async function showRef(cwd, args) {
|
|
@@ -1789,7 +1932,7 @@ async function showRef(cwd, args) {
|
|
|
1789
1932
|
throw new UsageError(`${ref} is not a unit of the branch now; its hunk changed`);
|
|
1790
1933
|
}
|
|
1791
1934
|
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1792
|
-
process.stdout.write(renderShownUnit(unit, commits));
|
|
1935
|
+
process.stdout.write(renderShownUnit(unit, units, commits));
|
|
1793
1936
|
return 0;
|
|
1794
1937
|
}
|
|
1795
1938
|
if (/^c_[a-z2-7]{6}$/.test(ref) && isCommentId(ref)) {
|
|
@@ -1880,6 +2023,9 @@ async function serverCommand(argv) {
|
|
|
1880
2023
|
await started.host.closed;
|
|
1881
2024
|
return 0;
|
|
1882
2025
|
}
|
|
2026
|
+
case "--help":
|
|
2027
|
+
case "-h":
|
|
2028
|
+
throw new HelpRequested();
|
|
1883
2029
|
default:
|
|
1884
2030
|
throw new UsageError("usage: follow-me server status|stop");
|
|
1885
2031
|
}
|
|
@@ -2101,6 +2247,11 @@ export async function main(argv, cwd) {
|
|
|
2101
2247
|
}
|
|
2102
2248
|
}
|
|
2103
2249
|
catch (error) {
|
|
2250
|
+
if (error instanceof HelpRequested) {
|
|
2251
|
+
const asked = command === undefined || command === "help" ? null : usageOf(command);
|
|
2252
|
+
say(asked ?? SHORT_USAGE);
|
|
2253
|
+
return 0;
|
|
2254
|
+
}
|
|
2104
2255
|
const known = [
|
|
2105
2256
|
UsageError,
|
|
2106
2257
|
CoverageError,
|