@ersbeth/codelife 0.0.0-bootstrap.0 → 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 +66 -0
- 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 +40 -1
- 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,308 @@
|
|
|
1
|
+
// `codelife overview` (K-6): where the repository stands, one screen, and the first rule of next.mjs
|
|
2
|
+
// that holds. Reads the contract, the tree, git, `origin`, the tracker and the registry; writes
|
|
3
|
+
// nothing, asks nothing, and never stops before the screen: a source that does not answer is printed
|
|
4
|
+
// as *unreadable*, its error quoted, never as an empty section (R-177).
|
|
5
|
+
import '../node-floor.mjs';
|
|
6
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { canStart, lotCount, orderLine, roadmap } from '../read-roadmap/lib.mjs';
|
|
11
|
+
import { next } from './next.mjs';
|
|
12
|
+
import { spellings } from '../tags.mjs';
|
|
13
|
+
import { cadence } from '../audit/cadence.mjs';
|
|
14
|
+
import { check, contract as readContract, SCHEMA } from '../contract.mjs';
|
|
15
|
+
import { auth, items, unmet } from '../setup/lib.mjs';
|
|
16
|
+
import { env, pool } from '../tracker/lib.mjs';
|
|
17
|
+
import { state as portState } from '../port/lib.mjs';
|
|
18
|
+
|
|
19
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
20
|
+
const MAX = 256 * 1024 * 1024;
|
|
21
|
+
const said = (r) => `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() || r.error?.message || '';
|
|
22
|
+
const sh = (file, args, input = '') => spawnSync(file, args, { encoding: 'utf8', input, maxBuffer: MAX });
|
|
23
|
+
const node = (script, args = [], input = '') => sh(process.execPath, [join(pkg, 'scripts', script), ...args], input);
|
|
24
|
+
const git = (...a) => sh('git', a);
|
|
25
|
+
const ok = (value) => ({ ok: value });
|
|
26
|
+
const bad = (source, r) => ({ unreadable: `${source} — ${typeof r === 'string' ? r : said(r)}` });
|
|
27
|
+
const read = (p) => { try { return readFileSync(p, 'utf8'); } catch { return null; } };
|
|
28
|
+
const section = (text, heading) => text.split(new RegExp(`^## ${heading}[ \\t]*$`, 'm'))[1]?.split(/^## /m)[0] ?? '';
|
|
29
|
+
const tag = (title) => (title.startsWith('[') && title.includes(']') ? title.slice(1, title.indexOf(']')) : '');
|
|
30
|
+
const lotOf = (title) => tag(title).match(/(?:^| · )lot #(\d+)$/)?.[1] ?? null;
|
|
31
|
+
const sentence = (title) => (title.startsWith('[') && title.includes('] ') ? title.slice(title.indexOf('] ') + 2) : title);
|
|
32
|
+
const days = (date) => Math.floor((Date.now() - Date.parse(date)) / 86400000);
|
|
33
|
+
const first = (body = '') => body.split('\n')[0];
|
|
34
|
+
// The screen, its sections in R-172's order whatever order they are read in.
|
|
35
|
+
const ORDER = ['Connection', 'Contract', 'Target', 'Audit', 'Issues', 'Merge requests', 'Resumption'];
|
|
36
|
+
const sections = new Map(ORDER.map((s) => [s, []]));
|
|
37
|
+
let current;
|
|
38
|
+
const head = (name) => { current = sections.get(name); };
|
|
39
|
+
// line(text, open) — a line of the current section; `open`, true or its own wording, also lists it
|
|
40
|
+
// under **Left open**: what is stale, to do or unreadable, which the result block copies.
|
|
41
|
+
const leftOpen = [];
|
|
42
|
+
const line = (text, open) => { current.push(`- ${text}`); if (open) leftOpen.push(open === true ? text.trim() : open); };
|
|
43
|
+
const print = (tail) => console.log(['### overview', ...ORDER.filter((s) => sections.get(s).length).flatMap((s) => ['', `**${s}**`, ...sections.get(s)]),
|
|
44
|
+
'', '**Left open**', ...(leftOpen.length ? leftOpen.map((l) => `- ${l}`) : ['- nothing']), '', tail].join('\n'));
|
|
45
|
+
const state = {};
|
|
46
|
+
|
|
47
|
+
// The contract (R-175), held by the one reader's check.
|
|
48
|
+
const parsed = readContract();
|
|
49
|
+
if (parsed === null) {
|
|
50
|
+
head('Contract');
|
|
51
|
+
line('no contract at `.claude/codelife.md`', true);
|
|
52
|
+
print('**Next**: `/setup` — a repository without a contract has nothing else to read (row 1)');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const contract = { schema: parsed.data.codelife, kind: parsed.data.kind ?? null, tracker: parsed.data.tracker?.kind ?? null, versionFile: parsed.data.release?.version_file ?? null };
|
|
56
|
+
const forge = contract.tracker === 'glab' || contract.tracker === 'gh' ? contract.tracker : 'none';
|
|
57
|
+
const pj = (() => { try { return JSON.parse(read('package.json')); } catch { return null; } })();
|
|
58
|
+
{
|
|
59
|
+
const { invalid, stale } = check(parsed);
|
|
60
|
+
const status = invalid.length ? 'invalid' : stale.length ? 'stale' : 'ok';
|
|
61
|
+
state.contract = ok({ status, lines: [...invalid, ...stale] });
|
|
62
|
+
head('Contract');
|
|
63
|
+
line(status === 'ok' ? `current — schema ${contract.schema}, the plugin reads ${SCHEMA}` : `${status}:`);
|
|
64
|
+
for (const l of [...invalid, ...stale]) line(` ${l}`, `contract: ${l}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `origin`, the forge's heads, read once: what the forge has, not this clone's memory of it.
|
|
68
|
+
const remote = git('remote', 'get-url', 'origin');
|
|
69
|
+
const where = remote.status === 0 ? remote.stdout.trim().match(/^[^@/]+@([^:/]+):(.+?)(?:\.git)?$|^[a-z+]+:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?$/) : null;
|
|
70
|
+
const host = where?.[1] ?? where?.[3], project = where?.[2] ?? where?.[4];
|
|
71
|
+
const ls = git('ls-remote', '--heads', 'origin');
|
|
72
|
+
const heads = ls.status === 0 ? new Map(ls.stdout.split('\n').filter(Boolean).map((l) => { const [sha, ref] = l.split('\t'); return [ref.replace('refs/heads/', ''), sha]; })) : null;
|
|
73
|
+
|
|
74
|
+
// The connection (R-174), first, and the project's merge settings.
|
|
75
|
+
head('Connection');
|
|
76
|
+
let connected = null, account = null;
|
|
77
|
+
const forgeCall = (args) => spawnSync(forge, args, { encoding: 'utf8', env: env(), input: '', maxBuffer: MAX });
|
|
78
|
+
if (forge === 'none') {
|
|
79
|
+
line('tracker.kind none — nothing to connect; issues are the files of issues/');
|
|
80
|
+
} else if (!host) {
|
|
81
|
+
connected = false;
|
|
82
|
+
line(`not connected → /setup: origin reads as no host — ${said(remote) || 'no origin'}`, true);
|
|
83
|
+
} else {
|
|
84
|
+
const au = auth(forge, host);
|
|
85
|
+
connected = au.connected;
|
|
86
|
+
if (!connected) line(`not connected to ${host} → /setup: ${au.line}`, true);
|
|
87
|
+
else {
|
|
88
|
+
const a = node('tracker/account.mjs');
|
|
89
|
+
account = a.status === 0 ? a.stdout.trim() : null;
|
|
90
|
+
const exp = au.expires;
|
|
91
|
+
const left = exp && exp !== 'never' ? -days(exp) : null;
|
|
92
|
+
line(`connected as ${account ?? `an account unreadable — ${said(a)}`} on ${host}${left !== null && left < 30 ? `, expires in ${left} days — /setup to rotate` : ''}`, account === null || (left !== null && left < 30));
|
|
93
|
+
const p = forgeCall(forge === 'glab' ? ['api', 'projects/:id'] : ['api', `repos/${project}`]);
|
|
94
|
+
if (p.status !== 0) line(`the project's settings: unreadable — ${said(p)}`, true);
|
|
95
|
+
else for (const x of unmet(forge, host, project, JSON.parse(p.stdout))) line(`the project ${x}`, true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
state.connected = ok(connected);
|
|
99
|
+
|
|
100
|
+
// The open issues, their fields and comments, by tracker/list.whole.mjs; an issue's state events
|
|
101
|
+
// by tracker/view.mjs, only where its marks need them (#1150).
|
|
102
|
+
let issues = null, issuesError = null;
|
|
103
|
+
if (forge !== 'none' && !connected) issuesError = 'the tracker — this machine is not connected';
|
|
104
|
+
else {
|
|
105
|
+
const l = node('tracker/list.whole.mjs');
|
|
106
|
+
if (l.status !== 0) issuesError = `the tracker's issues — ${said(l)}`;
|
|
107
|
+
else issues = JSON.parse(l.stdout).map((i) => ({ ...i, lot: lotOf(i.title), umbrella: i.labels.includes('lot') }));
|
|
108
|
+
}
|
|
109
|
+
const fromIssues = (fn) => (issues ? ok(fn(issues)) : { unreadable: issuesError });
|
|
110
|
+
|
|
111
|
+
// The target (R-176, R-273): the kind, the base items left to do, the toolchain, the codelife line.
|
|
112
|
+
head('Target');
|
|
113
|
+
{
|
|
114
|
+
const todo = [];
|
|
115
|
+
let unread = null;
|
|
116
|
+
for (const i of items({ forge, connected }).filter((x) => x.item !== 'connect')) {
|
|
117
|
+
if (i.state === 'not read') line(`${i.item}: not read — not connected`);
|
|
118
|
+
else if (i.why[0]?.startsWith('the labels could not be read')) unread = `the tracker's labels — ${i.why[0].replace(/^.*? — /, '')}`;
|
|
119
|
+
else if (i.state === 'to do') for (const w of i.why) todo.push(`${i.item} — ${w}`);
|
|
120
|
+
}
|
|
121
|
+
state.base = unread ? { unreadable: unread } : ok(todo);
|
|
122
|
+
line(`kind: ${contract.kind}${contract.kind === 'none' ? ' — base only' : ''}`);
|
|
123
|
+
if (unread) line(`base: unreadable — ${unread}`, true);
|
|
124
|
+
line(todo.length ? 'base items to do:' : unread ? 'base, the tree side: done' : 'base: done');
|
|
125
|
+
for (const t of todo) line(` ${t}`, `base: ${t}`);
|
|
126
|
+
// The toolchain: one line of information, never a Next — the port's own measure (K-20, R-19).
|
|
127
|
+
if (contract.kind === 'lib' || contract.kind === 'app') {
|
|
128
|
+
const t = portState({ forge, connected });
|
|
129
|
+
line(`toolchain: ${t.summary}`, /not read/.test(t.summary));
|
|
130
|
+
}
|
|
131
|
+
const c = node('overview/codelife-line.mjs');
|
|
132
|
+
const cl = c.stdout.trim() || `codelife: could not be read — ${said(c)}`;
|
|
133
|
+
line(cl, /could not be read|\/setup$/.test(cl));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The audit: the campaign table's last row against the cadence, a campaign under way, its findings.
|
|
137
|
+
head('Audit');
|
|
138
|
+
{
|
|
139
|
+
const { last: lastRow, next: when, due } = cadence();
|
|
140
|
+
if (!lastRow) line('no campaign yet');
|
|
141
|
+
else {
|
|
142
|
+
line(`last campaign ${lastRow[0]} (${lastRow[2]}) on ${lastRow[1]}`);
|
|
143
|
+
if (when) line(`next full campaign due ${when}`);
|
|
144
|
+
}
|
|
145
|
+
if (due) line(`a campaign is due: ${due}`, true);
|
|
146
|
+
state.due = ok(due);
|
|
147
|
+
const progress = (existsSync('docs/audit') ? readdirSync('docs/audit') : []).filter((d) => /^A\d+-/.test(d) && /IN PROGRESS/i.test(read(`docs/audit/${d}/00-index.md`) ?? ''));
|
|
148
|
+
state.campaign = ok(progress[0] ?? null);
|
|
149
|
+
for (const d of progress) line(`in progress: docs/audit/${d}/`);
|
|
150
|
+
const n = lastRow?.[0].match(/^A(\d+)/)?.[1];
|
|
151
|
+
if (n) {
|
|
152
|
+
const f = fromIssues((all) => all.filter((i) => !i.umbrella && tag(i.title).startsWith(`audit:A${n}-`)));
|
|
153
|
+
line('unreadable' in f ? `findings of A${n} still open: unreadable — ${f.unreadable}` : `findings of A${n} still open: ${f.ok.length}`, 'unreadable' in f);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The issues and the lots (R-172), by the roadmap's order; the roadmap's mark against docs/spec/.
|
|
158
|
+
head('Issues');
|
|
159
|
+
{
|
|
160
|
+
const r = roadmap();
|
|
161
|
+
const roadmaps = (r.roadmap.match(/#\d+/g) ?? []).map((x) => Number(x.slice(1)));
|
|
162
|
+
let order = [], left = [], outOfShape = null;
|
|
163
|
+
if (/^#\d+$/.test(r.roadmap) && issues) {
|
|
164
|
+
const body = issues.find((i) => i.n === roadmaps[0])?.body ?? '';
|
|
165
|
+
for (const l of section(body, 'The order').split('\n').filter((x) => /^\d+\. /.test(x))) {
|
|
166
|
+
const o = orderLine(l);
|
|
167
|
+
if (!o) { outOfShape = l; break; }
|
|
168
|
+
order.push(o);
|
|
169
|
+
}
|
|
170
|
+
left = [...section(body, 'Left alone').matchAll(/^- #(\d+)/gm)].map((m) => Number(m[1]));
|
|
171
|
+
}
|
|
172
|
+
// ahead: what docs/spec/ carries that the mark does not (row 6)
|
|
173
|
+
const last = git('log', '-1', '--format=%H', '--', 'docs/spec/').stdout.trim();
|
|
174
|
+
const since = (mark) => {
|
|
175
|
+
const log = git('log', '--format=%h %s', `${mark}..HEAD`, '--', 'docs/spec/').stdout.split('\n').filter(Boolean);
|
|
176
|
+
const spec = log.filter((l) => / docs\(spec\)/.test(l));
|
|
177
|
+
const shown = (spec.length ? spec : log);
|
|
178
|
+
return `${shown.length} commit${shown.length === 1 ? '' : 's'} on docs/spec/ since the mark — ${shown.slice(0, 3).join('; ')}${shown.length > 3 ? '; …' : ''}`;
|
|
179
|
+
};
|
|
180
|
+
if (r.roadmap.startsWith('could not tell')) state.ahead = bad('the roadmap', r.roadmap.replace(/^could not tell: /, ''));
|
|
181
|
+
else if (r.roadmap === 'none') state.ahead = ok(last ? 'no roadmap, and docs/spec/ has commits' : null);
|
|
182
|
+
else if (r.roadmap.startsWith('two')) state.ahead = ok(`two roadmaps, ${r.roadmap.slice(5)} — close the one you do not keep`);
|
|
183
|
+
else if (outOfShape) state.ahead = ok(`a line of the roadmap's order is out of shape: ${outOfShape}`);
|
|
184
|
+
else if (!r.sha) state.ahead = ok(`the roadmap's mark is ${r.mark}`);
|
|
185
|
+
else if (r.mark.endsWith('not contained')) state.ahead = ok(`the mark ${r.sha} is not in HEAD`);
|
|
186
|
+
else state.ahead = ok(last && git('merge-base', '--is-ancestor', last, r.sha).status !== 0 ? since(r.sha) : null);
|
|
187
|
+
line(`roadmap: ${r.roadmap}${r.sha ? `, mark ${r.sha.slice(0, 8)}` : r.mark !== 'none' ? `, mark ${r.mark}` : ''} — ${'unreadable' in state.ahead ? `unreadable: ${state.ahead.unreadable}` : state.ahead.ok ? `ahead: ${state.ahead.ok}` : 'current'}`, 'unreadable' in state.ahead || !!state.ahead.ok);
|
|
188
|
+
if (!issues) {
|
|
189
|
+
line(`issues: unreadable — ${issuesError}`, true);
|
|
190
|
+
for (const k of ['lots', 'orphans', 'plans', 'stops']) state[k] = { unreadable: issuesError };
|
|
191
|
+
} else {
|
|
192
|
+
const umbrellas = issues.filter((i) => i.umbrella && !roadmaps.includes(i.n));
|
|
193
|
+
const open = umbrellas.map((u) => String(u.n));
|
|
194
|
+
const orderRead = /^#\d+$/.test(r.roadmap) && !outOfShape;
|
|
195
|
+
const start = orderRead ? canStart(order, open).map(Number) : [];
|
|
196
|
+
const lacked = orderRead || r.roadmap === 'none' ? umbrellas.filter((u) => !order.some((o) => Number(o.lot) === u.n)).map((u) => u.n).sort((a, b) => a - b) : [];
|
|
197
|
+
state.lots = r.roadmap.startsWith('could not tell') ? { unreadable: `the roadmap — ${r.roadmap}` } : ok({ start, lacked });
|
|
198
|
+
const lotLine = (u, why) => {
|
|
199
|
+
const c = lotCount(u.body);
|
|
200
|
+
const branch = [...(heads?.keys() ?? []), ...git('for-each-ref', '--format=%(refname:short)', 'refs/heads/lot/').stdout.split('\n')].find((b) => b.startsWith(`lot/${u.n}-`));
|
|
201
|
+
line(`lot #${u.n} ${sentence(u.title)} — ${c.total - c.open} of ${c.total} done, ${c.effort}${branch ? ` — in progress on ${branch}` : ''} — ${why}`);
|
|
202
|
+
};
|
|
203
|
+
for (const o of order) { const u = umbrellas.find((x) => x.n === Number(o.lot)); if (u) lotLine(u, start.includes(u.n) ? 'can start' : `waits for ${o.after.filter((a) => open.includes(a)).map((a) => `#${a}`).join(', ')}`); }
|
|
204
|
+
for (const n of lacked) lotLine(umbrellas.find((u) => u.n === n), `not in the roadmap's order — can start, waiting for nothing`);
|
|
205
|
+
if (!orderRead && r.roadmap !== 'none') for (const u of umbrellas) lotLine(u, 'no order read — none can start');
|
|
206
|
+
if (!umbrellas.length) line('lots: none open');
|
|
207
|
+
const orphans = issues.filter((i) => !i.umbrella && !i.lot && !roadmaps.includes(i.n));
|
|
208
|
+
state.orphans = r.roadmap.startsWith('could not tell') ? { unreadable: `the roadmap — ${r.roadmap}` } : ok(orphans.map((o) => ({ n: o.n, left: left.includes(o.n) })));
|
|
209
|
+
const sev = (i) => i.labels.find((l) => l.startsWith('severity-'))?.slice(9) ?? 'no severity';
|
|
210
|
+
line(`orphans: ${orphans.length ? '' : 'none'}`);
|
|
211
|
+
for (const s of ['critical', 'high', 'mid', 'low', 'no severity']) for (const o of orphans.filter((i) => sev(i) === s)) line(` ${s} — #${o.n} ${o.title}${left.includes(o.n) ? ' — left alone by the last /roadmap' : ''}`);
|
|
212
|
+
// the resumption artefacts the issues carry (R-177): the last plan, closing, lot-closing or refused
|
|
213
|
+
const plans = [], stops = [];
|
|
214
|
+
let unread = null;
|
|
215
|
+
for (const i of issues) {
|
|
216
|
+
const marks = i.comments.map((c) => ({ ...c, kind: first(c.body).match(/^codelife: (plan|closing|lot-closing|refused) — /)?.[1] })).filter((c) => c.kind);
|
|
217
|
+
const lastMark = marks.at(-1);
|
|
218
|
+
if (!lastMark) continue;
|
|
219
|
+
const to = i.lot ?? i.n;
|
|
220
|
+
if (lastMark.kind === 'plan') { plans.push({ n: i.n, to, why: 'a plan with no closing comment' }); continue; }
|
|
221
|
+
if (lastMark.kind === 'refused') { stops.push({ n: i.n, to, why: `a refused held choice — ${lastMark.body.split('\n').slice(1).find((x) => x.trim())?.trim() ?? 'no reason written'}` }); continue; }
|
|
222
|
+
const v = node('tracker/view.mjs', [String(i.n)]);
|
|
223
|
+
if (v.status !== 0) { unread ??= `#${i.n}'s state events — ${said(v)}`; continue; }
|
|
224
|
+
const closed = JSON.parse(v.stdout).events.some((e) => e.state === 'closed' && e.date >= lastMark.date);
|
|
225
|
+
if (closed) stops.push({ n: i.n, to, why: `reopened after its ${lastMark.kind}` });
|
|
226
|
+
else {
|
|
227
|
+
const waits = marks.filter((m) => m.kind === 'plan').at(-1)?.body.match(/^Waits for: (C-\d+)/m)?.[1];
|
|
228
|
+
stops.push({ n: i.n, to, waits, why: `a STATUS, the run that posted it having stopped${waits ? ` — waits for ${waits}` : ''}${i.lot ? ` — lot #${i.lot}` : ''}` });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
state.plans = ok(plans);
|
|
232
|
+
state.stops = unread ? { unreadable: unread } : ok(stops);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// The merge requests (R-172): target, age, author, and whether a review pass was posted.
|
|
237
|
+
head('Merge requests');
|
|
238
|
+
if (forge === 'none') { line('not tracked'); state.mrs = ok([]); }
|
|
239
|
+
else if (!connected) { line('unreadable — this machine is not connected', 'merge requests: unreadable — not connected'); state.mrs = { unreadable: 'the merge requests — not connected' }; }
|
|
240
|
+
else {
|
|
241
|
+
const PASS = /^codelife: review — pass \d+ — [0-9a-f]{7,40}$/;
|
|
242
|
+
const l = forgeCall(forge === 'glab' ? ['api', 'projects/:id/merge_requests?state=opened&per_page=100', '--paginate', '--output', 'ndjson']
|
|
243
|
+
: ['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,baseRefName,createdAt,author']);
|
|
244
|
+
if (l.status !== 0) { line(`unreadable — ${said(l)}`, `merge requests: unreadable — ${said(l)}`); state.mrs = bad('the merge requests', l); }
|
|
245
|
+
else {
|
|
246
|
+
const mrs = (forge === 'glab' ? l.stdout.split('\n').filter(Boolean).map((x) => JSON.parse(x)).map((m) => ({ n: m.iid, title: m.title, target: m.target_branch, at: m.created_at, author: m.author?.username }))
|
|
247
|
+
: JSON.parse(l.stdout).map((m) => ({ n: m.number, title: m.title, target: m.baseRefName, at: m.createdAt, author: m.author?.login }))).sort((a, b) => a.n - b.n);
|
|
248
|
+
// In parallel, eight at a time, each call as forgeCall makes it: no token variable, stdin closed (#1098).
|
|
249
|
+
const notes = await pool(mrs, 8, (m) => new Promise((resolve) => execFile(forge, forge === 'glab'
|
|
250
|
+
? ['api', `projects/:id/merge_requests/${m.n}/notes?per_page=100`, '--paginate', '--output', 'ndjson'] : ['pr', 'view', String(m.n), '--json', 'comments'],
|
|
251
|
+
{ encoding: 'utf8', env: env(), maxBuffer: MAX }, (e, out, err) => resolve(e ? { error: (err || e.message).trim() } : { bodies: forge === 'glab' ? out.split('\n').filter(Boolean).map((x) => JSON.parse(x).body) : JSON.parse(out).comments.map((c) => c.body) }))).stdin.end());
|
|
252
|
+
const failed = notes.find((x) => x.error);
|
|
253
|
+
if (failed) { line(`unreadable — ${failed.error}`, `merge requests: unreadable — ${failed.error}`); state.mrs = { unreadable: `a merge request's comments — ${failed.error}` }; }
|
|
254
|
+
else {
|
|
255
|
+
state.mrs = ok(mrs.map((m, k) => ({ n: m.n, own: !!account && m.author === account, reviewed: notes[k].bodies.some((b) => PASS.test(first(b))) })));
|
|
256
|
+
if (!mrs.length) line('none open');
|
|
257
|
+
mrs.forEach((m, k) => line(`!${m.n} ${m.title} → ${m.target}, ${days(m.at)} days old, by ${m.author} — ${state.mrs.ok[k].own ? 'yours: it awaits your merge' : state.mrs.ok[k].reviewed ? 'reviewed' : 'no review'}`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The resumption artefacts (R-177): the issues' marks above, held commits, a campaign under way, a
|
|
263
|
+
// version without its tag, the notes, and the branches merged into develop still on origin.
|
|
264
|
+
head('Resumption');
|
|
265
|
+
{
|
|
266
|
+
for (const k of ['plans', 'stops']) if (state[k].ok) for (const s of state[k].ok) line(`#${s.n}: ${s.why}`);
|
|
267
|
+
if (state.plans.unreadable) line(`the issues' comments: unreadable — ${issuesError}`, true);
|
|
268
|
+
else if (state.stops.unreadable) line(`the stopped runs: unreadable — ${state.stops.unreadable}`, true);
|
|
269
|
+
const branch = git('branch', '--show-current').stdout.trim();
|
|
270
|
+
if (!branch) { state.held = ok(null); }
|
|
271
|
+
else {
|
|
272
|
+
const h = node('git/held.mjs');
|
|
273
|
+
const waits = h.stdout.split('\n').filter((x) => x.startsWith('waits\t'));
|
|
274
|
+
if (h.status !== 0) { state.held = bad('the held commits', h.stdout.replace(/^could not tell: /, '').trim() || said(h)); line(`held commits: unreadable — ${state.held.unreadable}`, true); }
|
|
275
|
+
else if (!waits.length) state.held = ok(null);
|
|
276
|
+
else {
|
|
277
|
+
const to = branch.match(/^(?:lot|fix|feature|hotfix)\/(\d+)-/)?.[1] ?? waits[0].split('\t')[2].match(/\d+/)?.[0] ?? '<n>';
|
|
278
|
+
state.held = ok({ to, why: `${waits.length} held commit${waits.length === 1 ? '' : 's'} wait on ${branch}` });
|
|
279
|
+
for (const w of waits) { const [, hash, cited, held] = w.split('\t'); line(`held: ${hash.slice(0, 8)} ${cited} — ${held}`); }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (state.campaign.ok) line(`campaign in progress: docs/audit/${state.campaign.ok}/`);
|
|
283
|
+
const vf = contract.versionFile, text = vf && read(vf);
|
|
284
|
+
let version = null, name = '';
|
|
285
|
+
if (text) { try { ({ version = null, name = '' } = JSON.parse(text)); } catch { version = text.match(/\d+\.\d+\.\d+/)?.[0] ?? null; } }
|
|
286
|
+
const untagged = version && version !== '0.0.0' && !git('tag', '-l', ...spellings(version, name)).stdout.trim() ? version : null;
|
|
287
|
+
state.untagged = ok(untagged);
|
|
288
|
+
if (untagged) line(`version ${untagged} in ${vf} carries no tag`);
|
|
289
|
+
const notes = (existsSync('docs/brainstorm') ? readdirSync('docs/brainstorm') : []).filter((f) => f.endsWith('.md') && f !== 'README.md' && statSync(`docs/brainstorm/${f}`).isFile()).sort();
|
|
290
|
+
state.notes = ok(notes.map((f) => f.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '')));
|
|
291
|
+
line(`notes in docs/brainstorm/: ${notes.length ? notes.join(', ') : 'none'}`);
|
|
292
|
+
if (!heads) line(`branches merged into develop still on origin: could not tell — ${said(ls)}`, true);
|
|
293
|
+
else if (!heads.has('develop')) line('branches merged into develop still on origin: none — origin has no develop');
|
|
294
|
+
else {
|
|
295
|
+
const base = heads.get('develop'), dflt = git('symbolic-ref', '--short', 'refs/remotes/origin/HEAD').stdout.trim().replace(/^origin\//, '');
|
|
296
|
+
const unknown = [], merged = [];
|
|
297
|
+
if (git('cat-file', '-e', `${base}^{commit}`).status !== 0) unknown.push('develop');
|
|
298
|
+
else for (const [name, sha] of heads) {
|
|
299
|
+
if (name === 'develop' || name === 'main' || name === dflt) continue;
|
|
300
|
+
if (git('cat-file', '-e', `${sha}^{commit}`).status !== 0) unknown.push(name);
|
|
301
|
+
else if (git('merge-base', '--is-ancestor', sha, base).status === 0) merged.push(name);
|
|
302
|
+
}
|
|
303
|
+
line(`branches merged into develop still on origin: ${unknown.length ? `could not tell — fetch first (${unknown.join(', ')})` : merged.length ? merged.join(', ') : 'none left'}`, unknown.length > 0 || merged.length > 0);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const n = next(state);
|
|
308
|
+
print(`**Next**: \`${n.next}\` — ${n.why} (row ${n.row})`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The package manager of the repository at <repo>: `packageManager` first, the lockfile when the
|
|
2
|
+
// field is absent. Imported by the hook scripts, which run a script or a binary through it; ''
|
|
3
|
+
// when neither names one.
|
|
4
|
+
import './node-floor.mjs'
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export function packageManager(repo) {
|
|
9
|
+
let pm = ''
|
|
10
|
+
try { pm = (JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8')).packageManager ?? '').replace(/@.*$/, '') } catch { /* no manifest */ }
|
|
11
|
+
const has = (f) => existsSync(join(repo, f))
|
|
12
|
+
if (pm === 'pnpm' || (!pm && has('pnpm-lock.yaml'))) return 'pnpm'
|
|
13
|
+
if (pm === 'yarn' || (!pm && has('yarn.lock'))) return 'yarn'
|
|
14
|
+
if (pm === 'npm' || (!pm && has('package-lock.json'))) return 'npm'
|
|
15
|
+
return ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The command that runs the `package.json` script <name>, or a binary of the repository.
|
|
19
|
+
export const script = (pm, name) => (pm === 'npm' ? ['npm', 'run', name] : [pm, name])
|
|
20
|
+
export const exec = (pm, ...cmd) => (pm === 'npm' ? ['npm', 'exec', '--', ...cmd] : [pm, 'exec', ...cmd])
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife port item <name> [--write | --check] [--test <file>] [--playground]` (K-2 —
|
|
3
|
+
// port-toolchain, R-158): one toolchain item. Without a flag it reads only: the files it would write
|
|
4
|
+
// from the plugin's templates, an existing file's difference included, and the checks it runs. With
|
|
5
|
+
// --write it writes them, then runs the checks; with --check, the checks alone, once the item's work
|
|
6
|
+
// by hand is done. It never commits: the `- paths:` line is what `codelife commit` takes.
|
|
7
|
+
import '../node-floor.mjs'
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { contract, entries } from '../contract.mjs'
|
|
11
|
+
import { runChecks } from '../verify/lib.mjs'
|
|
12
|
+
import { json, read, say, writer } from '../setup/lib.mjs'
|
|
13
|
+
import { at, characterisation, dir, hasTypes, installed, ITEMS, pack, standard, templates } from './lib.mjs'
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2)
|
|
16
|
+
const name = args[0], write = args.includes('--write'), only = args.includes('--check'), playground = args.includes('--playground')
|
|
17
|
+
const test = args.includes('--test') ? args[args.indexOf('--test') + 1] : undefined
|
|
18
|
+
const known = ['--write', '--check', '--playground', '--test']
|
|
19
|
+
if (!ITEMS.includes(name) || (write && only) || args.slice(1).some((a, i, all) => !known.includes(a) && all[i - 1] !== '--test')) {
|
|
20
|
+
console.error(`usage: codelife port item <${ITEMS.join('|')}> [--write | --check] [--test <file>] [--playground]`); process.exit(2)
|
|
21
|
+
}
|
|
22
|
+
console.log(`### port item ${name}`)
|
|
23
|
+
const kind = contract()?.data.kind
|
|
24
|
+
const { paths, failed, stop, done, put, scripts } = writer(write, test)
|
|
25
|
+
if (kind !== 'lib' && kind !== 'app') stop(`kind: ${kind ?? 'unknown'} — there is no toolchain to port`)
|
|
26
|
+
const pj = json(at('package.json')) ?? stop(`no ${at('package.json')}`)
|
|
27
|
+
const template = (p) => readFileSync(join(templates, p), 'utf8').replaceAll('<package-name>', pj.name)
|
|
28
|
+
const json2 = (text, obj) => `${JSON.stringify(obj, null, /^[ \t]+(?=")/m.exec(text)?.[0] ?? ' ')}\n`
|
|
29
|
+
|
|
30
|
+
// The commands, through pnpm in the package's directory; the characterisation tests through the
|
|
31
|
+
// runner the package has — vite-plus's once item d installed it.
|
|
32
|
+
const exec = (label, ...cmd) => ({ name: label, file: 'pnpm', args: [...(dir() === '.' ? [] : ['--dir', dir()]), 'exec', ...cmd], shell: process.platform === 'win32' })
|
|
33
|
+
const vp = Boolean(installed('vite-plus'))
|
|
34
|
+
const tests = (folder) => (vp ? exec(`vp test run ${folder ?? ''}`.trim(), 'vp', 'test', 'run', ...(folder ? [folder] : [])) : exec(`vitest run ${folder ?? ''}`.trim(), 'vitest', 'run', ...(folder ? [folder] : [])))
|
|
35
|
+
const floor = existsSync(at('test/characterisation')) ? [tests('test/characterisation')] : []
|
|
36
|
+
const checks = {
|
|
37
|
+
a1: () => [{ name: 'pnpm install --frozen-lockfile', file: 'pnpm', args: ['install', '--frozen-lockfile'], shell: process.platform === 'win32' }],
|
|
38
|
+
a2: () => [tests()],
|
|
39
|
+
b: () => floor,
|
|
40
|
+
c: () => floor,
|
|
41
|
+
d: () => [kind === 'lib' ? exec('vp pack', 'vp', 'pack') : exec('vp build', 'vp', 'build'), ...floor],
|
|
42
|
+
e: () => [vp ? exec('vp check', 'vp', 'check') : exec('tsc --noEmit', 'tsc', '--noEmit'), ...floor],
|
|
43
|
+
"e'": () => [exec('vp check', 'vp', 'check'), ...floor],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// What each item writes from the plugin; an item absent here is done by hand, then --check.
|
|
47
|
+
const writes = {
|
|
48
|
+
b() {
|
|
49
|
+
if (kind === 'app') return say('by hand', 'an application\'s characterisation — the build passing, a start-up smoke in Vitest browser mode, the pure modules snapshotted')
|
|
50
|
+
const types = hasTypes(pj)
|
|
51
|
+
for (const t of characterisation()) {
|
|
52
|
+
if (t === 'declarations.test.ts' && !types) continue
|
|
53
|
+
const text = template(`characterisation/${t}`)
|
|
54
|
+
put(at(`test/characterisation/${t}`), (types ? text.replace(/^\s*\/\/ <\/?declarations>\n/gm, '') : text.replace(/^\s*\/\/ <declarations>\n[\s\S]*?^\s*\/\/ <\/declarations>\n/gm, '')))
|
|
55
|
+
}
|
|
56
|
+
if (!types) say('left out', 'declarations.test.ts and the surface\'s declared-names case — the build ships no declaration file: file the issue that adds them from the first build that does')
|
|
57
|
+
},
|
|
58
|
+
d() {
|
|
59
|
+
if (kind === 'app') say('by hand', 'vite.config.ts carrying the framework\'s plugin — no template names a framework')
|
|
60
|
+
else put(at('vite.config.ts'), template('vite.config.ts'))
|
|
61
|
+
if (kind === 'lib' && playground) {
|
|
62
|
+
for (const f of ['index.html', 'main.ts']) put(at(`playground/${f}`), template(`playground/${f}`))
|
|
63
|
+
if (pj.scripts?.dev !== 'PLAYGROUND=1 vp dev') {
|
|
64
|
+
const text = read(at('package.json'))
|
|
65
|
+
say('dev', '`PLAYGROUND=1 vp dev` into the package\'s scripts')
|
|
66
|
+
put(at('package.json'), json2(text, { ...pj, scripts: { ...pj.scripts, dev: 'PLAYGROUND=1 vp dev' } }))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (kind === 'lib') {
|
|
70
|
+
const g = pack()
|
|
71
|
+
const declared = entries('generated').find((e) => e.path.some((p) => p.replace(/\/$/, '') === g.path.replace(/\/$/, '')))
|
|
72
|
+
if (!declared?.command.some((c) => c.includes('vp pack'))) say('contract', `generated[] ${declared ? 'replaces its entry for' : 'declares'} ${g.path}: \`{ path: ${g.path}, command: ${g.command}, when: [${g.when}] }\` — edit .claude/codelife.md, then \`codelife setup contract .claude/codelife.md\`, in this item's commit`)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
e() { put(at('tsconfig.json'), template('tsconfig.json')) },
|
|
76
|
+
f() {
|
|
77
|
+
const root = json('package.json'), want = standard(kind)
|
|
78
|
+
for (const [s, cmd] of Object.entries(want)) say(s, root?.scripts?.[s] === cmd ? `\`${cmd}\` — already` : `\`${root?.scripts?.[s] ?? 'nothing'}\` → \`${cmd}\``)
|
|
79
|
+
put('package.json', json2(read('package.json'), { ...root, scripts: { ...root?.scripts, ...want } }))
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!only) writes[name]?.()
|
|
84
|
+
if (!writes[name] && !only) say('by hand', 'this item has nothing to write from the plugin: do its work, then `codelife port item ' + name + ' --check`')
|
|
85
|
+
if (!write && !only) {
|
|
86
|
+
for (const c of name === 'f' ? Object.keys(standard(kind)) : checks[name]()) say('check', typeof c === 'string' ? c : c.name)
|
|
87
|
+
if (name !== 'f') say('check', 'test:all')
|
|
88
|
+
done()
|
|
89
|
+
}
|
|
90
|
+
if (name === 'f') {
|
|
91
|
+
if (!test) say('test:one', 'not run — give a test file with --test <file>')
|
|
92
|
+
if (scripts(Object.keys(standard(kind)).filter((s) => s !== 'test:one' || test))) failed.push('a script is red — this item does not fix the repository\'s tests')
|
|
93
|
+
} else if (runChecks(checks[name](), '- ') || scripts(['test:all'])) failed.push('a check is red — report it, and stop')
|
|
94
|
+
done()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// What `codelife port prep`, `codelife port item`, `codelife port version` and `codelife overview`
|
|
2
|
+
// share (K-2 — port-toolchain; K-20 — The target, R-19): the ported package, each toolchain item's
|
|
3
|
+
// state read from the tree, the `port:` issues and what they make of an item to do, and the version
|
|
4
|
+
// rule. Nothing is recorded: an item's state is what this file finds at each call.
|
|
5
|
+
import '../node-floor.mjs'
|
|
6
|
+
import { existsSync, readdirSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { basename, dirname, join } from 'node:path'
|
|
9
|
+
import { contract, entries } from '../contract.mjs'
|
|
10
|
+
import { lines as noneLines } from '../tracker/none.mjs'
|
|
11
|
+
import { git, json, pkg, read, said, section, sh } from '../setup/lib.mjs'
|
|
12
|
+
|
|
13
|
+
// The items in the order the port plays them, and the items each one's precondition names.
|
|
14
|
+
export const ITEMS = ['a1', 'a2', 'b', 'c', 'd', 'e', "e'", 'f']
|
|
15
|
+
const PRE = { a1: [], a2: ['a1'], c: ['b'], d: ['b'], e: ['b'], "e'": ['e'], f: ['d', 'e', "e'"] }
|
|
16
|
+
export const MARK = /generated by codelife/
|
|
17
|
+
const RUNNERS = ['vitest', 'vite-plus', 'jest', 'mocha', 'ava', 'jasmine']
|
|
18
|
+
const OLD_BUILD = /^(rollup|webpack|tsup|esbuild|parcel|vitest)\.config\.[cm]?[jt]s$|^vite\.config\.[cm]?js$/
|
|
19
|
+
const OLD_LINT = /^(\.eslintrc(\..+)?|eslint\.config\..+|\.prettierrc(\..+)?|prettier\.config\..+|biome\.jsonc?)$/
|
|
20
|
+
const OLD_LINT_DEPS = ['eslint', 'prettier', '@biomejs/biome']
|
|
21
|
+
|
|
22
|
+
// The ported package: the directory of the contract's version file — the root, or a workspace's package.
|
|
23
|
+
export const dir = () => dirname(contract()?.data.release?.version_file ?? 'package.json')
|
|
24
|
+
export const at = (p) => (dir() === '.' ? p : join(dir(), p))
|
|
25
|
+
export const deps = (pj) => ({ ...pj?.dependencies, ...pj?.devDependencies })
|
|
26
|
+
export const templates = join(pkg, 'scripts/target/templates')
|
|
27
|
+
export const characterisation = () => readdirSync(join(templates, 'characterisation')).sort()
|
|
28
|
+
|
|
29
|
+
// The target's five commands, verbatim from the kind's `## Standard scripts` table.
|
|
30
|
+
export const standard = (kind) => Object.fromEntries([...section(read(join(pkg, `scripts/target/target-${kind}.md`)) ?? '', 'Standard scripts')
|
|
31
|
+
.matchAll(/^\| `([^`]+)` \| `([^`]+)` \|/gm)].map((m) => [m[1], m[2]]))
|
|
32
|
+
// The generated[] entry a library's build implies, and the command that runs from the root.
|
|
33
|
+
export const pack = () => ({ path: `${at('dist')}/`, command: dir() === '.' ? 'pnpm exec vp pack' : `pnpm --dir ${dir()} exec vp pack`, when: [`${at('src')}/**`] })
|
|
34
|
+
export const hasTypes = (pj) => Boolean(pj?.types ?? pj?.typings) || /\.d\.m?ts"/.test(JSON.stringify(pj?.exports ?? ''))
|
|
35
|
+
|
|
36
|
+
// The version rule (R-162). view <args> — `pnpm view … --json` parsed, or { error }.
|
|
37
|
+
const view = (...a) => {
|
|
38
|
+
const r = sh('pnpm', ['view', ...a, '--json'])
|
|
39
|
+
try { return r.status === 0 ? JSON.parse(r.stdout || 'null') : { error: said(r) || `pnpm view ${a.join(' ')} failed` } } catch { return { error: `pnpm view ${a.join(' ')} printed no JSON` } }
|
|
40
|
+
}
|
|
41
|
+
const parts = (v) => v.split('.').map(Number)
|
|
42
|
+
const newer = (a, b) => { const [x, y] = [parts(a), parts(b)]; for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false }
|
|
43
|
+
// window — the repository's minimumReleaseAge, in minutes: pnpm-workspace.yaml's, else pnpm's 1440.
|
|
44
|
+
export const window = () => Number(read('pnpm-workspace.yaml')?.match(/^minimumReleaseAge:\s*(\d+)/m)?.[1] ?? 1440)
|
|
45
|
+
// releases <tool> — { all, past, error }: its stable versions with their dates, newest first, and those
|
|
46
|
+
// older than the window.
|
|
47
|
+
export const releases = (tool) => {
|
|
48
|
+
const t = view(tool, 'time')
|
|
49
|
+
if (!t || t.error) return { error: t?.error ?? `no release of ${tool}` }
|
|
50
|
+
const all = Object.entries(t).filter(([v]) => /^\d+\.\d+\.\d+$/.test(v)).sort(([a], [b]) => (newer(a, b) ? -1 : 1)).map(([v, d]) => ({ v, date: d.slice(0, 10), at: Date.parse(d) }))
|
|
51
|
+
const limit = Date.now() - window() * 60000
|
|
52
|
+
return { all, past: all.filter((r) => r.at < limit) }
|
|
53
|
+
}
|
|
54
|
+
// vitePlus — { v, date, pins } for the newest vite-plus past the window, its exact pins; or { error }.
|
|
55
|
+
export const vitePlus = () => {
|
|
56
|
+
const r = releases('vite-plus')
|
|
57
|
+
if (r.error || !r.past.length) return { error: r.error ?? 'every vite-plus release is inside the window' }
|
|
58
|
+
const m = view(`vite-plus@${r.past[0].v}`, 'dependencies', 'peerDependencies')
|
|
59
|
+
if (m?.error) return m
|
|
60
|
+
return { ...r.past[0], pins: { ...m?.dependencies, ...m?.peerDependencies } }
|
|
61
|
+
}
|
|
62
|
+
// carried <tool> — true when vite-plus carries it: Vitest and every @vitest/* package, Oxlint, Oxfmt, its Vite.
|
|
63
|
+
export const carried = (tool) => /^(vitest|@vitest\/.+|oxlint|oxfmt|vite)$/.test(tool)
|
|
64
|
+
// installed <name> — the version node_modules holds, in the package's directory or at the root.
|
|
65
|
+
export const installed = (name) => json(join(at('node_modules'), name, 'package.json'))?.version ?? json(join('node_modules', name, 'package.json'))?.version ?? null
|
|
66
|
+
// pin <tool> — the version the installed vite-plus pins, else the newest one past the window; or { error }.
|
|
67
|
+
export const pin = (tool) => {
|
|
68
|
+
const local = json(join(at('node_modules'), 'vite-plus/package.json')) ?? json('node_modules/vite-plus/package.json')
|
|
69
|
+
const from = local ? { v: local.version, pins: { ...local.dependencies, ...local.peerDependencies } } : vitePlus()
|
|
70
|
+
return from.error ? from : { version: from.pins[tool], by: from.v }
|
|
71
|
+
}
|
|
72
|
+
// peers <tool> — each lockfile package's peerDependencies range on the tool, `[package, range]`.
|
|
73
|
+
export const peers = (tool, lockfile = 'pnpm-lock.yaml') => {
|
|
74
|
+
const out = []
|
|
75
|
+
let section = '', name = '', inPeers = false
|
|
76
|
+
for (const line of (read(lockfile) ?? '').split('\n')) {
|
|
77
|
+
if (/^[a-z]/.test(line)) section = line.split(/\s+/)[0]
|
|
78
|
+
if (section !== 'packages:') continue
|
|
79
|
+
if (/^ {2}[^ ]/.test(line)) name = line.trim().replace(/:$/, '')
|
|
80
|
+
if (/^ {4}[^ ]/.test(line)) inPeers = line.trim().split(/\s+/)[0] === 'peerDependencies:'
|
|
81
|
+
if (inPeers && line.trim().split(/\s+/)[0] === `${tool}:`) out.push([name, line.replace(/^ +[^:]+: /, '').replace(/^(['"])(.*)\1$/, '$2')])
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
// admitted <tool> — the versions every peer range in the lockfile admits, as the registry resolves
|
|
86
|
+
// each range; or { error }. Without a range, every version.
|
|
87
|
+
export const admitted = (tool, ranges) => {
|
|
88
|
+
let set = null
|
|
89
|
+
for (const [, range] of ranges) {
|
|
90
|
+
const v = view(`${tool}@${range}`, 'version')
|
|
91
|
+
if (v?.error) return v
|
|
92
|
+
const these = new Set([v ?? []].flat())
|
|
93
|
+
set = set ? new Set([...set].filter((x) => these.has(x))) : these
|
|
94
|
+
}
|
|
95
|
+
return { set }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// measure <kind> — each item's reasons to do: [] done, null not applicable, undefined not read.
|
|
99
|
+
const measure = (kind) => {
|
|
100
|
+
const d = dir(), pj = json(at('package.json')), rootPj = json('package.json'), has = deps(pj)
|
|
101
|
+
const tracked = git('ls-files').stdout.split('\n').filter(Boolean)
|
|
102
|
+
const files = (where) => { try { return readdirSync(where) } catch { return [] } }
|
|
103
|
+
const configs = (re) => files(d).filter((f) => re.test(f))
|
|
104
|
+
const preset = (f) => read(at(f))?.includes('@ersbeth/codelife/presets')
|
|
105
|
+
const marked = tracked.filter((f) => f.includes('characterisation/') && MARK.test((read(f) ?? '').split('\n').slice(0, 3).join('\n')))
|
|
106
|
+
const vitest = installed('vitest') ?? has.vitest?.replace(/^[\^~=v]+/, '')
|
|
107
|
+
const why = {
|
|
108
|
+
a1: [
|
|
109
|
+
...(/^pnpm@/.test(rootPj?.packageManager ?? '') ? [] : [`packageManager is ${rootPj?.packageManager ?? 'unset'}, not pnpm`]),
|
|
110
|
+
...(existsSync('pnpm-lock.yaml') ? [] : ['no pnpm-lock.yaml']),
|
|
111
|
+
...['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'].filter(existsSync).map((f) => `${f} is still there`),
|
|
112
|
+
...(/^minimumReleaseAge:/m.test(read('pnpm-workspace.yaml') ?? '') ? [] : ['minimumReleaseAge is not set in pnpm-workspace.yaml']),
|
|
113
|
+
...[...new Set(['package.json', at('package.json')])].flatMap((f) => Object.entries(deps(json(f))).filter(([, s]) => /^(file|link):/.test(s)).map(([n, s]) => `${f}: ${n} is ${s}, not workspace:*`)),
|
|
114
|
+
...[...(read('.gitmodules') ?? '').matchAll(/^\s*path\s*=\s*(.+)$/gm)].map((m) => m[1].trim()).filter((p) => !p.startsWith('submodules/')).map((p) => `the submodule ${p} is not under submodules/`),
|
|
115
|
+
],
|
|
116
|
+
a2: has.vitest || has['vite-plus'] ? [] : [`no Vitest in ${at('package.json')}`],
|
|
117
|
+
b: kind === 'app' ? (marked.length ? [] : ['no characterisation test'])
|
|
118
|
+
: characterisation().filter((t) => t !== 'declarations.test.ts' && !marked.some((f) => basename(f) === t)).map((t) => `characterisation/${t} is missing`),
|
|
119
|
+
c: !has.vitest ? null : (() => {
|
|
120
|
+
const p = pin('vitest')
|
|
121
|
+
if (p.error) return undefined
|
|
122
|
+
return vitest === p.version ? [] : [`vitest is ${vitest}, vite-plus ${p.by} pins ${p.version}`]
|
|
123
|
+
})(),
|
|
124
|
+
d: [
|
|
125
|
+
...(has['vite-plus'] ? [] : [`no vite-plus in ${at('package.json')}`]),
|
|
126
|
+
...(kind === 'lib' && !preset('vite.config.ts') ? [`${at('vite.config.ts')} does not call the package's preset`] : kind === 'app' && !existsSync(at('vite.config.ts')) ? [`no ${at('vite.config.ts')}`] : []),
|
|
127
|
+
...configs(OLD_BUILD).map((f) => `${at(f)} is still there`),
|
|
128
|
+
...(kind === 'lib' && !entries('generated').some((g) => g.path.some((p) => p.replace(/\/$/, '') === at('dist')) && g.command.some((c) => c.includes('vp pack')))
|
|
129
|
+
? [`generated[] does not declare ${pack().path} by vp pack`] : []),
|
|
130
|
+
],
|
|
131
|
+
e: [
|
|
132
|
+
...(read(at('tsconfig.json'))?.includes('@ersbeth/codelife/presets/tsconfig.json') ? [] : [`${at('tsconfig.json')} does not extend the package's preset`]),
|
|
133
|
+
...(() => { const js = tracked.filter((f) => f.startsWith(`${at('src')}/`) && /\.[cm]?jsx?$/.test(f)); return js.length ? [`${js.length} JavaScript files under ${at('src')}/`] : [] })(),
|
|
134
|
+
],
|
|
135
|
+
"e'": [...(has['vite-plus'] ? [] : [`no vite-plus in ${at('package.json')} to run Oxlint and Oxfmt`]), ...configs(OLD_LINT).map((f) => `${at(f)} is still there`), ...OLD_LINT_DEPS.filter((n) => has[n]).map((n) => `${n} is still a dependency of ${at('package.json')}`)],
|
|
136
|
+
f: Object.entries(standard(kind)).filter(([s, cmd]) => rootPj?.scripts?.[s] !== cmd).map(([s, cmd]) => `${s} runs ${rootPj?.scripts?.[s] ? `\`${rootPj.scripts[s]}\`` : 'nothing'}, the target's is \`${cmd}\``),
|
|
137
|
+
}
|
|
138
|
+
return { why, runner: RUNNERS.some((r) => has[r]), traced: marked.length > 0 || preset('vite.config.ts') || preset('tsconfig.json') }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// portIssues <forge> <connected> — the `port:<item>` issues, open or closed: { byItem } or { error }.
|
|
142
|
+
const portIssues = (forge, connected) => {
|
|
143
|
+
if (forge !== 'none' && !connected) return { error: 'this machine is not connected to the host' }
|
|
144
|
+
let listing
|
|
145
|
+
if (forge === 'none') listing = existsSync('issues') ? noneLines(true) : []
|
|
146
|
+
else {
|
|
147
|
+
const term = join(tmpdir(), `codelife-port-${process.pid}`)
|
|
148
|
+
writeFileSync(term, 'port:')
|
|
149
|
+
const r = sh(process.execPath, [join(pkg, 'scripts/tracker/search.mjs'), term, '--all'])
|
|
150
|
+
if (r.status !== 0) return { error: said(r) }
|
|
151
|
+
listing = r.stdout.split('\n')
|
|
152
|
+
}
|
|
153
|
+
const byItem = {}
|
|
154
|
+
for (const l of listing) {
|
|
155
|
+
const m = l.match(/^#?(\d+)\t\[([^\]]*)\]/)
|
|
156
|
+
const item = m?.[2].match(/(?:^|\s)port:(a1|a2|b|c|d|e'|e|f)(?=$|\s)/)?.[1]
|
|
157
|
+
if (!item || byItem[item]) continue
|
|
158
|
+
const v = sh(process.execPath, [join(pkg, 'scripts/tracker/view.mjs'), m[1], '--bare'])
|
|
159
|
+
if (v.status !== 0) return { error: `#${m[1]} — ${said(v)}` }
|
|
160
|
+
const j = JSON.parse(v.stdout)
|
|
161
|
+
byItem[item] = { n: Number(m[1]), open: !/^closed$/i.test(j.state ?? ''), wontfix: (j.labels ?? []).includes('wontfix') }
|
|
162
|
+
}
|
|
163
|
+
return { byItem }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// state <{ forge, connected }> — { items, summary, next }: each item { item, state, why, pre, issue },
|
|
167
|
+
// its state done, not applicable, not read, to do, deferred, refused or blocked; the toolchain's
|
|
168
|
+
// summary line; the first item to propose, or null.
|
|
169
|
+
export const state = ({ forge, connected }) => {
|
|
170
|
+
const kind = contract()?.data.kind
|
|
171
|
+
if (kind !== 'lib' && kind !== 'app') return { items: [], summary: 'not applicable', next: null }
|
|
172
|
+
const { why, runner, traced } = measure(kind)
|
|
173
|
+
const issues = portIssues(forge, connected)
|
|
174
|
+
const items = []
|
|
175
|
+
for (const item of ITEMS) {
|
|
176
|
+
const w = why[item], pre = item === 'b' ? (runner ? [] : ['a2']) : PRE[item]
|
|
177
|
+
const issue = issues.byItem?.[item]
|
|
178
|
+
let s = w === null ? 'not applicable' : w === undefined ? 'not read' : w.length ? 'to do' : 'done'
|
|
179
|
+
const by = pre.filter((p) => ['deferred', 'refused', 'blocked'].includes(items.find((x) => x.item === p).state))
|
|
180
|
+
if (s === 'to do' && issue?.open) s = 'deferred'
|
|
181
|
+
else if (s === 'to do' && issue?.wontfix) s = 'refused'
|
|
182
|
+
else if (s === 'to do' && by.length) s = 'blocked'
|
|
183
|
+
items.push({ item, state: s, why: s === 'blocked' ? [`by ${by.join(', ')}`] : w ?? (s === 'not read' ? ['could not tell — the registry did not answer'] : []), pre, issue })
|
|
184
|
+
}
|
|
185
|
+
const named = (s) => items.filter((i) => i.state === s).map((i) => (i.issue && s !== 'blocked' ? `${i.item} (#${i.issue.n})` : i.item))
|
|
186
|
+
const left = ['to do', 'deferred', 'refused', 'blocked', 'not read'].filter((s) => named(s).length).map((s) => `${s} ${named(s).join(', ')}`)
|
|
187
|
+
const summary = !traced && !Object.keys(issues.byItem ?? {}).length
|
|
188
|
+
? `not ported${issues.error ? ` — the port: issues not read, ${issues.error}` : ''}`
|
|
189
|
+
: !left.length ? 'current' : `ported — ${left.join(' · ')}${issues.error ? ` · the port: issues not read, ${issues.error}` : ''}`
|
|
190
|
+
const ready = (i) => i.pre.every((p) => ['done', 'not applicable'].includes(items.find((x) => x.item === p).state))
|
|
191
|
+
return { items, summary, next: items.find((i) => ['to do', 'deferred'].includes(i.state) && ready(i))?.item ?? null }
|
|
192
|
+
}
|
|
193
|
+
// shown <state> — the measure as `codelife port prep` prints it and CONF reads it: [key, value] per item, then the summary.
|
|
194
|
+
export const shown = (s) => [...s.items.map((i) => [`item ${i.item}`, `${i.state}${i.issue && i.state !== 'done' ? ` #${i.issue.n}` : ''}${i.why.length && i.state !== 'done' ? ` — ${i.why.join('; ')}` : ''}`]), ['toolchain', s.summary]]
|