@ersbeth/codelife 0.0.0-stage → 0.11.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/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/agents/evaluate.md +97 -0
- package/agents/ground.md +49 -0
- package/agents/implementer.md +49 -0
- package/agents/judge.md +128 -0
- package/agents/planner.md +45 -0
- package/agents/prerequisites.md +62 -0
- package/agents/reader.md +40 -0
- package/agents/reproduce.md +65 -0
- package/bin/codelife +4 -0
- package/package.json +38 -4
- package/presets/tsconfig.json +14 -0
- package/presets/vite.config.d.mts +3 -0
- package/presets/vite.config.mjs +49 -0
- package/scripts/audit/cadence.mjs +24 -0
- package/scripts/audit/close.mjs +57 -0
- package/scripts/audit/file.mjs +70 -0
- package/scripts/audit/fold.mjs +48 -0
- package/scripts/audit/lib.mjs +93 -0
- package/scripts/audit/prepare.mjs +180 -0
- package/scripts/audit/table.mjs +65 -0
- package/scripts/brainstorm/decided.mjs +6 -0
- package/scripts/brainstorm/lib.mjs +33 -0
- package/scripts/brainstorm/named.mjs +9 -0
- package/scripts/changelog.mjs +100 -0
- package/scripts/check-tag.mjs +19 -0
- package/scripts/checks.mjs +53 -0
- package/scripts/codelife.mjs +42 -0
- package/scripts/commit-msg.mjs +43 -0
- package/scripts/contract.mjs +179 -0
- package/scripts/former-form.mjs +170 -0
- package/scripts/git/branches.mjs +6 -0
- package/scripts/git/commit.mjs +40 -0
- package/scripts/git/diff.mjs +17 -0
- package/scripts/git/diff_lines.mjs +5 -0
- package/scripts/git/held.mjs +53 -0
- package/scripts/git/is_ancestor.mjs +28 -0
- package/scripts/git/lib.mjs +20 -0
- package/scripts/git/log_since.mjs +27 -0
- package/scripts/git/parent.mjs +51 -0
- package/scripts/git/show.mjs +4 -0
- package/scripts/git/status.mjs +25 -0
- package/scripts/git/submodules.mjs +5 -0
- package/scripts/init.mjs +21 -0
- package/scripts/install.mjs +32 -0
- package/scripts/lenses/A11Y.md +91 -0
- package/scripts/lenses/ARCH.md +90 -0
- package/scripts/lenses/BUG.md +52 -0
- package/scripts/lenses/BUILD.md +40 -0
- package/scripts/lenses/COMPAT.md +67 -0
- package/scripts/lenses/CONF.md +49 -0
- package/scripts/lenses/DEPS.md +43 -0
- package/scripts/lenses/DOC.md +73 -0
- package/scripts/lenses/FAIL.md +57 -0
- package/scripts/lenses/GOV.md +56 -0
- package/scripts/lenses/GUARD.md +47 -0
- package/scripts/lenses/INTENT.md +52 -0
- package/scripts/lenses/LEAN.md +49 -0
- package/scripts/lenses/LINES.md +48 -0
- package/scripts/lenses/PERF.md +61 -0
- package/scripts/lenses/PKG.md +58 -0
- package/scripts/lenses/SEC.md +49 -0
- package/scripts/lenses/STANDARDS.md +62 -0
- package/scripts/lenses/TYPE.md +56 -0
- package/scripts/lenses/VERIF.md +81 -0
- package/scripts/link.mjs +40 -0
- package/scripts/methods/cause.md +21 -0
- package/scripts/methods/comments.md +157 -0
- package/scripts/methods/description.md +67 -0
- package/scripts/methods/message.md +24 -0
- package/scripts/methods/red-green.md +28 -0
- package/scripts/methods/refusal.md +22 -0
- package/scripts/mr/open.mjs +219 -0
- package/scripts/node-floor.mjs +19 -0
- package/scripts/onboard/prep.mjs +93 -0
- package/scripts/overview/codelife-line.mjs +33 -0
- package/scripts/overview/next.mjs +76 -0
- package/scripts/overview/overview.mjs +308 -0
- package/scripts/package-manager.mjs +20 -0
- package/scripts/port/item.mjs +94 -0
- package/scripts/port/lib.mjs +194 -0
- package/scripts/port/prep.mjs +33 -0
- package/scripts/port/version.mjs +50 -0
- package/scripts/pre-commit.mjs +43 -0
- package/scripts/pre-push.mjs +53 -0
- package/scripts/prepare-lib.mjs +81 -0
- package/scripts/read-roadmap/lib.mjs +116 -0
- package/scripts/read-roadmap/moved.mjs +70 -0
- package/scripts/read-roadmap/range.mjs +15 -0
- package/scripts/release/cut.mjs +120 -0
- package/scripts/release/entry.mjs +52 -0
- package/scripts/release/lib.mjs +136 -0
- package/scripts/release/prep.mjs +58 -0
- package/scripts/review-mr/prepare.mjs +163 -0
- package/scripts/roadmap/assemble.mjs +269 -0
- package/scripts/roadmap/cut.mjs +88 -0
- package/scripts/roadmap/prepare.mjs +249 -0
- package/scripts/roadmap/roadmap.md +30 -0
- package/scripts/roadmap/write.mjs +53 -0
- package/scripts/search.mjs +29 -0
- package/scripts/setup/contract.mjs +25 -0
- package/scripts/setup/item.mjs +166 -0
- package/scripts/setup/lib.mjs +205 -0
- package/scripts/setup/prep.mjs +106 -0
- package/scripts/spec-form.mjs +444 -0
- package/scripts/spec.mjs +56 -0
- package/scripts/tags.mjs +27 -0
- package/scripts/target/target-app.md +134 -0
- package/scripts/target/target-base.md +127 -0
- package/scripts/target/target-lib.md +143 -0
- package/scripts/target/templates/audit/README.md +21 -0
- package/scripts/target/templates/characterisation/bundle.test.ts +43 -0
- package/scripts/target/templates/characterisation/declarations.test.ts +44 -0
- package/scripts/target/templates/characterisation/entries.ts +50 -0
- package/scripts/target/templates/characterisation/examples.test.ts +18 -0
- package/scripts/target/templates/characterisation/import.test.ts +22 -0
- package/scripts/target/templates/characterisation/surface.test.ts +68 -0
- package/scripts/target/templates/cliff.toml +61 -0
- package/scripts/target/templates/githooks/commit-msg +9 -0
- package/scripts/target/templates/githooks/pre-commit +9 -0
- package/scripts/target/templates/githooks/pre-push +9 -0
- package/scripts/target/templates/playground/index.html +11 -0
- package/scripts/target/templates/playground/main.ts +9 -0
- package/scripts/target/templates/spec/README.md +35 -0
- package/scripts/target/templates/spec/capability.md +36 -0
- package/scripts/target/templates/spec/glossary.md +5 -0
- package/scripts/target/templates/spec/journey.md +29 -0
- package/scripts/target/templates/spec/principles.md +15 -0
- package/scripts/target/templates/spec/users.md +15 -0
- package/scripts/target/templates/tsconfig.json +9 -0
- package/scripts/target/templates/vite.config.ts +4 -0
- package/scripts/text.mjs +6 -0
- package/scripts/tracker/account.mjs +5 -0
- package/scripts/tracker/auth.mjs +9 -0
- package/scripts/tracker/cli.version.mjs +5 -0
- package/scripts/tracker/close.mjs +20 -0
- package/scripts/tracker/create.mjs +47 -0
- package/scripts/tracker/issue.section.mjs +25 -0
- package/scripts/tracker/issue.update.mjs +13 -0
- package/scripts/tracker/issue.view.mjs +16 -0
- package/scripts/tracker/lib.mjs +130 -0
- package/scripts/tracker/list.mjs +14 -0
- package/scripts/tracker/list.whole.mjs +20 -0
- package/scripts/tracker/lot.row.mjs +42 -0
- package/scripts/tracker/mr.checkout_command.mjs +5 -0
- package/scripts/tracker/mr.comments.mjs +50 -0
- package/scripts/tracker/mr.create.mjs +12 -0
- package/scripts/tracker/mr.description.mjs +8 -0
- package/scripts/tracker/mr.fetch_head.mjs +5 -0
- package/scripts/tracker/mr.find_for_branch.mjs +9 -0
- package/scripts/tracker/mr.post.mjs +40 -0
- package/scripts/tracker/mr.update_description.mjs +8 -0
- package/scripts/tracker/mr.verdict.mjs +34 -0
- package/scripts/tracker/mr.view.mjs +20 -0
- package/scripts/tracker/none.mjs +79 -0
- package/scripts/tracker/note.mjs +12 -0
- package/scripts/tracker/reopen.mjs +8 -0
- package/scripts/tracker/search.mjs +8 -0
- package/scripts/tracker/search_id.mjs +18 -0
- package/scripts/tracker/templates/bug.md +35 -0
- package/scripts/tracker/templates/feature.md +40 -0
- package/scripts/tracker/templates/lot.md +46 -0
- package/scripts/tracker/title.join.mjs +6 -0
- package/scripts/tracker/title.leave.mjs +7 -0
- package/scripts/tracker/title.lot_of.mjs +8 -0
- package/scripts/tracker/title.search_id_filter.mjs +17 -0
- package/scripts/tracker/title.sentence_of.mjs +5 -0
- package/scripts/tracker/view.mjs +25 -0
- package/scripts/verbs.mjs +579 -0
- package/scripts/verify/BUILD.mjs +33 -0
- package/scripts/verify/DEPS.mjs +15 -0
- package/scripts/verify/GOV.mjs +34 -0
- package/scripts/verify/GUARD.mjs +34 -0
- package/scripts/verify/LINES.mjs +22 -0
- package/scripts/verify/PKG.mjs +56 -0
- package/scripts/verify/TYPE.mjs +34 -0
- package/scripts/verify/cut.mjs +73 -0
- package/scripts/verify/history.mjs +32 -0
- package/scripts/verify/lib.mjs +129 -0
- package/scripts/verify/report.mjs +104 -0
- package/scripts/verify/timing.mjs +19 -0
- package/scripts/verify.mjs +29 -0
- package/scripts/work/branch.mjs +81 -0
- package/scripts/work/close.mjs +74 -0
- package/scripts/work/member.mjs +73 -0
- package/scripts/work/prepare.mjs +116 -0
- package/scripts/work/reopen.mjs +40 -0
- package/scripts/work-feedback/prepare.mjs +96 -0
- package/skills/audit/SKILL.md +110 -0
- package/skills/audit/templates/index.md +36 -0
- package/skills/audit/templates/lens.md +44 -0
- package/skills/brainstorm/SKILL.md +144 -0
- package/skills/brainstorm/references/exit.md +57 -0
- package/skills/brainstorm/templates/note.md +79 -0
- package/skills/commit/SKILL.md +68 -0
- package/skills/mr/SKILL.md +82 -0
- package/skills/new-issue/SKILL.md +87 -0
- package/skills/onboard/SKILL.md +103 -0
- package/skills/onboard/templates/CLAUDE.md +31 -0
- package/skills/onboard/templates/CONTRIBUTING.md +34 -0
- package/skills/onboard/templates/README.md +30 -0
- package/skills/onboard/templates/SKILL.md +24 -0
- package/skills/onboard/templates/package-CLAUDE.md +15 -0
- package/skills/overview/SKILL.md +59 -0
- package/skills/port-toolchain/SKILL.md +127 -0
- package/skills/release/SKILL.md +78 -0
- package/skills/review-mr/SKILL.md +106 -0
- package/skills/review-mr/templates/pass.md +29 -0
- package/skills/roadmap/SKILL.md +137 -0
- package/skills/setup/SKILL.md +112 -0
- package/skills/work/SKILL.md +139 -0
- package/skills/work-feedback/SKILL.md +113 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// generated by codelife — maintained by hand
|
|
2
|
+
//
|
|
3
|
+
// The playground is where an example lives once it stops being dead code in a README. Import the
|
|
4
|
+
// library by its package name: `tsconfig.json`'s `paths` send that to `src/`, so an edit to the source shows up
|
|
5
|
+
// here on the next reload.
|
|
6
|
+
import * as lib from '<package-name>'
|
|
7
|
+
|
|
8
|
+
const out = document.querySelector('#out')
|
|
9
|
+
if (out) out.textContent = Object.keys(lib).sort().join('\n')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- generated by codelife — /brainstorm -->
|
|
2
|
+
# {{project}} — the specification
|
|
3
|
+
|
|
4
|
+
{{The vision, in a few sentences: the problem the product solves, for whom, and what it
|
|
5
|
+
deliberately does not do.}}
|
|
6
|
+
|
|
7
|
+
What the system must do, and nothing about what is done: what is implemented is the code, and what
|
|
8
|
+
remains is the open issues citing an identifier. A decision still waiting to be specified is a note
|
|
9
|
+
in `docs/brainstorm/`. This file is the only index: a domain has no README, its card is the
|
|
10
|
+
paragraph under its heading here.
|
|
11
|
+
|
|
12
|
+
## Users
|
|
13
|
+
|
|
14
|
+
[`users.md`](users.md) — who the product is for, one `P-<n>` per persona.
|
|
15
|
+
|
|
16
|
+
## Journeys
|
|
17
|
+
|
|
18
|
+
{{One line per journey, first priority first:
|
|
19
|
+
`- [J-{{n}} — <journey>](journeys/<slug>.md) — <the goal, in one sentence>`.}}
|
|
20
|
+
|
|
21
|
+
## Product
|
|
22
|
+
|
|
23
|
+
{{One `###` per domain — a **user activity**, never a component — then its card in one paragraph,
|
|
24
|
+
then one line per capability:
|
|
25
|
+
`- [K-{{n}} — <capability>](product/<activity>/<slug>.md) — <what it lets the user do>`.}}
|
|
26
|
+
|
|
27
|
+
## System
|
|
28
|
+
|
|
29
|
+
{{One `###` per domain — a **component** — its card, then one line per capability:
|
|
30
|
+
`- [K-{{n}} — <capability>](system/<component>/<slug>.md) — <what it provides>`.}}
|
|
31
|
+
|
|
32
|
+
## Glossary and principles
|
|
33
|
+
|
|
34
|
+
[`glossary.md`](glossary.md) — the terms, and the synonyms to avoid.
|
|
35
|
+
[`principles.md`](principles.md) — what the product must respect.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- generated by codelife — /brainstorm -->
|
|
2
|
+
# K-{{n}} — {{capability}}
|
|
3
|
+
|
|
4
|
+
## What it is for, and for whom
|
|
5
|
+
|
|
6
|
+
{{The need it answers and its client — a persona, or the code that calls it.}}
|
|
7
|
+
|
|
8
|
+
## Behaviour
|
|
9
|
+
|
|
10
|
+
### R-{{n}} — {{the rule, as one observable sentence}}
|
|
11
|
+
|
|
12
|
+
{{Free prose, in the present tense, with a "because" clause where the rule would look arbitrary.}}
|
|
13
|
+
|
|
14
|
+
#### C-{{n}} — {{the case}}
|
|
15
|
+
|
|
16
|
+
Given {{the situation}}
|
|
17
|
+
When {{what is done}}
|
|
18
|
+
Then {{what is observed}}
|
|
19
|
+
|
|
20
|
+
## Internal design
|
|
21
|
+
|
|
22
|
+
{{The mechanism, in the present tense: the data, the steps, the choices a reader of the code cannot
|
|
23
|
+
recover alone — and the system capabilities it uses, each cited `K-{{n}} — <its heading>`. Empty
|
|
24
|
+
for a capability with no algorithm.}}
|
|
25
|
+
|
|
26
|
+
## What stays open
|
|
27
|
+
|
|
28
|
+
{{The questions no decision has answered yet, each with what would settle it.}}
|
|
29
|
+
|
|
30
|
+
## Rejected
|
|
31
|
+
|
|
32
|
+
{{One line per rejected option, as the note wrote it, opening on the address it concerns — the
|
|
33
|
+
identifier alone for a rule or a case, `- R-{{n}} — <option> — <cost> — <date>`, and
|
|
34
|
+
`- § <heading> — <option> — <cost> — <date>` for what carries none. A former state that was
|
|
35
|
+
replaced is such a line, "in force <from> → <to>", and a retired rule or case keeps its identifier
|
|
36
|
+
there, so its number is never assigned again.}}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- generated by codelife — /brainstorm -->
|
|
2
|
+
# J-{{n}} — {{journey}}
|
|
3
|
+
|
|
4
|
+
**Persona.** {{`P-{{n}} — <persona>`.}}
|
|
5
|
+
**Priority.** {{First, second, … — the first-priority journeys define the minimum product.}}
|
|
6
|
+
**Goal.** {{What the persona has achieved when the journey ends.}}
|
|
7
|
+
**Independent test.** {{How someone who did not build it checks that the goal is reached.}}
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
|
|
11
|
+
{{Numbered steps, each pointing at the case it walks and specifying nothing itself:
|
|
12
|
+
`1. <what the persona does> — C-{{n}} — <its heading>`.}}
|
|
13
|
+
|
|
14
|
+
## Success criteria
|
|
15
|
+
|
|
16
|
+
{{What is measured, and the value that counts as success.}}
|
|
17
|
+
|
|
18
|
+
## Out of this journey
|
|
19
|
+
|
|
20
|
+
{{What a reader might expect here and belongs to another journey, named.}}
|
|
21
|
+
|
|
22
|
+
## What stays open
|
|
23
|
+
|
|
24
|
+
{{The questions no decision has answered yet, each with what would settle it.}}
|
|
25
|
+
|
|
26
|
+
## Rejected
|
|
27
|
+
|
|
28
|
+
{{One line per rejected option, opening on the address it concerns: `- J-{{n}} — <option> —
|
|
29
|
+
<cost> — <date>` for what carries an identifier, `- § <heading> — …` for what carries none.}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- generated by codelife — /brainstorm -->
|
|
2
|
+
# Principles
|
|
3
|
+
|
|
4
|
+
{{What the product must respect — units, tolerances, budgets, platforms — one `##` per principle,
|
|
5
|
+
cited as `principles.md § <heading>`. How one works in the repository is `CLAUDE.md`'s, not this
|
|
6
|
+
file's.}}
|
|
7
|
+
|
|
8
|
+
## What stays open
|
|
9
|
+
|
|
10
|
+
{{The questions no decision has answered yet, each with what would settle it.}}
|
|
11
|
+
|
|
12
|
+
## Rejected
|
|
13
|
+
|
|
14
|
+
{{One line per refusal that concerns no capability — "no CI" is the refusal of a principle:
|
|
15
|
+
`- § <heading> — <option> — <cost> — <date>`.}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- generated by codelife — /brainstorm -->
|
|
2
|
+
# Users
|
|
3
|
+
|
|
4
|
+
## P-{{n}} — {{persona}}
|
|
5
|
+
|
|
6
|
+
{{Who they are; their context of use; what they come to do; what they are not.}}
|
|
7
|
+
|
|
8
|
+
## What stays open
|
|
9
|
+
|
|
10
|
+
{{The questions no decision has answered yet, each with what would settle it.}}
|
|
11
|
+
|
|
12
|
+
## Rejected
|
|
13
|
+
|
|
14
|
+
{{One line per rejected option, opening on the address it concerns: `- P-{{n}} — <option> —
|
|
15
|
+
<cost> — <date>` for what carries an identifier, `- § <heading> — …` for what carries none.}}
|
package/scripts/text.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// lf <text> — the text with every line ending in `\n` alone. A file checked out on Windows, or a body
|
|
2
|
+
// a forge's web editor saved, ends its lines in `\r\n`, which a reader splitting on `\n` and matching
|
|
3
|
+
// `^…$` misreads (#1124): what a script parses, it reads through this first.
|
|
4
|
+
import './node-floor.mjs'
|
|
5
|
+
|
|
6
|
+
export const lf = (text) => text.replace(/\r\n?/g, '\n')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// tracker.auth(host): one line — `connected, expires <date>`, `connected, expiry unavailable`,
|
|
2
|
+
// `expired` or `not connected: <the CLI's words>` — exit 0 only when connected. The host defaults to
|
|
3
|
+
// origin's, the CLI is the contract's; the reading is scripts/setup/lib.mjs's `auth`, /setup's own.
|
|
4
|
+
import { cli, origin } from './lib.mjs';
|
|
5
|
+
import { auth } from '../setup/lib.mjs';
|
|
6
|
+
|
|
7
|
+
const a = auth(cli(), process.argv[2] || origin().host);
|
|
8
|
+
console.log(a.line);
|
|
9
|
+
process.exit(a.connected ? 0 : 1);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// tracker.cli.version(): `<cli> <version>`, the forge CLI's own; the caller judges it against the floor.
|
|
2
|
+
import { cli, die, run } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
const version = run(['--version']).match(/\d+(\.\d+)+/)?.[0] || die('the CLI printed no version');
|
|
5
|
+
console.log(`${cli()} ${version}`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// tracker.close(n, closing fields, wontfix?): the comment, read from its file, then the close, then with
|
|
2
|
+
// `wontfix` the label — in that order, so a close cut midway
|
|
3
|
+
// leaves the reasoning and never a silently closed issue. `node close.mjs <n> <comment file> [wontfix]`.
|
|
4
|
+
import { arg, body, cli, kind, run } from './lib.mjs';
|
|
5
|
+
import { close } from './none.mjs';
|
|
6
|
+
|
|
7
|
+
const usage = '<n> <comment file> [wontfix]';
|
|
8
|
+
const n = arg(0, usage), file = arg(1, usage), wontfix = process.argv[4] === 'wontfix';
|
|
9
|
+
const comment = body(file);
|
|
10
|
+
if (kind() === 'none') close(n, comment, wontfix);
|
|
11
|
+
else if (cli() === 'glab') {
|
|
12
|
+
run(['issue', 'note', n, '-m', comment]);
|
|
13
|
+
run(['issue', 'close', n]);
|
|
14
|
+
if (wontfix) run(['issue', 'update', n, '--label', 'wontfix']);
|
|
15
|
+
} else {
|
|
16
|
+
run(['issue', 'comment', n, '--body-file', file]);
|
|
17
|
+
run(['issue', 'close', n]);
|
|
18
|
+
if (wontfix) run(['issue', 'edit', n, '--add-label', 'wontfix']);
|
|
19
|
+
}
|
|
20
|
+
console.log(`closed #${n}${wontfix ? ', wontfix' : ''}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// `codelife issue create [--once] <bug|feature|lot|-> <severity|-> <origin|-> <file>` (review-mr
|
|
2
|
+
// C-868): one new issue, the file's `# ` line its sentence and the rest its body; the title opens on the origin's
|
|
3
|
+
// tag (`tagged` in lib.mjs), its labels are the nature and `severity-<s>` — an
|
|
4
|
+
// umbrella's, `lot` alone after `[lot · <origin>]`; `-`, the roadmap's, none. Under
|
|
5
|
+
// `tracker.kind: none`, a new `issues/NNN-<slug>.md`. Prints `created #<n> — <link or path>`; a
|
|
6
|
+
// number that does not read back is a failure, never an empty one. With `--once`, an origin that
|
|
7
|
+
// is an identifier — an audit's finding or lot — files nothing when an issue's tag already holds it,
|
|
8
|
+
// open or closed, and prints `exists #<n> — <its line>`: what makes a cut filing run again.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { arg, cli, die, kind, run, tagged, text } from './lib.mjs';
|
|
15
|
+
import { create } from './none.mjs';
|
|
16
|
+
|
|
17
|
+
const once = process.argv[2] === '--once';
|
|
18
|
+
if (once) process.argv.splice(2, 1);
|
|
19
|
+
const usage = '[--once] <bug|feature|lot|-> <critical|high|mid|low|-> <origin|-> <file>';
|
|
20
|
+
const nature = arg(0, usage), severity = arg(1, usage), origin = arg(2, usage), file = arg(3, usage);
|
|
21
|
+
if (!['bug', 'feature', 'lot', '-'].includes(nature)) die(`usage: codelife issue create ${usage} — not '${nature}'`);
|
|
22
|
+
if (!['critical', 'high', 'mid', 'low', '-'].includes(severity)) die(`usage: codelife issue create ${usage} — not '${severity}'`);
|
|
23
|
+
const [, sentence = '', body = ''] = text(file).match(/^# (.+)\n+([\s\S]*)$/) ?? [];
|
|
24
|
+
if (!sentence.trim() || !body.trim()) die(`${file}: a '# ' line, then the body`);
|
|
25
|
+
const title = tagged(origin === '-' ? '' : origin, nature === 'lot', sentence.trim());
|
|
26
|
+
if (once && origin !== '-') {
|
|
27
|
+
const key = join(mkdtempSync(join(tmpdir(), 'codelife-once-')), 'key');
|
|
28
|
+
writeFileSync(key, origin);
|
|
29
|
+
const r = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'search_id.mjs'), key, '--all'], { encoding: 'utf8' });
|
|
30
|
+
if (r.status !== 0) die((r.stderr || r.stdout).trim());
|
|
31
|
+
const hit = r.stdout.split('\n').find((l) => /^#\d+/.test(l));
|
|
32
|
+
if (hit) {
|
|
33
|
+
console.log(`exists ${hit.match(/^#\d+/)[0]} — ${hit}`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const labels = [...(nature !== '-' ? [nature] : []), ...(severity !== '-' ? [`severity-${severity}`] : [])];
|
|
38
|
+
if (kind() === 'none') {
|
|
39
|
+
const path = create(title, body, nature !== '-' ? nature : '', severity !== '-' ? severity : '');
|
|
40
|
+
console.log(`created #${Number(path.match(/issues\/(\d+)/)[1])} — ${path}`);
|
|
41
|
+
} else {
|
|
42
|
+
const out = cli() === 'glab'
|
|
43
|
+
? run(['issue', 'create', '-t', title, '-d', body, ...(labels.length ? ['-l', labels.join(',')] : []), '-y', '--no-editor'])
|
|
44
|
+
: run(['issue', 'create', '--title', title, '--body-file', '-', ...labels.flatMap((l) => ['--label', l])], body);
|
|
45
|
+
const url = out.trim().split('\n').at(-1).trim(), n = url.match(/\/(\d+)$/)?.[1];
|
|
46
|
+
console.log(n ? `created #${n} — ${url}` : die(`the issue was sent and its number does not read back: ${out.trim()}`));
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `codelife issue section <n> <heading>`: one section of an issue —
|
|
2
|
+
// the description from its `## <heading>` line, matched case-blind on the heading's opening words, to
|
|
3
|
+
// the next `## `; `plan` prints instead the latest comment opening on `codelife: plan — `. Exit 1 when
|
|
4
|
+
// the issue has no such section.
|
|
5
|
+
import { arg, die } from './lib.mjs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const n = arg(0, '<n> <heading>'), heading = process.argv.slice(3).join(' ') || die('usage: codelife issue section <n> <heading>');
|
|
11
|
+
const r = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'view.mjs'), n], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
12
|
+
if (r.status !== 0) die(r.stderr.trim());
|
|
13
|
+
const issue = JSON.parse(r.stdout);
|
|
14
|
+
let out;
|
|
15
|
+
if (heading.toLowerCase() === 'plan') out = issue.comments.filter((c) => c.body.startsWith('codelife: plan — ')).at(-1)?.body;
|
|
16
|
+
else {
|
|
17
|
+
const lines = issue.body.split('\n'), want = heading.toLowerCase();
|
|
18
|
+
const at = lines.findIndex((l) => l.startsWith('## ') && l.slice(3).toLowerCase().startsWith(want));
|
|
19
|
+
if (at >= 0) {
|
|
20
|
+
const end = lines.findIndex((l, i) => i > at && l.startsWith('## '));
|
|
21
|
+
out = lines.slice(at, end < 0 ? undefined : end).join('\n').trim();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!out) die(`#${n} has no ${heading.toLowerCase() === 'plan' ? '`codelife: plan` comment' : `section '## ${heading}'`}`);
|
|
25
|
+
console.log(out);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// tracker.issue.update(n, body, title?): the body read from its file — `-` leaves it — and the title
|
|
2
|
+
// from its own, when given.
|
|
3
|
+
// `node issue.update.mjs <n> <body file>|- [<title file>]`.
|
|
4
|
+
import { arg, body, cli, kind, run, text } from './lib.mjs';
|
|
5
|
+
import { retitle, update } from './none.mjs';
|
|
6
|
+
|
|
7
|
+
const usage = '<n> <body file>|- [<title file>]';
|
|
8
|
+
const n = arg(0, usage), file = arg(1, usage), titled = process.argv[4];
|
|
9
|
+
const description = file !== '-' && body(file), title = titled && text(titled).trim();
|
|
10
|
+
if (kind() === 'none') { if (description) update(n, description); if (title) retitle(n, title); }
|
|
11
|
+
else if (cli() === 'glab') run(['issue', 'update', n, ...(description ? ['--description', description] : []), ...(title ? ['--title', title] : [])]);
|
|
12
|
+
else run(['issue', 'edit', n, ...(description ? ['--body-file', file] : []), ...(title ? ['--title', title] : [])]);
|
|
13
|
+
console.log(`updated #${n}`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `codelife issue view <n>` (#1050): the issue whole, as a person reads it — its title, state and
|
|
2
|
+
// labels, its description, then every comment and every close or reopening, oldest first, each under
|
|
3
|
+
// its date and author. What view.mjs reads, written for a reader rather than a script.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { arg, die } from './lib.mjs';
|
|
8
|
+
|
|
9
|
+
const n = arg(0, '<n>');
|
|
10
|
+
const r = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'view.mjs'), n], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
11
|
+
if (r.status !== 0) die(r.stderr.trim());
|
|
12
|
+
const i = JSON.parse(r.stdout);
|
|
13
|
+
const said = [...i.comments.map((c) => ({ date: c.date, head: `${c.date || 'undated'} — ${c.author || 'unknown'}`, body: c.body.trim() })),
|
|
14
|
+
...i.events.map((e) => ({ date: e.date, head: `${e.date} — ${e.state}` }))].sort((a, b) => (a.date || '').localeCompare(b.date || ''));
|
|
15
|
+
console.log([`# #${n} ${i.title}`, '', `- state: ${i.state}`, `- labels: ${i.labels.join(', ') || 'none'}`, '', i.body.trim(),
|
|
16
|
+
...said.flatMap((c) => ['', `## ${c.head}`, ...(c.body ? ['', c.body] : [])])].join('\n'));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// What the scripts under scripts/tracker/ share — each runs one operation on the tracker, named after
|
|
2
|
+
// it, and is the one place its forge form is written. From the repository's root: the CLI the
|
|
3
|
+
// contract's `tracker.kind` names, origin's host and project read at each call, the user's token
|
|
4
|
+
// variables stripped.
|
|
5
|
+
import '../node-floor.mjs';
|
|
6
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { contract, valid } from '../contract.mjs';
|
|
9
|
+
|
|
10
|
+
// TOKENS, env — the user's token variables, and the environment every forge call runs in without
|
|
11
|
+
// them: the CLI's own stored login is the connection, never a variable a shell profile set.
|
|
12
|
+
export const TOKENS = ['GITLAB_TOKEN', 'GITLAB_HOST', 'GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GH_HOST'];
|
|
13
|
+
export const env = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !TOKENS.includes(k)));
|
|
14
|
+
|
|
15
|
+
export const die = (why) => {
|
|
16
|
+
console.error(why);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// kind — the contract's `tracker.kind`; `none` when it names no forge.
|
|
21
|
+
export const kind = () => { const v = valid(); return v.stop ? die(v.stop) : v.contract.data.tracker?.kind || 'none'; };
|
|
22
|
+
|
|
23
|
+
// cli — `glab` or `gh`, from the contract; under `none` every operation scripted here but those that
|
|
24
|
+
// read `issues/` (none.mjs) is refused.
|
|
25
|
+
export const cli = () => {
|
|
26
|
+
const k = kind();
|
|
27
|
+
if (k !== 'glab' && k !== 'gh') die(`refused under tracker.kind ${k}: a merge request has no file-based equivalent`);
|
|
28
|
+
return k;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// call <args> [input] — the CLI's { status, stdout, stderr, error }; its stdin is `input`, empty when
|
|
32
|
+
// none is given, so a CLI that would ask a question answers at once rather than waiting.
|
|
33
|
+
export const call = (args, input = '') => spawnSync(cli(), args, { encoding: 'utf8', env: env(), input, maxBuffer: 256 * 1024 * 1024 });
|
|
34
|
+
|
|
35
|
+
// run <args> [input] — the CLI's stdout; its failure is the operation's, quoted, the contract's
|
|
36
|
+
// `## Tracker notes` under it: the forge's traps the team wrote, read where one is met.
|
|
37
|
+
const failed = (args, said) => {
|
|
38
|
+
const notes = contract()?.sections.get('Tracker notes');
|
|
39
|
+
die(`${cli()} ${args.join(' ')}: ${said.trim()}${notes ? `\n\nThe contract's ## Tracker notes:\n${notes}` : ''}`);
|
|
40
|
+
};
|
|
41
|
+
export const run = (args, input) => {
|
|
42
|
+
const r = call(args, input);
|
|
43
|
+
if (r.status !== 0) failed(args, r.stderr || r.stdout || r.error?.message || '');
|
|
44
|
+
return r.stdout;
|
|
45
|
+
};
|
|
46
|
+
// runLater <args> — run's answer as a promise, for reads made side by side, stdin closed (#1098).
|
|
47
|
+
export const runLater = (args) => new Promise((resolve) => execFile(cli(), args, { encoding: 'utf8', env: env(), maxBuffer: 256 * 1024 * 1024 },
|
|
48
|
+
(e, out, err) => (e ? failed(args, err || out || e.message) : resolve(out))).stdin.end());
|
|
49
|
+
// pool <items> <n> <fn> — fn over the items, n at a time, the answers in the items' order.
|
|
50
|
+
export const pool = async (items, n, fn) => {
|
|
51
|
+
const out = new Array(items.length);
|
|
52
|
+
let i = 0;
|
|
53
|
+
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => { while (i < items.length) { const k = i++; out[k] = await fn(items[k]); } }));
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
export const ndjson = (text) => text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
57
|
+
// comments <n> — the forge call that reads issue <n>'s comments, every page; commentsOf <its stdout>
|
|
58
|
+
// — them, oldest first, { author, date, body }. On glab the notes API, its system notes dropped —
|
|
59
|
+
// never `issue view --comments`, one page of 20 notes, system ones counted; on gh `issue view --json
|
|
60
|
+
// comments`, a shape view.mjs's own call also has.
|
|
61
|
+
export const comments = (n) => (cli() === 'glab' ? ['api', `projects/:id/issues/${n}/notes?sort=asc&per_page=100`, '--paginate', '--output', 'ndjson']
|
|
62
|
+
: ['issue', 'view', String(n), '--json', 'comments']);
|
|
63
|
+
export const commentsOf = (out) => (cli() === 'glab' ? ndjson(out).filter((c) => !c.system).map((c) => ({ author: c.author?.username, date: c.created_at, body: c.body }))
|
|
64
|
+
: JSON.parse(out).comments.map((c) => ({ author: c.author?.login, date: c.createdAt, body: c.body })));
|
|
65
|
+
export const git = (...args) => {
|
|
66
|
+
const r = spawnSync('git', args, { encoding: 'utf8' });
|
|
67
|
+
if (r.status !== 0) die(`git ${args.join(' ')}: ${r.stderr.trim()}`);
|
|
68
|
+
return r.stdout;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// origin — { host, project } from `git remote get-url origin`, both forms of step 1.
|
|
72
|
+
// parseOrigin <url> — the same, or null when the URL reads as neither: a path on this machine.
|
|
73
|
+
export const parseOrigin = (url) => {
|
|
74
|
+
const m = url.match(/^[^@/]+@([^:/]+):(.+?)(?:\.git)?$/) || url.match(/^[a-z+]+:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?$/);
|
|
75
|
+
return m ? { host: m[1], project: m[2] } : null;
|
|
76
|
+
};
|
|
77
|
+
export const origin = () => {
|
|
78
|
+
const url = git('remote', 'get-url', 'origin').trim();
|
|
79
|
+
return parseOrigin(url) ?? die(`origin ${url} reads as neither form`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// pages <glab issue list flags> — every row, where `glab issue list` prints one page: 30 rows by
|
|
83
|
+
// default, 100 at most. A failure of the CLI is the operation's.
|
|
84
|
+
export const pages = (flags) => {
|
|
85
|
+
const rows = [];
|
|
86
|
+
for (let p = 1; ; p++) {
|
|
87
|
+
const page = run(['issue', 'list', ...flags, '-P', '100', '-p', String(p)]).split('\n').filter((l) => l.startsWith('#'));
|
|
88
|
+
rows.push(...page);
|
|
89
|
+
if (page.length < 100) return rows;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// text <file or fd> — what `$(cat <file>)` gives: the file, its trailing newlines gone. Forge text
|
|
94
|
+
// arrives this way, by file or on stdin (`text(0)`), and never as an argument.
|
|
95
|
+
export const text = (src) => readFileSync(src, 'utf8').replace(/\n+$/, '');
|
|
96
|
+
// body <file> — the text a write publishes: refused, nothing sent, when the file holds only blank
|
|
97
|
+
// lines, since an empty description or comment replaces what was there with nothing (#1062).
|
|
98
|
+
export const body = (file) => text(file).trim() ? text(file) : die(`${file} is empty; nothing sent`);
|
|
99
|
+
|
|
100
|
+
// word <term> — a test of whether a text holds the term as a whole word, regardless of case and as a
|
|
101
|
+
// fixed string: what `grep -iwF` matches, so `hang` is not found inside `change`.
|
|
102
|
+
export const word = (term) => {
|
|
103
|
+
const re = new RegExp(`(?<![A-Za-z0-9_])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_])`, 'i');
|
|
104
|
+
return (s) => re.test(s);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// matching <term> [all] — the open issues the forge's search finds for one term, one line each, every
|
|
108
|
+
// page; with `all`, the closed ones too. The term goes to the CLI as one argument, never through a shell.
|
|
109
|
+
export const matching = (term, all) => (cli() === 'glab'
|
|
110
|
+
? pages([`--search=${term}`, '--in', 'title,description', ...(all ? ['--all'] : [])])
|
|
111
|
+
: run(['issue', 'list', `--search=${term}`, '--state', all ? 'all' : 'open', '--limit', '100000']).split('\n').filter(Boolean));
|
|
112
|
+
|
|
113
|
+
// rank <lines> — each line once, after the number of searches it came back from and a tab, the most
|
|
114
|
+
// first, a tie in the order the lines first came. A line is known by its first tab-separated field —
|
|
115
|
+
// an issue's number, whose row ends on an age that moves between searches.
|
|
116
|
+
export const rank = (lines) => {
|
|
117
|
+
const seen = new Map();
|
|
118
|
+
for (const line of lines.filter(Boolean)) {
|
|
119
|
+
const k = line.split('\t')[0];
|
|
120
|
+
seen.set(k, { line: seen.get(k)?.line ?? line, n: (seen.get(k)?.n ?? 0) + 1 });
|
|
121
|
+
}
|
|
122
|
+
return [...seen.values()].sort((a, b) => b.n - a.n).map(({ line, n }) => `${n}\t${line}`);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const first = (body = '') => body.split('\n')[0];
|
|
126
|
+
|
|
127
|
+
// tagged <origin> <umbrella> <sentence> — the title an issue is filed under: `[<origin>] `, an
|
|
128
|
+
// umbrella's `[lot · <origin>] ` — `[lot] ` with none —, or the sentence alone (templates.md R-75).
|
|
129
|
+
export const tagged = (origin, umbrella, sentence) => `${umbrella ? `[lot${origin ? ` · ${origin}` : ''}] ` : origin ? `[${origin}] ` : ''}${sentence}`;
|
|
130
|
+
export const arg = (i, what) => process.argv[2 + i] || die(`usage: node ${process.argv[1].split('/').pop()} ${what}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// tracker.list(flags…): the open issues, one `#<n>\t<title>\t(<labels>)\t<when>` line each, every
|
|
2
|
+
// page, whatever the tracker — the flags the CLI's own. On glab through `pages` of lib.mjs, which
|
|
3
|
+
// reads past the 30 rows `glab issue list` prints; on gh from its JSON, since a piped row carries a
|
|
4
|
+
// state column a reader of glab's rows does not expect (#1095); under `none`, the files of issues/,
|
|
5
|
+
// every one with `-A`, other flags ignored (#1066).
|
|
6
|
+
import { cli, kind, pages, run } from './lib.mjs';
|
|
7
|
+
import { lines } from './none.mjs';
|
|
8
|
+
|
|
9
|
+
const flags = process.argv.slice(2);
|
|
10
|
+
const rows = kind() === 'none' ? lines(flags.includes('-A'))
|
|
11
|
+
: cli() === 'glab' ? pages(flags)
|
|
12
|
+
: JSON.parse(run(['issue', 'list', '--limit', '100000', '--json', 'number,title,labels,updatedAt', ...flags]))
|
|
13
|
+
.map((i) => `#${i.number}\t${i.title}\t(${i.labels.map((l) => l.name).join(', ')})\t${i.updatedAt}`);
|
|
14
|
+
process.stdout.write(rows.map((l) => `${l}\n`).join(''));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The open issues as one JSON array, { n, title, body, labels, comments } each, for a reader of every
|
|
2
|
+
// one (`codelife overview`): the fields from one paginated list call, the comments only of an issue
|
|
3
|
+
// that has some, four reads at a time — not each issue through view.mjs, three forge calls apiece,
|
|
4
|
+
// which gitlab.com answered in minutes once dozens ran side by side (#1150). No state events: a
|
|
5
|
+
// reader that needs an issue's reads view.mjs. Under `none`, the files of issues/.
|
|
6
|
+
import { cli, comments, commentsOf, kind, ndjson, origin, pool, run, runLater } from './lib.mjs';
|
|
7
|
+
import { lines, view } from './none.mjs';
|
|
8
|
+
import { lf } from '../text.mjs';
|
|
9
|
+
|
|
10
|
+
let issues;
|
|
11
|
+
if (kind() === 'none') issues = lines(false).map((l) => Number(l.slice(1, l.indexOf('\t')))).map((n) => { const { title, body, labels, comments: c } = view(n); return { n, title, body, labels, comments: c }; });
|
|
12
|
+
else {
|
|
13
|
+
const listed = cli() === 'glab'
|
|
14
|
+
? ndjson(run(['api', 'projects/:id/issues?state=opened&per_page=100', '--paginate', '--output', 'ndjson']))
|
|
15
|
+
.map((i) => ({ n: i.iid, title: i.title, body: i.description || '', labels: i.labels || [], some: i.user_notes_count > 0 }))
|
|
16
|
+
: JSON.parse(run(['api', `repos/${origin().project}/issues?state=open&per_page=100`, '--paginate', '--slurp'])).flat().filter((i) => !i.pull_request)
|
|
17
|
+
.map((i) => ({ n: i.number, title: i.title, body: i.body || '', labels: i.labels.map((l) => l.name), some: i.comments > 0 }));
|
|
18
|
+
issues = await pool(listed, 4, async ({ some, ...i }) => ({ ...i, comments: some ? commentsOf(await runLater(comments(i.n))) : [] }));
|
|
19
|
+
}
|
|
20
|
+
console.log(JSON.stringify(issues, (_, v) => (typeof v === 'string' ? lf(v) : v)));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// An umbrella's body once one row of its `## The order` task list is written — the body on stdin, the
|
|
2
|
+
// body printed, `issue.update` writing what it printed; a write already made leaves the body as it was:
|
|
3
|
+
// node lot.row.mjs add <member> <sentence file> [<effort>] append `- [ ] #<member> (<effort>) <sentence>`
|
|
4
|
+
// node lot.row.mjs leave <member> <date> <reason file> remove the row, one `Left on …` line under the list
|
|
5
|
+
// node lot.row.mjs tick <member> `- [ ] #<member>` → `- [x] #<member>`, that row only
|
|
6
|
+
// node lot.row.mjs untick <member> `- [x] #<member>` → `- [ ] #<member>`, that row only
|
|
7
|
+
import { arg, die, text } from './lib.mjs';
|
|
8
|
+
|
|
9
|
+
const usage = 'add <member> <sentence file> [<effort>] | leave <member> <date> <reason file> | tick <member> | untick <member>';
|
|
10
|
+
const [mode, member] = [arg(0, usage), arg(1, usage)];
|
|
11
|
+
const lines = text(0).split('\n');
|
|
12
|
+
const start = lines.indexOf('## The order'); // a tick on a body without it reads the whole body
|
|
13
|
+
if (start < 0 && !/^(un)?tick$/.test(mode)) die('the body has no `## The order` section');
|
|
14
|
+
// The section's end, and the indexes in it of the lines matching <re>: read afresh after each splice.
|
|
15
|
+
const end = () => { const e = lines.findIndex((l, i) => i > start && l.startsWith('## ')); return e < 0 ? lines.length : e; };
|
|
16
|
+
const find = (re) => lines.flatMap((l, i) => (i > start && i < end() && re.test(l) ? [i] : []));
|
|
17
|
+
const ROW = /^- \[[ xX]\] #\d+\b/;
|
|
18
|
+
|
|
19
|
+
const mine = find(new RegExp(`^- \\[[ xX]\\] #${member}\\b`))[0];
|
|
20
|
+
if (mode === 'tick') {
|
|
21
|
+
if (mine !== undefined) lines[mine] = lines[mine].replace(/^- \[ \]/, '- [x]');
|
|
22
|
+
} else if (mode === 'untick') {
|
|
23
|
+
if (mine !== undefined) lines[mine] = lines[mine].replace(/^- \[[xX]\]/, '- [ ]');
|
|
24
|
+
} else if (mode === 'add') {
|
|
25
|
+
if (mine === undefined) {
|
|
26
|
+
const effort = process.argv[5] ? ` (${process.argv[5]})` : '';
|
|
27
|
+
const row = `- [ ] #${member}${effort} ${text(arg(2, usage))}`;
|
|
28
|
+
const [rows, left] = [find(ROW), find(/^Left on /)];
|
|
29
|
+
if (rows.length) lines.splice(rows.at(-1) + 1, 0, row);
|
|
30
|
+
else if (left.length) lines.splice(left[0], 0, row, '');
|
|
31
|
+
else lines.splice(find(/./).at(-1) + 1 || start + 1, 0, '', row);
|
|
32
|
+
}
|
|
33
|
+
} else if (mode === 'leave') {
|
|
34
|
+
const note = `Left on ${arg(2, usage)}: #${member} — ${text(arg(3, usage))}`;
|
|
35
|
+
if (mine !== undefined) lines.splice(mine, lines[mine - 1] === '' && lines[mine + 1] === '' ? 2 : 1);
|
|
36
|
+
if (!lines.includes(note)) {
|
|
37
|
+
const under = find(/^(- \[[ xX]\] #\d+\b|Left on )/);
|
|
38
|
+
if (under.length) lines.splice(under.at(-1) + 1, 0, ...(ROW.test(lines[under.at(-1)]) ? ['', note] : [note]));
|
|
39
|
+
else lines.splice(find(/./).at(-1) + 1 || start + 1, 0, '', note);
|
|
40
|
+
}
|
|
41
|
+
} else die(`unknown mode ${mode}: ${usage}`);
|
|
42
|
+
console.log(lines.join('\n'));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// `codelife mr comments <n>` (C-865, C-867): what a later review pass carries and what /work-feedback
|
|
2
|
+
// answers — the latest review pass whole, /work-feedback's answers to it, the comments people wrote
|
|
3
|
+
// after it, and the threads a person opened that are still open. A pass is a general comment whose
|
|
4
|
+
// first line reads `codelife: review — pass <k> — <sha>`, an answer `codelife: answer — pass <k> —`,
|
|
5
|
+
// or `codelife: answer — comments —` where no pass was posted;
|
|
6
|
+
// a record of codelife's other than these is no one's comment. Prints one `### comments` summary line block, then each body under its own heading.
|
|
7
|
+
// Each list is read once. A thread is open when its first note is resolvable and not resolved —
|
|
8
|
+
// on GitLab a diff discussion or a general one a person started, GitLab's system notes counted in
|
|
9
|
+
// none; on gh a review thread, a conversation comment being no thread.
|
|
10
|
+
import { arg, cli, first, origin, run } from './lib.mjs';
|
|
11
|
+
|
|
12
|
+
const n = arg(0, '<n>');
|
|
13
|
+
const PASS = /^codelife: review — pass (\d+) — ([0-9a-f]{7,40})$/;
|
|
14
|
+
let notes, threads;
|
|
15
|
+
if (cli() === 'glab') {
|
|
16
|
+
const { host, project } = origin();
|
|
17
|
+
const url = (x) => `https://${host}/${project}/-/merge_requests/${n}#note_${x.id}`;
|
|
18
|
+
const read = (type) => JSON.parse(run(['mr', 'note', 'list', n, '-t', type, '-F', 'json']) || '[]')
|
|
19
|
+
.map((d) => d.notes.filter((x) => !x.system)).filter((said) => said.length).map((said) => ({ said, author: said[0].author?.username,
|
|
20
|
+
path: said[0].position?.new_path ?? said[0].position?.old_path, open: !!said[0].resolvable && !said[0].resolved,
|
|
21
|
+
first: first(said[0].body), last: said.length > 1 ? first(said.at(-1).body) : '', url: url(said[0]) }));
|
|
22
|
+
const general = read('general');
|
|
23
|
+
threads = [...read('diff'), ...general];
|
|
24
|
+
notes = general.flatMap((t) => t.said.map((x) => ({ author: x.author?.username, date: x.created_at || '', body: x.body, url: url(x), open: t.open })));
|
|
25
|
+
} else {
|
|
26
|
+
notes = JSON.parse(run(['pr', 'view', n, '--json', 'comments'])).comments
|
|
27
|
+
.map((c) => ({ author: c.author?.login, date: c.createdAt || '', body: c.body, url: c.url }));
|
|
28
|
+
const [o, r] = origin().project.split('/');
|
|
29
|
+
const query = 'query($o:String!,$r:String!,$n:Int!,$endCursor:String){repository(owner:$o,name:$r){pullRequest(number:$n){reviewThreads(first:50,after:$endCursor){pageInfo{hasNextPage endCursor} nodes{isResolved path first:comments(first:1){nodes{body url author{login}}} last:comments(last:1){nodes{body}} comments{totalCount}}}}}}';
|
|
30
|
+
threads = JSON.parse(run(['api', 'graphql', '--paginate', '--slurp', '-F', `o=${o}`, '-F', `r=${r}`, '-F', `n=${n}`, '-f', `query=${query}`]))
|
|
31
|
+
.flatMap((p) => p.data.repository.pullRequest.reviewThreads.nodes).map((t) => ({ author: t.first.nodes[0]?.author?.login, path: t.path,
|
|
32
|
+
open: !t.isResolved, first: first(t.first.nodes[0]?.body), last: t.comments.totalCount > 1 ? first(t.last.nodes[0]?.body) : '', url: t.first.nodes[0]?.url }));
|
|
33
|
+
}
|
|
34
|
+
notes.sort((x, y) => x.date.localeCompare(y.date));
|
|
35
|
+
const passes = notes.map((c, i) => ({ ...c, i, m: first(c.body).match(PASS) })).filter((c) => c.m);
|
|
36
|
+
const last = passes.at(-1);
|
|
37
|
+
const since = notes.slice(last ? last.i + 1 : 0);
|
|
38
|
+
const answers = since.filter((c) => first(c.body).startsWith(last ? `codelife: answer — pass ${last.m[1]} — ` : 'codelife: answer — comments — '));
|
|
39
|
+
const after = since.filter((c) => !c.open && !first(c.body).startsWith('codelife:'));
|
|
40
|
+
const open = threads.filter((t) => t.open && !t.first.startsWith('codelife:'));
|
|
41
|
+
|
|
42
|
+
const out = ['### comments',
|
|
43
|
+
`- latest pass: ${last ? `${last.m[1]} — ${last.m[2]} — by ${last.author} — ${last.url}` : 'none'}`,
|
|
44
|
+
`- passes: ${passes.length ? passes.map((p) => `${p.m[1]} ${p.url}`).join(', ') : 'none'}`,
|
|
45
|
+
`- after it: ${answers.length} answer${answers.length === 1 ? '' : 's'}, ${after.length} comment${after.length === 1 ? '' : 's'} by people, ${open.length} open thread${open.length === 1 ? '' : 's'} a person opened`];
|
|
46
|
+
if (last) out.push('', `#### pass ${last.m[1]} — ${last.url}`, '', last.body.trim());
|
|
47
|
+
for (const c of answers) out.push('', `#### answer to ${last ? `pass ${last.m[1]}` : 'the comments'} by ${c.author} — ${c.url}`, '', c.body.trim());
|
|
48
|
+
for (const c of after) out.push('', `#### comment by ${c.author} — ${c.date} — ${c.url}`, '', c.body.trim());
|
|
49
|
+
for (const t of open) out.push('', `#### open thread by ${t.author}${t.path ? ` on ${t.path}` : ''} — ${t.url}`, '', `${t.first}${t.last ? `\n\nlatest: ${t.last}` : ''}`);
|
|
50
|
+
console.log(out.join('\n'));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// tracker.mr.create(b, target, title file, body file): the merge request of branch `b` into `target`,
|
|
2
|
+
// its source branch removed at the merge (the plugin's decision, not the project's checkbox) — prints
|
|
3
|
+
// its link. `node mr.create.mjs <branch> <target> <title file> <body file>`.
|
|
4
|
+
import { arg, body, cli, run, text } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
const [b, target] = [0, 1].map((i) => arg(i, '<branch> <target> <title file> <body file>'));
|
|
7
|
+
const title = text(arg(2, '<branch> <target> <title file> <body file>')), file = arg(3, '<branch> <target> <title file> <body file>');
|
|
8
|
+
const description = body(file);
|
|
9
|
+
const out = cli() === 'glab'
|
|
10
|
+
? run(['mr', 'create', '-s', b, '-b', target, '-t', title, '-d', description, '--remove-source-branch', '--yes', '--no-editor'])
|
|
11
|
+
: run(['pr', 'create', '--base', target, '--head', b, '--title', title, '--body-file', file]);
|
|
12
|
+
console.log(out.match(/https?:\/\/\S+/g)?.at(-1) ?? out.trim());
|