@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,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife port prep` (K-2 — port-toolchain, R-158, R-166): everything /port-toolchain states before
|
|
3
|
+
// it proposes — the kind and the ported package, each toolchain item's state with its reasons and its
|
|
4
|
+
// `port:` issue, the toolchain's summary line, and the first item to propose. Reads only; one
|
|
5
|
+
// `### port prep` block; a `- stop:` line per thing that stops the run, and exit 1.
|
|
6
|
+
import '../node-floor.mjs'
|
|
7
|
+
import { check, contract } from '../contract.mjs'
|
|
8
|
+
import { auth, git, items as base, origin, say } from '../setup/lib.mjs'
|
|
9
|
+
import { dir, shown, state } from './lib.mjs'
|
|
10
|
+
|
|
11
|
+
if (process.argv.length > 2) { console.error('usage: codelife port prep'); process.exit(2) }
|
|
12
|
+
console.log('### port prep')
|
|
13
|
+
const stop = (why) => { say('stop', why); process.exit(1) }
|
|
14
|
+
if (git('rev-parse', '--show-toplevel').status !== 0) stop('not a git repository')
|
|
15
|
+
const c = contract()
|
|
16
|
+
if (!c) stop('no contract — run /setup first')
|
|
17
|
+
const { invalid } = check(c)
|
|
18
|
+
if (invalid.length) stop(`the contract is invalid — ${invalid.join('; ')}: /setup`)
|
|
19
|
+
const kind = c.data.kind
|
|
20
|
+
if (kind === 'none') stop('kind: none — there is no toolchain to port')
|
|
21
|
+
const forge = c.data.tracker?.kind ?? 'none'
|
|
22
|
+
const o = origin()
|
|
23
|
+
const connected = forge === 'none' || Boolean(o && auth(forge, o.host).connected)
|
|
24
|
+
if (!connected) stop(`this machine is not connected to ${o?.host ?? 'its host'} — /setup connects it`)
|
|
25
|
+
const missing = base({ forge, connected }).filter((i) => ['scripts', 'hooks'].includes(i.item) && i.state === 'to do')
|
|
26
|
+
if (missing.length) stop(`the base is not in place — ${missing.map((i) => `${i.item}: ${i.why.join('; ')}`).join(' · ')}: /setup`)
|
|
27
|
+
const dirty = git('status', '--porcelain').stdout.trim()
|
|
28
|
+
if (dirty) stop(`the tree is not clean — an item's commit holds that item's changes alone: commit or stash yours first\n${dirty}`)
|
|
29
|
+
|
|
30
|
+
say('kind', `${kind} — the package ${dir() === '.' ? 'at the root' : dir()}`)
|
|
31
|
+
const s = state({ forge, connected })
|
|
32
|
+
for (const [k, v] of shown(s)) say(k, v)
|
|
33
|
+
say('next', s.next ?? 'none — no item to do has its precondition done')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife port version <tool>` (K-2 — port-toolchain, R-162): the version a tool moves to. Past the
|
|
3
|
+
// repository's minimumReleaseAge always; vite-plus itself, its newest release past it; a tool
|
|
4
|
+
// vite-plus carries, the exact version that release pins; any other, the newest release past it that
|
|
5
|
+
// every peerDependencies range of pnpm-lock.yaml admits. A move across a major is a `- ask:` line,
|
|
6
|
+
// with the newest release inside the current major; a newer release the window still holds is a
|
|
7
|
+
// `- held:` line. Reads only, from the registry and the lockfile; exit 1 when the registry cannot answer.
|
|
8
|
+
import '../node-floor.mjs'
|
|
9
|
+
import { say } from '../setup/lib.mjs'
|
|
10
|
+
import { admitted, at, carried, deps, installed, peers, releases, vitePlus, window } from './lib.mjs'
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
|
|
13
|
+
const [tool] = process.argv.slice(2)
|
|
14
|
+
if (!tool || process.argv.length > 3) { console.error('usage: codelife port version <tool>'); process.exit(2) }
|
|
15
|
+
console.log('### port version')
|
|
16
|
+
const fail = (why) => { say('stop', `the registry did not answer — ${why}`); process.exit(1) }
|
|
17
|
+
const date = (v, list) => list.find((r) => r.v === v)?.date ?? 'date unknown'
|
|
18
|
+
const major = (v) => v?.split('.')[0]
|
|
19
|
+
let pj = null
|
|
20
|
+
try { pj = JSON.parse(readFileSync(at('package.json'), 'utf8')) } catch { /* none */ }
|
|
21
|
+
const current = installed(tool) ?? deps(pj)[tool]?.replace(/^[\^~=v]+/, '') ?? null
|
|
22
|
+
say('current', current ? `${tool} ${current}` : `${tool} is not a dependency yet`)
|
|
23
|
+
say('window', `${window()} minutes — minimumReleaseAge`)
|
|
24
|
+
|
|
25
|
+
const r = releases(tool)
|
|
26
|
+
if (r.error) fail(r.error)
|
|
27
|
+
let chosen, why
|
|
28
|
+
if (tool === 'vite-plus' || carried(tool)) {
|
|
29
|
+
const vp = vitePlus()
|
|
30
|
+
if (vp.error) fail(vp.error)
|
|
31
|
+
chosen = tool === 'vite-plus' ? vp.v : vp.pins[tool]
|
|
32
|
+
if (!chosen) { say('stop', `vite-plus ${vp.v} pins no ${tool} — it is not a tool the toolchain carries`); process.exit(1) }
|
|
33
|
+
why = tool === 'vite-plus' ? 'the newest past the window' : `pinned exactly by vite-plus ${vp.v} (${vp.date}), the newest past the window — not the registry's newest`
|
|
34
|
+
} else {
|
|
35
|
+
const ranges = peers(tool)
|
|
36
|
+
for (const [p, range] of ranges) say('range', `${p} ${range}`)
|
|
37
|
+
const a = admitted(tool, ranges)
|
|
38
|
+
if (a.error) fail(a.error)
|
|
39
|
+
chosen = r.past.find((x) => !a.set || a.set.has(x.v))?.v
|
|
40
|
+
if (!chosen) { say('stop', `no release past the window is admitted by every range above`); process.exit(1) }
|
|
41
|
+
why = ranges.length ? 'the newest past the window that every peer range admits' : 'the newest past the window — no peer range in the lockfile'
|
|
42
|
+
}
|
|
43
|
+
say('version', `${tool} ${chosen} (${date(chosen, r.all)}) — ${why}`)
|
|
44
|
+
if (current === chosen) say('move', 'none — already on it')
|
|
45
|
+
if (current && major(chosen) !== major(current)) {
|
|
46
|
+
const inside = r.past.find((x) => major(x.v) === major(current))
|
|
47
|
+
say('ask', `${chosen} crosses a major from ${current} — put it to the user; the newest inside ${major(current)}.x past the window is ${inside ? `${inside.v} (${inside.date})` : 'none'}`)
|
|
48
|
+
}
|
|
49
|
+
const young = r.all.find((x) => !r.past.includes(x))
|
|
50
|
+
if (young && r.all.indexOf(young) < r.all.findIndex((x) => x.v === chosen)) say('held', `${young.v} (${young.date}) is inside the window — only if the work needs it, put to the user beside ${chosen}`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Holds what is fast and what the push tier does not, run by the pre-commit hook from the
|
|
3
|
+
// repository's root: the specification's form for every kind, and for a library the outputs its
|
|
4
|
+
// contract declares. It runs no verification tier — `codelife verify commit` is the commit tier's one
|
|
5
|
+
// run, and a hand commit is held by pre-push, whose tier contains the commit tier, before it reaches
|
|
6
|
+
// the remote.
|
|
7
|
+
import './node-floor.mjs'
|
|
8
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { entries, field } from './contract.mjs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const run = (cmd, opts = {}) => {
|
|
16
|
+
const r = spawnSync(cmd[0], cmd.slice(1), { stdio: 'inherit', shell: process.platform === 'win32', ...opts })
|
|
17
|
+
if (r.status !== 0) return r.status ?? 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// An edit to docs/spec/ is held here, read from the index — what this commit carries, not what the
|
|
21
|
+
// working tree holds beside it (#1085): docs/spec/ exported to a folder of its own, the form script
|
|
22
|
+
// run there. Once the seven entries exist, what breaks the form is refused; before that the script
|
|
23
|
+
// says the specification is not in the form. The script is the package's own, run by `node` alone.
|
|
24
|
+
const index = mkdtempSync(join(tmpdir(), 'codelife-index-'))
|
|
25
|
+
const staged = execFileSync('git', ['ls-files', '-z', '--', 'docs/spec'], { encoding: 'utf8' })
|
|
26
|
+
if (staged) execFileSync('git', ['checkout-index', '-z', '--stdin', `--prefix=${index}/`], { input: staged })
|
|
27
|
+
const form = run(['node', fileURLToPath(new URL('./spec-form.mjs', import.meta.url)), 'spec'], { cwd: index })
|
|
28
|
+
rmSync(index, { recursive: true, force: true })
|
|
29
|
+
if (form) process.exit(form)
|
|
30
|
+
|
|
31
|
+
if (field('kind') === 'lib') {
|
|
32
|
+
// A library versions its build, and the commit tier does not build it — pack is a push-tier cost —
|
|
33
|
+
// so this runs each generated[] entry the contract declares with a `when` and stages its path with
|
|
34
|
+
// the commit: the former bundler's command until item d of the port names vp pack, nothing where
|
|
35
|
+
// nothing is declared. Unconditionally, not behind a "did src/ change" test: the build is the
|
|
36
|
+
// authority on whether its output moved. `-A` so that a file the build stopped producing is staged
|
|
37
|
+
// as the deletion it is. Built from the WORKING TREE, so when the index and the tree differ the
|
|
38
|
+
// commit can carry output its own source does not.
|
|
39
|
+
for (const g of entries('generated').filter((e) => e.when.length)) {
|
|
40
|
+
for (const c of g.command) { const s = run([c], { shell: true }); if (s) process.exit(s) }
|
|
41
|
+
if (g.path.length) execFileSync('git', ['add', '-A', '--', ...g.path], { stdio: 'inherit' })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the complete verification tier before a push, so that a red tip never reaches the remote:
|
|
3
|
+
// the repository's `verify:push`, run through the package manager it names. When what is pushed is
|
|
4
|
+
// a version tag it runs `verify:release` instead, which contains it, so a tag pushed by hand,
|
|
5
|
+
// outside `/release`, is held by the same proof of the package as one `/release` cuts;
|
|
6
|
+
// `--no-verify` passes over it, the limit every hook already accepts.
|
|
7
|
+
// This is the tier /audit uses as its baseline, so what passes here is what the audit assumes
|
|
8
|
+
// about the commit it reads.
|
|
9
|
+
//
|
|
10
|
+
// This hook runs INSIDE a connection git has already opened: the refs on stdin exist only because
|
|
11
|
+
// git contacted the remote and read them first, and that socket is then held idle for as long as
|
|
12
|
+
// the tier takes. A remote that drops idle connections fails the push of a tip this tier just
|
|
13
|
+
// passed. There is no hook before the connection, so the answer is not here but in the clone's
|
|
14
|
+
// `core.sshCommand` keepalive, which the port imposes beside `core.hooksPath` — without it, a long
|
|
15
|
+
// tier and a proxied remote break a green push.
|
|
16
|
+
//
|
|
17
|
+
// In the WORKING TREE, where the dependencies are installed. A tier run anywhere else meets a pnpm
|
|
18
|
+
// workspace without its per-package node_modules, which are gitignored, and pnpm's pre-run check
|
|
19
|
+
// installs them there — through any link back into the clone, rewriting the clone's own
|
|
20
|
+
// node_modules, or refusing the push when it cannot. What that costs: the tree is measured instead
|
|
21
|
+
// of the tip the remote will hold, so uncommitted work can turn the tier green on a red tip, or
|
|
22
|
+
// refuse a push whose commits are all fine.
|
|
23
|
+
import './node-floor.mjs'
|
|
24
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
25
|
+
import { readFileSync } from 'node:fs'
|
|
26
|
+
import { packageManager, script } from './package-manager.mjs'
|
|
27
|
+
|
|
28
|
+
// One line per ref git is about to send: <local ref> <local sha> <remote ref> <remote sha>. A
|
|
29
|
+
// deletion carries the null sha as its local one: nothing arrives at the remote, so a push of
|
|
30
|
+
// deletions alone has nothing to verify. A hand run has no stdin and verifies nothing.
|
|
31
|
+
let refs = ''
|
|
32
|
+
try { refs = readFileSync(0, 'utf8') } catch { /* no stdin */ }
|
|
33
|
+
if (!refs.split('\n').some((l) => /[^0]/.test(l.split(/\s+/)[1] ?? ''))) process.exit(0)
|
|
34
|
+
|
|
35
|
+
const git = (...a) => execFileSync('git', a, { encoding: 'utf8' }).trim()
|
|
36
|
+
const top = git('rev-parse', '--show-toplevel')
|
|
37
|
+
// Git hands a hook its repository-local variables — the git directory of a linked worktree. Left
|
|
38
|
+
// set, every `git` the tier runs in a repository of its own reads and writes this one. Cleared
|
|
39
|
+
// after the lookup above, which still needs them.
|
|
40
|
+
const env = { ...process.env }
|
|
41
|
+
for (const k of git('rev-parse', '--local-env-vars').split('\n')) delete env[k]
|
|
42
|
+
|
|
43
|
+
const pm = packageManager(top)
|
|
44
|
+
if (!pm) {
|
|
45
|
+
console.error('\n Neither packageManager nor a lockfile names the package manager, so the push tier cannot run and the push is refused.\n')
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
// A version tag: the remote ref, what will exist there — `HEAD:refs/tags/v1.0.0` too (#1058) — under
|
|
49
|
+
// refs/tags/ with a name of `v` and a digit, pushed rather than deleted.
|
|
50
|
+
const versionTag = refs.split('\n').some((l) => { const [, sha, remote] = l.split(/\s+/); return /^refs\/tags\/v\d/.test(remote ?? '') && /[^0]/.test(sha ?? '') })
|
|
51
|
+
const [cmd, ...args] = script(pm, versionTag ? 'verify:release' : 'verify:push')
|
|
52
|
+
const r = spawnSync(cmd, args, { cwd: top, env, stdio: 'inherit', shell: process.platform === 'win32' })
|
|
53
|
+
process.exit(r.status ?? 1)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// What the preparations share (R-288) — /work's preparation and the branch script it
|
|
2
|
+
// both run: the summary a run is made from, the stop that ends it before any table, and the reads of the
|
|
3
|
+
// scripts under scripts/tracker/, scripts/git/ and scripts/read-roadmap/ — and the one umbrella close the `issue close`
|
|
4
|
+
// and `lot leave` verbs share. Nothing here reads stdin.
|
|
5
|
+
import './node-floor.mjs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { valid } from './contract.mjs';
|
|
12
|
+
|
|
13
|
+
const scripts = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const spawn = (file, args, input = '') => spawnSync(file, args, { encoding: 'utf8', input, maxBuffer: 256 * 1024 * 1024 });
|
|
15
|
+
|
|
16
|
+
export const said = (r) => (r.stdout + r.stderr).trim() || r.error?.message || '';
|
|
17
|
+
export const first = (body) => body.split('\n')[0];
|
|
18
|
+
// The first line a comment says below its heading: a STATUS's reason, a closing's verdict.
|
|
19
|
+
export const gist = (body) => body.split('\n').slice(1).find((l) => l.trim())?.trim();
|
|
20
|
+
|
|
21
|
+
// prep(heading) — { say, stop, done, include, op, must, title, kind }: the summary under `### <heading>`.
|
|
22
|
+
export const prep = (heading) => {
|
|
23
|
+
const summary = [`### ${heading}`];
|
|
24
|
+
const say = (line) => summary.push(`- ${line}`);
|
|
25
|
+
const stop = (why) => {
|
|
26
|
+
say(`stop: ${why}`);
|
|
27
|
+
console.log(summary.join('\n'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
};
|
|
30
|
+
const done = () => {
|
|
31
|
+
console.log(summary.join('\n'));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
};
|
|
34
|
+
// op <domain> <operation> <args> [stdin] — scripts/<domain>/<operation>.mjs: { status, stdout, stderr }.
|
|
35
|
+
const op = (skill, operation, args = [], input) => spawn(process.execPath, [join(scripts, skill, `${operation}.mjs`), ...args], input);
|
|
36
|
+
// must — the same, its stdout, or the stop naming the operation and its error.
|
|
37
|
+
const must = (skill, operation, args, input) => {
|
|
38
|
+
const r = op(skill, operation, args, input);
|
|
39
|
+
if (r.status !== 0) stop(`scripts/${skill}/${operation}.mjs failed — ${said(r)}`);
|
|
40
|
+
return r.stdout;
|
|
41
|
+
};
|
|
42
|
+
// include — another script's summary lines, kept; its stop is this run's.
|
|
43
|
+
const include = (r) => {
|
|
44
|
+
for (const l of r.stdout.split('\n').filter((l) => l.startsWith('- '))) summary.push(l);
|
|
45
|
+
if (r.status !== 0) {
|
|
46
|
+
if (!r.stdout.includes('- stop: ')) say(`stop: ${said(r)}`);
|
|
47
|
+
console.log(summary.join('\n'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
// title <script> <text> [args…] — a title script of scripts/tracker/, the text on its stdin.
|
|
52
|
+
const title = (script, text, ...args) => spawn(process.execPath, [join(scripts, 'tracker', script), ...args], text).stdout.trim();
|
|
53
|
+
// kind — the contract's `tracker.kind`, `none` when it names no forge.
|
|
54
|
+
const kind = () => { const v = valid(); return v.stop ? stop(v.stop) : v.contract.data.tracker?.kind || 'none'; };
|
|
55
|
+
// slug — a title's sentence as a branch name's words, cut at a word within 40 characters.
|
|
56
|
+
const slug = (text) => {
|
|
57
|
+
const s = title('title.sentence_of.mjs', text).toLowerCase().replace(/['’]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
58
|
+
return s.length > 40 ? s.slice(0, s.lastIndexOf('-', 40)) : s;
|
|
59
|
+
};
|
|
60
|
+
return { summary, say, stop, done, include, op, must, title, kind, slug };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// tasks — an umbrella's task list in order: { n, effort }.
|
|
64
|
+
export const tasks = (body) => [...body.matchAll(/^- \[[ xX]\] #(\d+)(?: \(([A-Za-z]+)\))?/gm)].map((m) => ({ n: m[1], effort: m[2] ?? '' }));
|
|
65
|
+
|
|
66
|
+
// currentBranch — the branch checked out, '' on none.
|
|
67
|
+
export const currentBranch = () => spawn('git', ['branch', '--show-current']).stdout.trim();
|
|
68
|
+
|
|
69
|
+
// closeLot(must, lot, state, rows, by, branch) — the umbrella closed on its `lot-closing` comment
|
|
70
|
+
// (R-193, R-70), the tracker's close line: the rows as the members closed, and — only when a branch is
|
|
71
|
+
// given and a row is left — the branch and the merge request to come; `wontfix` when no row is left. A
|
|
72
|
+
// leave runs on whatever branch, so it gives none and the comment says neither. An umbrella already
|
|
73
|
+
// closed — the merge closed its members first, or an earlier caller did — gets nothing (#1042).
|
|
74
|
+
export const closeLot = (must, lot, state, rows, by, branch) => {
|
|
75
|
+
if (/^closed$/i.test(state)) return `#${lot} already closed, no lot-closing comment`;
|
|
76
|
+
const file = join(mkdtempSync(join(tmpdir(), 'lot-closing-')), 'lot-closing.md');
|
|
77
|
+
writeFileSync(file, [`codelife: lot-closing — ${by} — ${new Date().toISOString().slice(0, 10)}`, '',
|
|
78
|
+
...(branch ? [`Branch: \`${branch}\`.`] : []), `Members closed: ${rows.map((n) => `#${n}`).join(', ') || 'none'}.`,
|
|
79
|
+
...(rows.length && branch ? [`Merge request: not yet opened — \`/work ${lot}\` opens it from the branch once every member's commit is there.`] : []), ''].join('\n'));
|
|
80
|
+
return must('tracker', 'close', [lot, file, ...(rows.length ? [] : ['wontfix'])]).trim();
|
|
81
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The one reading of the roadmap (K-41's R-277), which `codelife roadmap prep|write`, `codelife
|
|
2
|
+
// work prep` and `codelife overview` import: a line of its `## The order`, the lots that can start,
|
|
3
|
+
// an umbrella's count, the roadmap and its mark, and what a range of the specification changed.
|
|
4
|
+
import '../node-floor.mjs';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
// `3. #412 Undo of a stroke — after #405, #408` → { lot: '412', after: ['405', '408'] }; null for a
|
|
12
|
+
// line out of shape, an `after` in another form included — never read as waiting for nothing.
|
|
13
|
+
export const orderLine = (line) => {
|
|
14
|
+
if (!/^[0-9]+\. #[0-9]+ [^ ]/.test(line)) return null;
|
|
15
|
+
const lot = line.split(/[ \t]+/)[1].slice(1);
|
|
16
|
+
const i = line.indexOf(' — after ');
|
|
17
|
+
if (i < 0) return /after #/i.test(line) ? null : { lot, after: [] };
|
|
18
|
+
const after = line.slice(i + ' — after '.length);
|
|
19
|
+
if (!/^#[0-9]+(, #[0-9]+)*$/.test(after)) return null;
|
|
20
|
+
return { lot, after: after.replaceAll('#', '').split(', ') };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// canStart(order, open) — the open lots that can start, in the order's order: every lot each waits
|
|
24
|
+
// for is closed. `order` is orderLine's objects, `open` the open umbrellas' numbers as strings.
|
|
25
|
+
export const canStart = (order, open) =>
|
|
26
|
+
order.filter((o) => open.includes(o.lot) && !o.after.some((l) => open.includes(l))).map((o) => o.lot);
|
|
27
|
+
|
|
28
|
+
// lotCount(body) — an umbrella's rows: { open, total, effort }, the effort of its open rows as
|
|
29
|
+
// `2S 1M`, or `effort none` when no open row carries one.
|
|
30
|
+
export const lotCount = (body) => {
|
|
31
|
+
const rows = [...body.matchAll(/^- \[([ xX])\] #(\d+)(?: \(([SML])\))?/gm)];
|
|
32
|
+
const open = rows.filter((m) => m[1] === ' ');
|
|
33
|
+
const effort = ['S', 'M', 'L'].map((e) => `${open.filter((m) => m[3] === e).length}${e}`).filter((x) => !x.startsWith('0')).join(' ');
|
|
34
|
+
return { open: open.length, total: rows.length, effort: effort || 'effort none' };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// What moved and what a range changed, from the scripts under scripts/tracker/ and scripts/git/. `op` runs one of
|
|
38
|
+
// them, `must` the same but throws its error, which a script prints as the operation's answer.
|
|
39
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
40
|
+
const op = (skill, operation, args = [], input = '') =>
|
|
41
|
+
spawnSync(process.execPath, [join(pkg, 'scripts', skill, `${operation}.mjs`), ...args], { encoding: 'utf8', input, maxBuffer: 256 * 1024 * 1024 });
|
|
42
|
+
const said = (r) => (r.stdout + r.stderr).trim() || r.error?.message || '';
|
|
43
|
+
const must = (skill, operation, args, input) => {
|
|
44
|
+
const r = op(skill, operation, args, input);
|
|
45
|
+
if (r.status !== 0) throw new Error(`scripts/${skill}/${operation}.mjs failed — ${said(r)}`);
|
|
46
|
+
return r.stdout;
|
|
47
|
+
};
|
|
48
|
+
const lines = (text) => text.split('\n').filter(Boolean);
|
|
49
|
+
const section = (text, heading) => text.split(new RegExp(`^## ${heading}[ \\t]*$`, 'm'))[1]?.split(/^## /m)[0] ?? '';
|
|
50
|
+
|
|
51
|
+
// roadmap() — { roadmap, mark }: `#<n>`, `none`, `two: #a, #b` or `could not tell: <why>`, and the
|
|
52
|
+
// mark `<sha>, contained|not contained`, `out of shape: <text>`, `unresolved: <git>` or `none`, with
|
|
53
|
+
// `sha` when git resolves it. A lookup that fails could not tell, and is never `none` (R-279).
|
|
54
|
+
export const roadmap = () => {
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), 'read-roadmap-')), key = join(dir, 'key');
|
|
56
|
+
writeFileSync(key, 'roadmap');
|
|
57
|
+
const r = op('tracker', 'search_id', [key]);
|
|
58
|
+
rmSync(dir, { recursive: true, force: true });
|
|
59
|
+
if (r.status !== 0) return { roadmap: `could not tell: ${said(r)}`, mark: 'none' };
|
|
60
|
+
const found = lines(r.stdout).map((l) => l.match(/^#?(\d+)/)?.[1]).filter(Boolean);
|
|
61
|
+
if (!found.length) return { roadmap: 'none', mark: 'none' };
|
|
62
|
+
if (found.length > 1) return { roadmap: `two: ${found.map((n) => `#${n}`).join(', ')}`, mark: 'none' };
|
|
63
|
+
const body = JSON.parse(must('tracker', 'view', [found[0]])).body;
|
|
64
|
+
const text = lines(section(body, 'Mark'))[0] ?? '';
|
|
65
|
+
const sha = text.match(/^`([0-9a-f]{7,64})`/)?.[1];
|
|
66
|
+
if (!sha) return { roadmap: `#${found[0]}`, mark: `out of shape: ${text}` };
|
|
67
|
+
const at = op('git', 'is_ancestor', [sha, 'HEAD']);
|
|
68
|
+
if (at.status !== 0) return { roadmap: `#${found[0]}`, mark: `unresolved: ${said(at)}` };
|
|
69
|
+
return { roadmap: `#${found[0]}`, mark: `${sha}, ${at.stdout.trim()}`, sha };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// range(mark, head) — what the specification changed between two commits, read as one change, final
|
|
73
|
+
// state only: { changed, created, retired, other } — each `{ id, file, section }`, `other` without
|
|
74
|
+
// an id — or { unresolved } when either commit does not resolve, since a diff from a commit git
|
|
75
|
+
// cannot read comes back empty, and empty is not *nothing changed*. A line added belongs to the rule
|
|
76
|
+
// or the case whose heading stands above it at `head`, a line removed to the one it stood under at
|
|
77
|
+
// `mark`; a heading reworded is the same rule, changed, and one no heading at `head` carries any
|
|
78
|
+
// more is retired.
|
|
79
|
+
export const range = (mark, head) => {
|
|
80
|
+
const anc = op('git', 'is_ancestor', [mark, head]);
|
|
81
|
+
if (anc.status !== 0) return { unresolved: said(anc) };
|
|
82
|
+
const touched = new Map(), other = new Map(), atMark = new Set(), atHead = new Set(), added = new Set();
|
|
83
|
+
const id = (l) => l.match(/^#+ ([A-Z]+-\d+) — /)?.[1];
|
|
84
|
+
// where — for each line of a file, the identifier whose heading stands above it and the `##` section.
|
|
85
|
+
const where = (text) => {
|
|
86
|
+
let cur = null, sec = '';
|
|
87
|
+
return text.split('\n').map((l) => {
|
|
88
|
+
if (/^#+ /.test(l)) cur = id(l) ?? null;
|
|
89
|
+
if (/^## /.test(l)) sec = l.slice(3);
|
|
90
|
+
return { id: cur, section: sec };
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const files = JSON.parse(must('git', 'diff', [mark, head])).filter((f) => `${f.old ?? ''} ${f.path}`.includes('docs/spec/'));
|
|
94
|
+
for (const f of files) {
|
|
95
|
+
const old = f.old ?? f.path;
|
|
96
|
+
const before = f.status === 'A' ? [] : must('git', 'show', [mark], old);
|
|
97
|
+
const after = f.status === 'D' ? [] : must('git', 'show', [head], f.path);
|
|
98
|
+
for (const [text, set] of [[before, atMark], [after, atHead]]) for (const l of String(text).split('\n')) if (id(l)) set.add(id(l));
|
|
99
|
+
const [was, now] = [String(before), String(after)].map(where);
|
|
100
|
+
const hit = (file, at) => at && (at.id ? touched.set(at.id, { id: at.id, file, section: at.section })
|
|
101
|
+
: other.set(`${file}\0${at.section}`, { file, section: at.section }));
|
|
102
|
+
for (const hunk of must('git', 'diff_lines', [mark, head], [f.old, f.path].filter(Boolean).join('\0')).split(/^@@ /m).slice(1)) {
|
|
103
|
+
const [, a, b = '1', c, d = '1'] = hunk.match(/^-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
104
|
+
for (let i = 0; i < +b; i++) hit(old, was[+a - 1 + i]);
|
|
105
|
+
for (let i = 0; i < +d; i++) hit(f.path, now[+c - 1 + i]);
|
|
106
|
+
for (const l of hunk.split('\n')) if (l.startsWith('+') && id(l.slice(1))) added.add(id(l.slice(1)));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const all = [...touched.values()];
|
|
110
|
+
return {
|
|
111
|
+
changed: all.filter((t) => atHead.has(t.id) && !(added.has(t.id) && !atMark.has(t.id))),
|
|
112
|
+
created: all.filter((t) => added.has(t.id) && !atMark.has(t.id)),
|
|
113
|
+
retired: all.filter((t) => !atHead.has(t.id)),
|
|
114
|
+
other: [...other.values()],
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// read-roadmap.moved(n…): whether a rule each issue cites moved since the roadmap's mark. An issue
|
|
2
|
+
// filed from the specification describes a rule as it stood; when a brainstorm rewrites that rule and
|
|
3
|
+
// `/roadmap` has not run since, working the issue builds the former rule. One line per issue,
|
|
4
|
+
// `- moved #<n>: passes | stops: <why>`, after the result's `roadmap:` and `mark:` lines; exit 1 when
|
|
5
|
+
// any stops. `node moved.mjs <n>…`, from the repository's root.
|
|
6
|
+
//
|
|
7
|
+
// No roadmap passes, there being no mark to compare; any other mark that is no commit stops. What an
|
|
8
|
+
// issue cites is the `R-<n>` and `C-<n>` lines of its *Where it comes from* — none passes. What
|
|
9
|
+
// moved is `range(<mark>, HEAD)`, then the same up to the local `develop`: a rule rewritten there
|
|
10
|
+
// after this branch was cut moved all the same, and the local branch, not `origin/develop`, holds a
|
|
11
|
+
// specification commit not yet pushed — `origin/develop` in a clone that has none, so holds none.
|
|
12
|
+
// A change elsewhere in the specification blocks nothing.
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { range, roadmap } from './lib.mjs';
|
|
17
|
+
|
|
18
|
+
const issues = process.argv.slice(2);
|
|
19
|
+
if (!issues.length || !issues.every((n) => /^\d+$/.test(n))) {
|
|
20
|
+
console.error('usage: node moved.mjs <n>…');
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
24
|
+
const op = (skill, operation, args = []) =>
|
|
25
|
+
spawnSync(process.execPath, [join(pkg, 'scripts', skill, `${operation}.mjs`), ...args], { encoding: 'utf8', input: '', maxBuffer: 256 * 1024 * 1024 });
|
|
26
|
+
const out = [];
|
|
27
|
+
const say = (line) => out.push(`- ${line}`);
|
|
28
|
+
let stopped = false;
|
|
29
|
+
const answer = (n, why) => {
|
|
30
|
+
if (why) stopped = true;
|
|
31
|
+
say(`moved #${n}: ${why ? `stops: ${why}` : 'passes'}`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const dev = spawnSync('git', ['rev-parse', '--verify', '--quiet', 'refs/heads/develop']).status === 0 ? 'develop' : 'origin/develop';
|
|
35
|
+
const r = roadmap();
|
|
36
|
+
say(`roadmap: ${r.roadmap}`);
|
|
37
|
+
say(`mark: ${r.mark}`);
|
|
38
|
+
// moving — the identifiers a range touched, or the stop its unresolved range is.
|
|
39
|
+
const ranges = {};
|
|
40
|
+
const moving = (head) => {
|
|
41
|
+
if (!ranges[head]) {
|
|
42
|
+
const g = range(r.sha, head);
|
|
43
|
+
ranges[head] = g.unresolved ? g : { ids: new Set([...g.changed, ...g.created, ...g.retired].map((t) => t.id)) };
|
|
44
|
+
}
|
|
45
|
+
return ranges[head];
|
|
46
|
+
};
|
|
47
|
+
// since — the `docs(spec)` commits since the mark, on HEAD or on develop.
|
|
48
|
+
const since = (develop) => {
|
|
49
|
+
const s = op('git', 'log_since', develop ? [r.sha, dev] : [r.sha]);
|
|
50
|
+
if (s.status !== 0) return `(the docs(spec) commits could not be read: ${(s.stderr || s.stdout).trim()})`;
|
|
51
|
+
return JSON.parse(s.stdout).filter((c) => c.type === 'docs' && c.scope === 'spec').map((c) => `${c.sha.slice(0, 8)} ${c.subject}`).join('; ');
|
|
52
|
+
};
|
|
53
|
+
for (const n of issues) {
|
|
54
|
+
if (r.roadmap === 'none') { answer(n); continue; }
|
|
55
|
+
if (!r.sha) { answer(n, `the roadmap gives no mark to compare — roadmap ${r.roadmap}, mark ${r.mark}`); continue; }
|
|
56
|
+
const v = op('tracker', 'view', [n]);
|
|
57
|
+
if (v.status !== 0) { answer(n, `#${n} could not be read — ${(v.stderr || v.stdout).trim()}`); continue; }
|
|
58
|
+
const where = JSON.parse(v.stdout).body.split(/^## Where it comes from[ \t]*$/m)[1]?.split(/^## /m)[0] ?? '';
|
|
59
|
+
const cited = [...where.matchAll(/^(?:- )?([RC]-\d+) — /gm)].map((m) => m[1]);
|
|
60
|
+
if (!cited.length) { answer(n); continue; }
|
|
61
|
+
const head = moving('HEAD'), develop = moving(dev);
|
|
62
|
+
const bad = head.unresolved ?? develop.unresolved;
|
|
63
|
+
if (bad) { answer(n, `the range from the mark is unresolved — ${bad}`); continue; }
|
|
64
|
+
const here = cited.filter((i) => head.ids.has(i)), there = cited.filter((i) => develop.ids.has(i) && !head.ids.has(i));
|
|
65
|
+
if (here.length) answer(n, `${here.join(', ')} moved since the mark — ${since(false)} — \`/roadmap\` rewrites or closes the issue`);
|
|
66
|
+
else if (there.length) answer(n, `${there.join(', ')} moved on develop only — ${since(true)} — \`/roadmap\` run on develop, or develop brought into this branch`);
|
|
67
|
+
else answer(n);
|
|
68
|
+
}
|
|
69
|
+
console.log(out.join('\n'));
|
|
70
|
+
process.exit(stopped ? 1 : 0);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// read-roadmap.range(mark, head): lib.mjs's `range`, as JSON, the result's `range:` field; exit 1 on
|
|
2
|
+
// `unresolved: <git's answer>`. `node range.mjs <mark> <head>`, from the repository's root.
|
|
3
|
+
import { range } from './lib.mjs';
|
|
4
|
+
|
|
5
|
+
const [mark, head] = process.argv.slice(2);
|
|
6
|
+
if (!mark || !head) {
|
|
7
|
+
console.error('usage: node range.mjs <mark> <head>');
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
const r = range(mark, head);
|
|
11
|
+
if (r.unresolved) {
|
|
12
|
+
console.log(`unresolved: ${r.unresolved}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
console.log(JSON.stringify(r));
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife release cut [<version>] [--prepare]` (K-16): the release you confirmed, made locally.
|
|
3
|
+
// The version written into the version file and every published package.json (R-244), the
|
|
4
|
+
// changelog section, the generated outputs the range since the last tag makes stale, the release
|
|
5
|
+
// tier and the guards that range fires (R-245) — a red restores what it wrote and stops —, then
|
|
6
|
+
// `chore(release): <version>`. Without --prepare, the merge and the annotated tag (R-246): develop
|
|
7
|
+
// merged into main, tagged there, develop fast-forwarded; on main, tagged then merged back into
|
|
8
|
+
// develop; on a support/ branch, tagged in place. A version already prepared, with nothing landed
|
|
9
|
+
// after its commit, goes straight to the merge. Pushes nothing: prints one `push:` line per thing to
|
|
10
|
+
// push, each yours to run. Exit 1 on a stop, 2 on a usage error.
|
|
11
|
+
import '../node-floor.mjs';
|
|
12
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { compare, tagFor } from '../tags.mjs';
|
|
14
|
+
import { fired, git, guards, paths, runChecks, tier } from '../verify/lib.mjs';
|
|
15
|
+
import { changelog, dirtyPaths, hasOrigin, level, packages, read, said, say } from './lib.mjs';
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const prepare = args.includes('--prepare');
|
|
19
|
+
const [version, ...extra] = args.filter((a) => a !== '--prepare');
|
|
20
|
+
if (extra.length || (version && !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version))) { console.error('usage: codelife release cut [<version>] [--prepare]'); process.exit(2); }
|
|
21
|
+
|
|
22
|
+
console.log('### release cut');
|
|
23
|
+
const stop = (why) => { say('stop', why); process.exit(1); };
|
|
24
|
+
const run = (...a) => { const r = git(...a); if (r.status) stop(`git ${a.join(' ')} — ${said(r)}`); return r.out; };
|
|
25
|
+
const f = read(prepare);
|
|
26
|
+
if (f.stops.length) stop(f.stops[0]);
|
|
27
|
+
const l = level(f.branch);
|
|
28
|
+
if (l.stop) stop(l.stop);
|
|
29
|
+
const name = f.now?.name ?? '';
|
|
30
|
+
|
|
31
|
+
if (!changelog()) stop('no generated[] entry writes CHANGELOG.md — /setup');
|
|
32
|
+
let v = version;
|
|
33
|
+
if (f.resume && (!v || v === f.resume.version)) {
|
|
34
|
+
v = f.resume.version;
|
|
35
|
+
say('resumed', `${v}, prepared by ${f.resume.commit.slice(0, 8)}`);
|
|
36
|
+
if (prepare) { say('prepared', 'already — nothing to do'); process.exit(0); }
|
|
37
|
+
} else {
|
|
38
|
+
if (!v) stop(f.proposed ? `give the version you confirmed — prep proposed ${f.proposed.version}` : 'give the first version');
|
|
39
|
+
if (f.from && compare(v, f.from) <= 0) stop(`${v} is not after the last tag's ${f.from}`);
|
|
40
|
+
|
|
41
|
+
// The version, then the section — the generator reads the bumped file as the release being cut.
|
|
42
|
+
const range = f.tag ? paths('diff', '--name-only', `${f.tag}...HEAD`) : null;
|
|
43
|
+
const files = packages(f.vf);
|
|
44
|
+
for (const p of files) writeFileSync(p, readFileSync(p, 'utf8').replace(/("version"\s*:\s*")[^"]*(")/, `$1${v}$2`));
|
|
45
|
+
say('version', `${v} in ${files.join(', ')}`);
|
|
46
|
+
const restore = (why) => {
|
|
47
|
+
git('restore', '--source=HEAD', '--staged', '--worktree', '--', ':/');
|
|
48
|
+
const left = dirtyPaths();
|
|
49
|
+
stop(`${why} — every tracked file restored${left.length ? `; left untracked: ${left.join(', ')}` : ''}`);
|
|
50
|
+
};
|
|
51
|
+
const generated = guards('generated');
|
|
52
|
+
const outputs = generated.filter((g) => !(g.path ?? []).includes('CHANGELOG.md') && (!range || fired([g], range).length));
|
|
53
|
+
const shell = (cs) => (cs ?? []).map((c) => ({ name: c, file: c, shell: true }));
|
|
54
|
+
if (runChecks(shell([changelog(), ...outputs.flatMap((g) => g.command ?? [])]), '- ')) restore('a generator failed');
|
|
55
|
+
const declared = [...files, 'CHANGELOG.md', ...outputs.flatMap((g) => g.path ?? []).map((p) => p.replace(/\/$/, ''))];
|
|
56
|
+
const outside = dirtyPaths(restore).filter((p) => !declared.some((d) => p === d || p.startsWith(`${d}/`)));
|
|
57
|
+
if (outside.length) restore(`changed outside the declared paths: ${outside.join(', ')}`);
|
|
58
|
+
if (runChecks([tier('release'), ...shell(guards().filter((g) => !range || fired([g], range).length).flatMap((g) => g.run))], '- ')) restore('a check is red');
|
|
59
|
+
run('--literal-pathspecs', 'add', '-A', '--', ...dirtyPaths(restore));
|
|
60
|
+
const c = git('commit', '-m', `chore(release): ${v}`);
|
|
61
|
+
if (c.status) restore(`the commit was refused — ${said(c)}`);
|
|
62
|
+
say('commit', `${run('rev-parse', '--short=8', 'HEAD')} chore(release): ${v}`);
|
|
63
|
+
if (f.proposed?.support && v === f.proposed.version && f.kind === 'release') {
|
|
64
|
+
if (!git('rev-parse', '--verify', '--quiet', f.proposed.support).out) run('branch', f.proposed.support, f.tag);
|
|
65
|
+
say('support', `${f.proposed.support} at ${f.tag}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The section's size, counted here so that nobody counts it by hand: the brief shows it whole when it
|
|
70
|
+
// is short, and this line, its breaking entries and the file when it is not.
|
|
71
|
+
{
|
|
72
|
+
const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
|
|
73
|
+
const at = lines.findIndex((x) => x.startsWith(`## [${v}]`));
|
|
74
|
+
const end = lines.findIndex((x, i) => i > at && (x.startsWith('## ') || x.startsWith('<!--')));
|
|
75
|
+
const body = at < 0 ? [] : lines.slice(at, end < 0 ? undefined : end).filter((x) => x.trim());
|
|
76
|
+
const groups = []; for (const x of body) if (x.startsWith('### ')) groups.push([x.slice(4), 0]); else if (x.startsWith('- ') && groups.length) groups.at(-1)[1]++;
|
|
77
|
+
const breaking = body.filter((x) => x.startsWith('- ') && x.endsWith('— **breaking**')).length;
|
|
78
|
+
say('section', at < 0 ? `CHANGELOG.md has no [${v}] section` : `CHANGELOG.md [${v}], ${body.length} lines — ${[...groups.map(([g, n]) => `${g} ${n}`), `breaking ${breaking}`].join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
if (prepare) { say('prepared', `stopped at the release commit — merge this branch, then /release on develop`); process.exit(0); }
|
|
81
|
+
|
|
82
|
+
// The merge and the tag, locally; origin's copy of a branch about to move is brought forward first.
|
|
83
|
+
const tag = tagFor(v);
|
|
84
|
+
if (git('tag', '-l', tag).out) stop(`${tag} exists already — a tag consumers have fetched is never moved`);
|
|
85
|
+
const origin = hasOrigin();
|
|
86
|
+
const has = (ref) => !!git('rev-parse', '--verify', '--quiet', ref).out;
|
|
87
|
+
const contains = (a, b) => git('merge-base', '--is-ancestor', a, b).status === 0;
|
|
88
|
+
const forward = (b) => {
|
|
89
|
+
if (origin && git('ls-remote', '--exit-code', '--heads', 'origin', b).status === 0) {
|
|
90
|
+
run('fetch', 'origin', `+refs/heads/${b}:refs/remotes/origin/${b}`);
|
|
91
|
+
if (!has(`refs/heads/${b}`) || contains(b, `origin/${b}`)) run('fetch', '.', `refs/remotes/origin/${b}:refs/heads/${b}`);
|
|
92
|
+
else if (!contains(`origin/${b}`, b)) stop(`${b} diverged from origin/${b} — reconcile first`);
|
|
93
|
+
}
|
|
94
|
+
if (!has(`refs/heads/${b}`)) stop(`no ${b} branch — /setup creates it`);
|
|
95
|
+
};
|
|
96
|
+
const merge = (from, into) => {
|
|
97
|
+
forward(into);
|
|
98
|
+
run('switch', into);
|
|
99
|
+
const r = git('merge', '--no-ff', '-m', `Merge branch '${from}' into ${into} for ${v}`, from);
|
|
100
|
+
if (r.status) stop(`merging ${from} into ${into} conflicts — the merge is left in place for you: ${git('diff', '--name-only', '--diff-filter=U').out.split('\n').join(', ')}`);
|
|
101
|
+
say('merge', `${from} into ${into}: ${run('rev-parse', '--short=8', 'HEAD')}`);
|
|
102
|
+
};
|
|
103
|
+
const annotate = () => { run('tag', '-a', tag, '-m', tag); say('tag', `${tag} on ${git('branch', '--show-current').out}`); };
|
|
104
|
+
const pushes = [];
|
|
105
|
+
if (f.kind === 'release') {
|
|
106
|
+
merge('develop', 'main'); annotate();
|
|
107
|
+
run('switch', 'develop'); run('merge', '--ff-only', 'main');
|
|
108
|
+
say('develop', 'fast-forwarded to the merge');
|
|
109
|
+
pushes.push('main', 'develop');
|
|
110
|
+
} else if (f.kind === 'hotfix') {
|
|
111
|
+
annotate(); merge('main', 'develop'); run('switch', 'main');
|
|
112
|
+
pushes.push('main', 'develop');
|
|
113
|
+
} else {
|
|
114
|
+
annotate();
|
|
115
|
+
pushes.push(f.branch);
|
|
116
|
+
}
|
|
117
|
+
if (f.proposed?.support && git('rev-parse', '--verify', '--quiet', f.proposed.support).out) pushes.push(f.proposed.support);
|
|
118
|
+
if (!origin) say('push', 'nothing — no origin: the release stays in this clone');
|
|
119
|
+
else for (const p of [...pushes.map((b) => `git push origin ${b}`), `git push origin refs/tags/${tag}`]) say('push', p);
|
|
120
|
+
say('then', origin ? '`codelife release entry` once the tag is on origin' : 'nothing — no origin, no entry');
|