@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,53 @@
|
|
|
1
|
+
// /roadmap's last write (R-263): the roadmap's body from its template — the mark at the HEAD the run
|
|
2
|
+
// folder read, the orphans left alone, the open lots in the confirmed order with what each waits for
|
|
3
|
+
// (R-264) — every line held to the shape every reader parses before anything is sent. Rewrites the
|
|
4
|
+
// roadmap the folder found, or creates the first one. `codelife roadmap write <run folder> <order
|
|
5
|
+
// file> [<left alone file>]`: one `#<n> <sentence>[ — after #<a>, #<b>]` per line, in order.
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { prep, said } from '../prepare-lib.mjs';
|
|
11
|
+
import { orderLine } from '../read-roadmap/lib.mjs';
|
|
12
|
+
|
|
13
|
+
const [dir, orderFile, leftFile] = process.argv.slice(2);
|
|
14
|
+
if (!dir || !orderFile || !existsSync(join(dir, 'roadmap.json'))) {
|
|
15
|
+
console.error('usage: codelife roadmap write <run folder> <order file> [<left alone file>]');
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
const { say, stop, done, must } = prep('write');
|
|
19
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
20
|
+
const map = JSON.parse(readFileSync(join(dir, 'roadmap.json'), 'utf8'));
|
|
21
|
+
const issues = JSON.parse(readFileSync(join(dir, 'issues.json'), 'utf8'));
|
|
22
|
+
const lines = (f) => (f ? readFileSync(f, 'utf8') : '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
23
|
+
|
|
24
|
+
const order = lines(orderFile).map((l, i) => `${i + 1}. ${l.replace(/^\d+\. /, '')}`);
|
|
25
|
+
const lots = [];
|
|
26
|
+
for (const l of order) {
|
|
27
|
+
const o = orderLine(l);
|
|
28
|
+
if (!o) stop(`a line out of shape, nothing sent: ${l}`);
|
|
29
|
+
if (lots.includes(o.lot)) stop(`#${o.lot} is listed twice, nothing sent`);
|
|
30
|
+
lots.push(o.lot);
|
|
31
|
+
}
|
|
32
|
+
for (const l of lines(leftFile)) if (!/^#\d+ \S/.test(l)) stop(`a left-alone line out of shape, nothing sent: ${l}`);
|
|
33
|
+
// The orphans an argument kept out of this run's scope, left alone before, stay left alone.
|
|
34
|
+
const kept = lines(join(dir, 'kept.txt')).map((n) => `#${n} ${issues.find((i) => i.n === n)?.title ?? ''}`.trim());
|
|
35
|
+
const left = [...lines(leftFile), ...kept.filter((k) => !lines(leftFile).some((l) => l.split(' ')[0] === k.split(' ')[0]))];
|
|
36
|
+
|
|
37
|
+
const template = readFileSync(join(pkg, 'scripts/roadmap/roadmap.md'), 'utf8').replace(/^<!--[\s\S]*?-->\n+/, '');
|
|
38
|
+
const body = template
|
|
39
|
+
.replace('`<commit>`', `\`${map.head.slice(0, 8)}\``)
|
|
40
|
+
.replace(/^- #<n> <the issue's sentence>$/m, left.length ? left.map((l) => `- ${l}`).join('\n') : '*None.*')
|
|
41
|
+
.replace(/^1\. #<n> .*\n2\. #<n> .*$/m, order.length ? order.join('\n') : '*None.*');
|
|
42
|
+
const file = join(dir, 'roadmap.md');
|
|
43
|
+
if (map.n) {
|
|
44
|
+
writeFileSync(file, body);
|
|
45
|
+
must('tracker', 'issue.update', [map.n, file]);
|
|
46
|
+
say(`roadmap: #${map.n}, rewritten — mark ${map.head.slice(0, 8)}, ${order.length} lots, ${left.length} left alone`);
|
|
47
|
+
} else {
|
|
48
|
+
writeFileSync(file, `# What remains to implement, in order\n\n${body}`);
|
|
49
|
+
const r = spawnSync(process.execPath, [join(pkg, 'scripts/tracker/create.mjs'), '-', '-', 'roadmap', file], { encoding: 'utf8' });
|
|
50
|
+
if (r.status !== 0) stop(`the roadmap was not created — ${said(r)}`);
|
|
51
|
+
say(`roadmap: ${r.stdout.trim().replace(/^created /, '')}, created — mark ${map.head.slice(0, 8)}, ${order.length} lots, ${left.length} left alone`);
|
|
52
|
+
}
|
|
53
|
+
done();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife search <terms file>` (K-7 — new-issue, R-179): what is already known about a subject,
|
|
3
|
+
// before anything is filed or designed — the open issues, what docs/spec/ decided — a rule's heading,
|
|
4
|
+
// a refusal line, matched on its text and not its path —, then the notes of docs/brainstorm/ still
|
|
5
|
+
// pending, by their title and their Open items; each term searched alone
|
|
6
|
+
// as a whole word, regardless of case, each pass ranked by how many terms a line matched, the most
|
|
7
|
+
// first. One term per line of the file, never typed into a command: a term can quote someone else's
|
|
8
|
+
// text. Run from the repository's root; prints one `### search` block.
|
|
9
|
+
import './node-floor.mjs'
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
11
|
+
import { decided, unfinished } from './brainstorm/lib.mjs'
|
|
12
|
+
import { kind, matching, rank, word } from './tracker/lib.mjs'
|
|
13
|
+
import { matching as filed } from './tracker/none.mjs'
|
|
14
|
+
|
|
15
|
+
const SHOWN = 10
|
|
16
|
+
const file = process.argv[2]
|
|
17
|
+
if (!file) { console.error('usage: codelife search <terms file>'); process.exit(2) }
|
|
18
|
+
const terms = [...new Set(readFileSync(file, 'utf8').split('\n').map((t) => t.trim()).filter(Boolean))]
|
|
19
|
+
if (!terms.length) { console.error(`${file}: no term, one per line`); process.exit(2) }
|
|
20
|
+
|
|
21
|
+
const none = kind() === 'none'
|
|
22
|
+
const decisions = existsSync('docs/spec') ? decided(['docs/spec']) : []
|
|
23
|
+
const open = rank(terms.flatMap((t) => none ? filed(t) : matching(t)))
|
|
24
|
+
const text = (lines) => rank(terms.flatMap((t) => { const holds = word(t); return lines.filter((l) => holds(l.slice(l.indexOf(': ') + 2))) }))
|
|
25
|
+
const spec = text(decisions)
|
|
26
|
+
const notes = text(existsSync('docs/brainstorm') ? unfinished(['docs/brainstorm']) : [])
|
|
27
|
+
const pass = (name, lines) => [`- ${name}: ${lines.length} matched${lines.length > SHOWN ? `, the first ${SHOWN} shown` : ''}`, ...lines.slice(0, SHOWN)]
|
|
28
|
+
console.log(['### search', `- terms: ${terms.map((t) => `\`${t}\``).join(', ')}`,
|
|
29
|
+
...pass('open issues', open), ...pass('decided', spec), ...pass('unfinished notes', notes)].join('\n'))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife setup contract <file>` (K-1 — setup, R-149; K-19 R-13): the contract you confirmed,
|
|
3
|
+
// held to the schema by the one reader's check, then written to .claude/codelife.md — the file
|
|
4
|
+
// itself when it is the one you edited — and .claude/settings.json settled for a former marketplace
|
|
5
|
+
// copy. An invalid draft writes nothing: its lines, and exit 1.
|
|
6
|
+
import '../node-floor.mjs'
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { resolve } from 'node:path'
|
|
9
|
+
import { check, FILE, parse } from '../contract.mjs'
|
|
10
|
+
import { mergeSettings } from '../former-form.mjs'
|
|
11
|
+
import { say } from './lib.mjs'
|
|
12
|
+
|
|
13
|
+
const [file] = process.argv.slice(2)
|
|
14
|
+
if (!file || process.argv.length > 3) { console.error('usage: codelife setup contract <file>'); process.exit(2) }
|
|
15
|
+
const text = readFileSync(file, 'utf8')
|
|
16
|
+
const { invalid, stale } = check({ ...parse(text) })
|
|
17
|
+
for (const l of invalid) say('invalid', l)
|
|
18
|
+
for (const l of stale) say('stale', l)
|
|
19
|
+
if (invalid.length) process.exit(1)
|
|
20
|
+
if (resolve(file) !== resolve(FILE)) { mkdirSync('.claude', { recursive: true }); writeFileSync(FILE, text) }
|
|
21
|
+
const paths = [FILE]
|
|
22
|
+
const s = mergeSettings('.')
|
|
23
|
+
if (s.status === 'migrated') paths.push('.claude/settings.json')
|
|
24
|
+
say('settings', { migrated: 'the former marketplace lines replaced by "codelife@codelife": false', 'provides-marketplace': 'left: this repository provides the marketplace itself — a session here runs `claude --plugin-dir <its source>`', 'not-round-trip': s.note }[s.status] ?? 'nothing to change')
|
|
25
|
+
say('paths', paths.join(' '))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife setup item <name> [--write] [--test <file>]` (K-1 — setup, R-151): one base item. Without
|
|
3
|
+
// --write it reads only and prints what it would do, an existing file's difference included; with it,
|
|
4
|
+
// it does that, then runs the item's own check. It never commits: the `- paths:` line is what
|
|
5
|
+
// `codelife commit` takes, one item to one commit. A `- stop:` line exits 1, with nothing written.
|
|
6
|
+
import '../node-floor.mjs'
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { basename, join } from 'node:path'
|
|
9
|
+
import { contract } from '../contract.mjs'
|
|
10
|
+
import { detect, migrate } from '../former-form.mjs'
|
|
11
|
+
import { packageManager } from '../package-manager.mjs'
|
|
12
|
+
import { blank, develop, folderTemplate, forgeCall, git, hookTemplate, installed, isPackage, json, LABELS, labels, MARKER, names, origin, read, said, say, sh, templates, writer } from './lib.mjs'
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2)
|
|
15
|
+
const name = args[0], write = args.includes('--write'), test = args[args.indexOf('--test') + 1]
|
|
16
|
+
const ITEMS = ['labels', 'templates', 'scripts', 'hooks', 'folders', 'changelog', 'develop']
|
|
17
|
+
if (!ITEMS.includes(name) || args.slice(1).some((a, i, all) => a !== '--write' && a !== '--test' && all[i - 1] !== '--test')) {
|
|
18
|
+
console.error(`usage: codelife setup item <${ITEMS.join('|')}> [--write] [--test <file>]`); process.exit(2)
|
|
19
|
+
}
|
|
20
|
+
const forge = contract()?.data.tracker?.kind ?? 'none'
|
|
21
|
+
const { paths, failed, stop, done, put, scripts } = writer(write, test)
|
|
22
|
+
|
|
23
|
+
const items = {
|
|
24
|
+
labels() {
|
|
25
|
+
if (forge === 'none') stop('tracker.kind none — the labels live in the issues/ files, nothing to create')
|
|
26
|
+
const l = labels(forge)
|
|
27
|
+
if (l.error) stop(`the labels could not be read — ${l.error}`)
|
|
28
|
+
const missing = LABELS().filter((x) => !l.names.includes(x))
|
|
29
|
+
say('missing', missing.join(', ') || 'none')
|
|
30
|
+
const others = l.names.filter((x) => !LABELS().includes(x))
|
|
31
|
+
if (others.length) say('kept', `the project's own labels, left as they are: ${others.join(', ')}`)
|
|
32
|
+
if (!write) return
|
|
33
|
+
for (const x of missing) {
|
|
34
|
+
const r = forgeCall(forge, forge === 'glab' ? ['label', 'create', '--name', x] : ['label', 'create', x])
|
|
35
|
+
say(r.status === 0 ? 'created' : 'by hand', r.status === 0 ? x : `${x} — ${said(r)}`)
|
|
36
|
+
}
|
|
37
|
+
const after = labels(forge).names ?? []
|
|
38
|
+
for (const x of LABELS().filter((y) => !after.includes(y))) failed.push(`${x} is not on the tracker`)
|
|
39
|
+
},
|
|
40
|
+
templates() {
|
|
41
|
+
if (forge === 'none') stop('tracker.kind none — an issues/ file takes the same shape when it is created')
|
|
42
|
+
for (const { at, want } of templates(forge)) put(at, want)
|
|
43
|
+
},
|
|
44
|
+
folders() {
|
|
45
|
+
for (const p of names('Folders')) {
|
|
46
|
+
if (existsSync(p)) continue
|
|
47
|
+
if (p.endsWith('/')) { say('create', `${p} — kept by a .gitkeep`); if (write) { mkdirSync(p, { recursive: true }); writeFileSync(join(p, '.gitkeep'), ''); paths.push(join(p, '.gitkeep')) } }
|
|
48
|
+
else put(p, p.startsWith('docs/spec/') ? blank(readFileSync(folderTemplate(p), 'utf8'), json('package.json')?.name ?? basename(process.cwd())) : readFileSync(folderTemplate(p), 'utf8'))
|
|
49
|
+
}
|
|
50
|
+
if (write) for (const p of names('Folders').filter((x) => !existsSync(x))) failed.push(`${p} is missing`)
|
|
51
|
+
},
|
|
52
|
+
scripts() {
|
|
53
|
+
const pj = json('package.json')
|
|
54
|
+
if (!pj) stop('no package.json — write it with the five scripts you confirmed, then run this again')
|
|
55
|
+
const five = names('Scripts')
|
|
56
|
+
for (const s of five) say(s, pj.scripts?.[s] ? `\`${pj.scripts[s]}\`${/^echo ['"]?nothing to run/.test(pj.scripts[s]) ? ' — nothing to run: say it, and look once for a real check at this tier' : ''}` : 'missing')
|
|
57
|
+
const missing = five.filter((s) => !pj.scripts?.[s])
|
|
58
|
+
const node = process.versions.node
|
|
59
|
+
const lock = existsSync('pnpm-lock.yaml') && !pj.packageManager ? `pnpm@${sh('pnpm', ['--version']).stdout.trim()}` : null
|
|
60
|
+
const ignore = read('.gitignore') ?? ''
|
|
61
|
+
const ignored = /^\/?node_modules\/?$/m.test(ignore)
|
|
62
|
+
if (!pj.engines?.node) say('engines', `node >=${node}, the runtime this run is on`)
|
|
63
|
+
if (lock) say('packageManager', lock)
|
|
64
|
+
if (!ignored) say('.gitignore', 'node_modules/ — the prepare line makes an install happen here')
|
|
65
|
+
if (!write) return
|
|
66
|
+
if (missing.length) stop(`${missing.join(', ')} missing from package.json — write the mapping you confirmed first`)
|
|
67
|
+
if (!pj.engines?.node || lock) {
|
|
68
|
+
const text = readFileSync('package.json', 'utf8'), indent = /^[ \t]+(?=")/m.exec(text)?.[0] ?? ' '
|
|
69
|
+
if (!pj.engines?.node) pj.engines = { ...pj.engines, node: `>=${node}` }
|
|
70
|
+
if (lock) pj.packageManager = lock
|
|
71
|
+
writeFileSync('package.json', `${JSON.stringify(pj, null, indent)}\n`); paths.push('package.json')
|
|
72
|
+
}
|
|
73
|
+
if (!ignored) { writeFileSync('.gitignore', `${ignore}${ignore && !ignore.endsWith('\n') ? '\n' : ''}node_modules/\n`); paths.push('.gitignore') }
|
|
74
|
+
if (!test) say('test:one', 'not run — give a test file with --test <file>')
|
|
75
|
+
if (scripts([...five.filter((s) => s !== 'test:one'), ...(test ? ['test:one'] : [])])) failed.push('a script is red: the repository is red before codelife arrives — /setup does not fix its tests')
|
|
76
|
+
},
|
|
77
|
+
hooks() {
|
|
78
|
+
if (!installed()) stop(`the package @ersbeth/codelife is not installed — install it on your yes, then run this again`)
|
|
79
|
+
if (isPackage()) stop('this repository is the package itself: its hooks call scripts/ directly and stay its own')
|
|
80
|
+
const former = detect('.')
|
|
81
|
+
const own = (h) => (isPackage() ? `scripts/${h}.mjs` : `node_modules/@ersbeth/codelife/scripts/${h}.mjs`)
|
|
82
|
+
if (former.hooks.length || former.postinstall || former.marketplace) say('former form', `${[...former.hooks, ...(former.postinstall ? ['postinstall'] : []), ...(former.marketplace ? ['the marketplace'] : [])].join(', ')} — removed for the package's, the contract's guards[] rewritten`)
|
|
83
|
+
const pm = packageManager('.') || 'npm'
|
|
84
|
+
const pj = json('package.json') ?? stop('no package.json — the scripts item comes first')
|
|
85
|
+
const prepare = `node ${own('install')}`
|
|
86
|
+
if (!pj.scripts?.prepare?.includes(own('install'))) say('prepare', `\`${prepare}\`${pj.scripts?.prepare ? `, after the prepare already there` : ''}`)
|
|
87
|
+
const setup = contract()?.sections.get('Setup') ?? ''
|
|
88
|
+
if (!setup.includes('core.hooksPath')) say('## Setup', 'the line that wires a new clone, written into the contract')
|
|
89
|
+
if (write) {
|
|
90
|
+
// A hook over a red tier refuses every commit, its own included (C-830): both tiers first.
|
|
91
|
+
if (scripts(['verify:commit', 'verify:push'])) stop('a tier is red — nothing written: the repository is red before its hooks')
|
|
92
|
+
if (former.hooks.length || former.postinstall || former.marketplace) {
|
|
93
|
+
const r = migrate('.', 'hooks')
|
|
94
|
+
for (const [k, v] of Object.entries(r)) for (const x of v) say(k, x)
|
|
95
|
+
paths.push(...r.removed, ...r.rewritten)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const h of names('Hooks')) put(`.githooks/${h}`, hookTemplate(h), 0o755)
|
|
99
|
+
if (!write) return
|
|
100
|
+
const fresh = json('package.json')
|
|
101
|
+
if (!fresh.scripts?.prepare?.includes(own('install'))) {
|
|
102
|
+
const text = readFileSync('package.json', 'utf8'), indent = /^[ \t]+(?=")/m.exec(text)?.[0] ?? ' '
|
|
103
|
+
fresh.scripts = { ...fresh.scripts, prepare: fresh.scripts?.prepare ? `${fresh.scripts.prepare} && ${prepare}` : prepare }
|
|
104
|
+
writeFileSync('package.json', `${JSON.stringify(fresh, null, indent)}\n`)
|
|
105
|
+
}
|
|
106
|
+
if (!paths.includes('package.json')) paths.push('package.json')
|
|
107
|
+
if (!setup.includes('core.hooksPath')) {
|
|
108
|
+
const text = read('.claude/codelife.md')
|
|
109
|
+
const line = `Wire a new clone once: \`${pm === 'npm' ? 'npm run prepare' : `${pm} prepare`}\` — an install runs it too. It sets the local keys \`core.hooksPath\` (the hooks of \`.githooks/\`), \`fetch.prune\` and an ssh keepalive; until it runs, no hook holds a commit or a push here.`
|
|
110
|
+
writeFileSync('.claude/codelife.md', /^## Setup[ \t]*$/m.test(text) ? text.replace(/^## Setup[ \t]*\n/m, `## Setup\n\n${line}\n`) : `${text.replace(/\n*$/, '\n')}\n## Setup\n\n${line}\n`)
|
|
111
|
+
paths.push('.claude/codelife.md')
|
|
112
|
+
}
|
|
113
|
+
const install = sh(process.execPath, [own('install')], { stdio: 'inherit' })
|
|
114
|
+
if (install.status !== 0) failed.push(`${prepare} failed`)
|
|
115
|
+
// The checks (R-151): the keys, the refusal of a bad message, the form script, the link.
|
|
116
|
+
if (git('config', 'core.hooksPath').stdout.trim() !== '.githooks') failed.push('core.hooksPath is not .githooks')
|
|
117
|
+
if (git('config', 'fetch.prune').stdout.trim() !== 'true') failed.push('fetch.prune is not true')
|
|
118
|
+
const wip = git('commit', '--allow-empty', '-m', 'wip')
|
|
119
|
+
if (wip.status === 0) { git('reset', '--soft', 'HEAD~1'); failed.push('`git commit -m wip` was not refused — the commit-msg hook does not hold') }
|
|
120
|
+
else say('refused', '`git commit --allow-empty -m wip`, as it should be')
|
|
121
|
+
if (sh(process.execPath, [own('spec-form')]).status !== 2) failed.push(`node ${own('spec-form')} does not print its usage`)
|
|
122
|
+
if (!isPackage() && !existsSync('.claude/skills/codelife')) failed.push('.claude/skills/codelife is not linked')
|
|
123
|
+
},
|
|
124
|
+
changelog() {
|
|
125
|
+
if (!installed()) stop('the package is not installed — the hooks item comes first')
|
|
126
|
+
const former = detect('.').changelog
|
|
127
|
+
if (former.length) say('former form', `${former.join(', ')} — removed for the package's generator`)
|
|
128
|
+
const text = read('CHANGELOG.md')
|
|
129
|
+
const gen = isPackage() ? 'scripts/changelog.mjs' : 'node_modules/@ersbeth/codelife/scripts/changelog.mjs'
|
|
130
|
+
if (text?.includes(MARKER)) say('marker', 'there')
|
|
131
|
+
else if (text === null) say('write', 'CHANGELOG.md — generated from the commits, the marker below')
|
|
132
|
+
else {
|
|
133
|
+
if (/^## \[Unreleased\]/m.test(text)) stop('CHANGELOG.md has a hand-written [Unreleased] section: fold its entries into the commits git-cliff renders — an entry with no commit is named — show it, remove it on your yes, then run this again')
|
|
134
|
+
say('convert', 'CHANGELOG.md — the generated part above the marker from the last tag, the hand-written file below it untouched')
|
|
135
|
+
}
|
|
136
|
+
if (!write) return
|
|
137
|
+
if (former.length) {
|
|
138
|
+
const r = migrate('.', 'changelog')
|
|
139
|
+
for (const [k, v] of Object.entries(r)) for (const x of v) say(k, x)
|
|
140
|
+
paths.push(...r.removed, ...r.rewritten)
|
|
141
|
+
}
|
|
142
|
+
if (!text?.includes(MARKER)) writeFileSync('CHANGELOG.md', `${MARKER}\n${text ? `\n${text}` : ''}`)
|
|
143
|
+
const r = sh(process.execPath, [gen])
|
|
144
|
+
if (r.status !== 0) failed.push(`${gen}: ${said(r)}`)
|
|
145
|
+
const c = sh(process.execPath, [gen, '--check'])
|
|
146
|
+
if (c.status !== 0 || c.stdout !== read('CHANGELOG.md')) failed.push(`node ${gen} --check does not reproduce CHANGELOG.md`)
|
|
147
|
+
paths.push('CHANGELOG.md')
|
|
148
|
+
},
|
|
149
|
+
develop() {
|
|
150
|
+
const d = develop()
|
|
151
|
+
const o = origin()
|
|
152
|
+
const dflt = git('symbolic-ref', '--short', 'refs/remotes/origin/HEAD').stdout.trim().replace(/^origin\//, '')
|
|
153
|
+
if (dflt && dflt !== 'main') stop(`the default branch is ${dflt}, not main — rename it yourself in the forge's settings${o ? ` (${o.forge === 'gh' ? `https://github.com/${o.project}/settings` : `https://${o.host}/${o.project}/-/settings/repository`})` : ''}`)
|
|
154
|
+
say('develop', { absent: 'none — created from main', behind: `${d.why[0]} — fast-forwarded`, level: 'level with main or ahead of it — nothing to do', diverged: d.why[0], unknown: d.why[0] }[d.state])
|
|
155
|
+
if (d.state === 'diverged' || d.state === 'unknown') stop(d.why[0])
|
|
156
|
+
if (!write || d.state === 'level') return
|
|
157
|
+
const local = git('rev-parse', '--verify', '--quiet', 'develop').status === 0
|
|
158
|
+
const r = !local ? git('branch', 'develop', 'main')
|
|
159
|
+
: git('branch', '--show-current').stdout.trim() === 'develop' ? git('merge', '--ff-only', 'main') : git('fetch', '.', 'main:develop')
|
|
160
|
+
if (r.status !== 0) failed.push(`develop could not be moved — ${said(r)}`)
|
|
161
|
+
else if (develop().state !== 'level') failed.push('develop is still behind main')
|
|
162
|
+
else say('push', '`git push -u origin develop` — yours to run, with `! ` before it')
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
items[name]()
|
|
166
|
+
done()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// What `codelife setup prep`, `codelife setup item` and `codelife overview` share: the forge read
|
|
2
|
+
// from `origin` without a contract, the connection to its host, and the state of each base item of
|
|
3
|
+
// target-base.md, read from the tree, git and the tracker at each call (K-1 — setup, R-153). Nothing
|
|
4
|
+
// is recorded: an item's state is what this file finds.
|
|
5
|
+
import '../node-floor.mjs'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { env, parseOrigin, TOKENS } from '../tracker/lib.mjs'
|
|
11
|
+
import { detect } from '../former-form.mjs'
|
|
12
|
+
import { packageManager, script } from '../package-manager.mjs'
|
|
13
|
+
import { runChecks } from '../verify/lib.mjs'
|
|
14
|
+
|
|
15
|
+
export const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
16
|
+
const MAX = 256 * 1024 * 1024
|
|
17
|
+
// The changelog's generated/history marker, spelled in scripts/changelog.mjs alone and read from it.
|
|
18
|
+
export const MARKER = readFileSync(join(pkg, 'scripts/changelog.mjs'), 'utf8').match(/^const marker = '(.+)'$/m)[1]
|
|
19
|
+
export const read = (p) => { try { return readFileSync(p, 'utf8') } catch { return null } }
|
|
20
|
+
export const said = (r) => `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() || r.error?.message || ''
|
|
21
|
+
export const sh = (file, args, opts = {}) => spawnSync(file, args, { encoding: 'utf8', input: '', maxBuffer: MAX, ...opts })
|
|
22
|
+
export const git = (...a) => sh('git', a)
|
|
23
|
+
export const say = (key, value) => console.log(`- ${key}: ${value}`)
|
|
24
|
+
export const json = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return null } }
|
|
25
|
+
|
|
26
|
+
// origin — { host, project, forge }: the forge is the host's kind, never the machine's (R-150) —
|
|
27
|
+
// `?` for a host neither GitLab's nor GitHub's; null without an origin.
|
|
28
|
+
export const origin = () => {
|
|
29
|
+
const r = git('remote', 'get-url', 'origin')
|
|
30
|
+
const o = r.status === 0 ? parseOrigin(r.stdout.trim()) : null
|
|
31
|
+
if (!o) return null
|
|
32
|
+
return { ...o, forge: o.host === 'github.com' ? 'gh' : o.host === 'gitlab.com' || o.host.startsWith('gitlab.') ? 'glab' : '?' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// forgeCall <forge> <args> — the CLI with the user's token variables stripped (tracker/lib.mjs).
|
|
36
|
+
export { TOKENS }
|
|
37
|
+
export const forgeCall = (forge, args) => sh(forge, args, { env: env() })
|
|
38
|
+
|
|
39
|
+
// auth <forge> <host> — { connected, line }: `connected, expires <date>`, `connected, expiry
|
|
40
|
+
// unavailable`, `expired`, `not connected: <the CLI's words>`; a CLI absent from the path is not
|
|
41
|
+
// connected (R-150).
|
|
42
|
+
export const auth = (forge, host) => {
|
|
43
|
+
const s = forgeCall(forge, ['auth', 'status', '--hostname', host])
|
|
44
|
+
const out = said(s)
|
|
45
|
+
if (s.status !== 0 || !out.includes('✓')) return { connected: false, line: out.includes('401 Unauthorized') ? 'expired' : `not connected: ${out || `${forge} is not on the path`}` }
|
|
46
|
+
let expires
|
|
47
|
+
if (forge === 'glab') {
|
|
48
|
+
const t = forgeCall(forge, ['api', 'personal_access_tokens/self', '--hostname', host])
|
|
49
|
+
expires = t.status === 0 ? JSON.parse(t.stdout).expires_at || 'never' : ''
|
|
50
|
+
} else expires = forgeCall(forge, ['api', '-i', 'user']).stdout?.match(/^github-authentication-token-expiration:[ \t]*(.+)$/im)?.[1].trim()
|
|
51
|
+
return { connected: true, expires, line: expires ? `connected, expires ${expires}` : 'connected, expiry unavailable' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// unmet <forge> <host> <project> <the project's JSON> — each forge setting codelife relies on that is
|
|
55
|
+
// off, with the page that changes it (setup R-155): merge commits, and on GitHub the merged branch
|
|
56
|
+
// deleted, which `gh pr create` cannot ask for as glab's `--remove-source-branch` does (R-72).
|
|
57
|
+
export const unmet = (forge, host, project, j) => {
|
|
58
|
+
const at = forge === 'glab' ? `https://${host}/${project}/-/settings/merge_requests` : `https://github.com/${project}/settings`
|
|
59
|
+
return [...((forge === 'glab' ? j.merge_method === 'ff' : j.allow_merge_commit === false) ? ['forbids merge commits, which codelife merges with'] : []),
|
|
60
|
+
...(forge === 'gh' && j.delete_branch_on_merge === false ? ['keeps a merged branch: turn on Automatically delete head branches'] : [])].map((x) => `${x} — ${at}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The base, read from target-base.md's tables: a section's names, each backquoted name of a row's
|
|
64
|
+
// first cell, so the definition is the one place that lists them.
|
|
65
|
+
const base = () => readFileSync(join(pkg, 'scripts/target/target-base.md'), 'utf8')
|
|
66
|
+
export const section = (text, heading) => text.split(new RegExp(`^## ${heading}[ \\t]*$`, 'm'))[1]?.split(/^## /m)[0] ?? ''
|
|
67
|
+
export const names = (heading) => [...section(base(), heading).matchAll(/^\| (`[^|]*`) \|/gm)].flatMap((m) => [...m[1].matchAll(/`([^`]+)`/g)].map((x) => x[1]))
|
|
68
|
+
export const LABELS = () => [...(section(base(), 'Tracker').match(/labels — (.*?) — /s)?.[1] ?? '').matchAll(/`([^`]+)`/g)].map((m) => m[1])
|
|
69
|
+
|
|
70
|
+
// labels <forge> — the tracker's label names, or { error }.
|
|
71
|
+
export const labels = (forge) => {
|
|
72
|
+
const l = forgeCall(forge, forge === 'glab' ? ['api', 'projects/:id/labels?per_page=100', '--paginate', '--output', 'ndjson'] : ['label', 'list', '--limit', '1000', '--json', 'name'])
|
|
73
|
+
if (l.status !== 0) return { error: said(l) }
|
|
74
|
+
return { names: forge === 'glab' ? l.stdout.split('\n').filter(Boolean).map((x) => JSON.parse(x).name) : JSON.parse(l.stdout).map((x) => x.name) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// templates <forge> — each issue template at the forge's native location, in the form the forge
|
|
78
|
+
// reads: GitLab's file and its `/label` quick action, GitHub's frontmatter (K-27).
|
|
79
|
+
export const templates = (forge) => {
|
|
80
|
+
const dir = join(pkg, 'scripts/tracker/templates')
|
|
81
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md')).sort().map((f) => {
|
|
82
|
+
const t = readFileSync(join(dir, f), 'utf8')
|
|
83
|
+
if (forge === 'glab') return { at: `.gitlab/issue_templates/${f}`, want: `${t}\n/label ~${t.match(/labels="([^"]+)"/)?.[1]}\n` }
|
|
84
|
+
const meta = Object.fromEntries([...(t.match(/^<!-- codelife: (.*) -->$/m)?.[1] ?? '').matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]]))
|
|
85
|
+
const body = t.split('\n').filter((x) => !x.startsWith('<!-- codelife:')).join('\n')
|
|
86
|
+
return { at: `.github/ISSUE_TEMPLATE/${f}`, want: `---\nname: ${meta.name}\nabout: ${meta.about}\nlabels: ${meta.labels}\n---\n${body}` }
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A hook's template, and where the folders' files come from.
|
|
91
|
+
export const hookTemplate = (h) => read(join(pkg, 'scripts/target/templates/githooks', h))
|
|
92
|
+
export const folderTemplate = (p) => join(pkg, 'scripts/target/templates', p.replace(/^docs\//, ''))
|
|
93
|
+
// blank <template> <project> — a specification file as the base writes it: the project's name filled,
|
|
94
|
+
// every line a `{{…}}` guidance touches gone, a span across lines included. A document left with its
|
|
95
|
+
// guidance breaks the form the pre-commit hook holds, once the seven entries exist.
|
|
96
|
+
export const blank = (text, project) => {
|
|
97
|
+
const out = []
|
|
98
|
+
let depth = 0
|
|
99
|
+
for (const line of text.replaceAll('{{project}}', project).split('\n')) {
|
|
100
|
+
const was = depth
|
|
101
|
+
depth += (line.match(/\{\{/g) ?? []).length - (line.match(/\}\}/g) ?? []).length
|
|
102
|
+
if (!was && !depth && !line.includes('{{')) out.push(line)
|
|
103
|
+
}
|
|
104
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The version this plugin is, and the one the repository installed.
|
|
108
|
+
export const version = () => json(join(pkg, 'package.json')).version
|
|
109
|
+
export const isPackage = () => json('package.json')?.name === '@ersbeth/codelife'
|
|
110
|
+
export const installed = () => (isPackage() ? version() : json('node_modules/@ersbeth/codelife/package.json')?.version ?? null)
|
|
111
|
+
|
|
112
|
+
// items <{ forge, connected }> — every base item in the order /setup plays it, each
|
|
113
|
+
// { item, state: 'done' | 'to do' | 'not applicable' | 'not read', why: [] }.
|
|
114
|
+
export const ITEMS = ['connect', 'labels', 'templates', 'scripts', 'hooks', 'folders', 'changelog', 'develop']
|
|
115
|
+
export const items = ({ forge, connected }) => {
|
|
116
|
+
const tracked = forge === 'glab' || forge === 'gh'
|
|
117
|
+
const pj = json('package.json')
|
|
118
|
+
const former = detect('.')
|
|
119
|
+
const why = {
|
|
120
|
+
connect: !tracked ? null : connected ? [] : ['this machine is not connected to the host'],
|
|
121
|
+
labels: !tracked ? null : !connected ? undefined : (() => {
|
|
122
|
+
const l = labels(forge)
|
|
123
|
+
if (l.error) return [`the labels could not be read — ${l.error}`]
|
|
124
|
+
const missing = LABELS().filter((x) => !l.names.includes(x))
|
|
125
|
+
return missing.length ? [`missing ${missing.join(', ')}`] : []
|
|
126
|
+
})(),
|
|
127
|
+
templates: !tracked ? null : templates(forge).flatMap(({ at, want }) => {
|
|
128
|
+
const mine = read(at)
|
|
129
|
+
return mine === null ? [`${at} is missing`] : mine !== want ? [`${at} differs from this codelife's`] : []
|
|
130
|
+
}),
|
|
131
|
+
scripts: names('Scripts').filter((s) => !pj?.scripts?.[s]).map((s) => `${s} is missing from package.json`),
|
|
132
|
+
hooks: [
|
|
133
|
+
...(installed() ? [] : ['the package @ersbeth/codelife is not installed']),
|
|
134
|
+
...(git('config', 'core.hooksPath').stdout.trim() === '.githooks' ? [] : [`core.hooksPath is ${git('config', 'core.hooksPath').stdout.trim() || 'unset'}, not .githooks`]),
|
|
135
|
+
...names('Hooks').flatMap((h) => {
|
|
136
|
+
const mine = read(`.githooks/${h}`)
|
|
137
|
+
// a hook of the repository's own, no marker, is kept: the item showed it, and its reason stays (R-151)
|
|
138
|
+
return mine === null ? [`.githooks/${h} is missing`] : /generated by codelife/.test(mine) && mine !== hookTemplate(h) ? [`.githooks/${h} differs from this codelife's`] : []
|
|
139
|
+
}),
|
|
140
|
+
...(former.postinstall ? ['a postinstall wires the hooks, where the package\'s prepare should'] : []),
|
|
141
|
+
...(former.marketplace ? ['.claude/settings.json still enables the former marketplace copy'] : []),
|
|
142
|
+
],
|
|
143
|
+
folders: names('Folders').filter((p) => !existsSync(p)).map((p) => `${p} is missing`),
|
|
144
|
+
changelog: [
|
|
145
|
+
...(former.changelog.length ? [`former copies: ${former.changelog.join(', ')}`] : []),
|
|
146
|
+
...(!existsSync('CHANGELOG.md') ? ['CHANGELOG.md is missing'] : read('CHANGELOG.md').includes(MARKER) ? [] : ['CHANGELOG.md has no generated/history marker']),
|
|
147
|
+
],
|
|
148
|
+
develop: develop().why,
|
|
149
|
+
}
|
|
150
|
+
return ITEMS.map((item) => {
|
|
151
|
+
const w = why[item]
|
|
152
|
+
return { item, state: w === null ? 'not applicable' : w === undefined ? 'not read' : w.length ? 'to do' : 'done', why: w ?? (item === 'labels' ? ['not connected'] : []) }
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// writer <write> [test] — what an item verb of /setup or /port-toolchain shares: `put` shows what
|
|
157
|
+
// writing a file changes — `write` when absent, `same`, or `differs` and the difference git prints —
|
|
158
|
+
// and with --write writes it; `scripts` runs package.json scripts through the package manager,
|
|
159
|
+
// `test:one` on the --test file, true when one is red or missing; `paths` and `failed` close the
|
|
160
|
+
// block at `done`; `stop` exits 1 with nothing written.
|
|
161
|
+
export const writer = (write, test) => {
|
|
162
|
+
const paths = [], failed = []
|
|
163
|
+
const stop = (why) => { say('stop', why); process.exit(1) }
|
|
164
|
+
const done = () => {
|
|
165
|
+
if (paths.length) say('paths', paths.join(' '))
|
|
166
|
+
if (failed.length) { for (const f of failed) say('failed', f); process.exit(1) }
|
|
167
|
+
process.exit(0)
|
|
168
|
+
}
|
|
169
|
+
const put = (path, text, mode) => {
|
|
170
|
+
const mine = read(path)
|
|
171
|
+
if (mine === text) return say('same', path)
|
|
172
|
+
if (mine === null) say('write', path)
|
|
173
|
+
else {
|
|
174
|
+
const tmp = join(dirname(path), `.${process.pid}.codelife`)
|
|
175
|
+
writeFileSync(tmp, text)
|
|
176
|
+
const diff = git('diff', '--no-index', '--', path, tmp).stdout.split('\n')
|
|
177
|
+
say('differs', `${path} — yours on -, codelife's on +\n${diff.slice(diff.findIndex((l) => l.startsWith('@@'))).join('\n')}`)
|
|
178
|
+
rmSync(tmp)
|
|
179
|
+
}
|
|
180
|
+
if (!write) return
|
|
181
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
182
|
+
writeFileSync(path, text)
|
|
183
|
+
if (mode) chmodSync(path, mode)
|
|
184
|
+
paths.push(path)
|
|
185
|
+
}
|
|
186
|
+
const scripts = (list) => {
|
|
187
|
+
const pm = packageManager('.') || 'npm', pj = json('package.json')
|
|
188
|
+
return runChecks(list.map((s) => (pj?.scripts?.[s] ? { name: s, file: script(pm, s)[0], args: [...script(pm, s).slice(1), ...(s === 'test:one' ? [test] : [])], shell: process.platform === 'win32' }
|
|
189
|
+
: { name: s, missing: `no ${s} script` })), '- ')
|
|
190
|
+
}
|
|
191
|
+
return { paths, failed, stop, done, put, scripts }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// develop — where develop stands against main: { why, state: absent | behind | level | diverged | unknown }.
|
|
195
|
+
export const develop = () => {
|
|
196
|
+
const has = (ref) => git('rev-parse', '--verify', '--quiet', `${ref}^{commit}`).status === 0
|
|
197
|
+
const dev = has('develop') ? 'develop' : has('origin/develop') ? 'origin/develop' : null
|
|
198
|
+
const main = has('main') ? 'main' : has('origin/main') ? 'origin/main' : null
|
|
199
|
+
if (!main) return { state: 'unknown', why: ['no main branch to read develop against'] }
|
|
200
|
+
if (!dev) return { state: 'absent', why: ['no develop branch'] }
|
|
201
|
+
const [d, m] = [git('rev-parse', dev).stdout.trim(), git('rev-parse', main).stdout.trim()]
|
|
202
|
+
if (d === m || git('merge-base', '--is-ancestor', m, d).status === 0) return { state: 'level', why: [] }
|
|
203
|
+
if (git('merge-base', '--is-ancestor', d, m).status === 0) return { state: 'behind', why: [`develop is ${git('rev-list', '--count', `${d}..${m}`).stdout.trim()} commits behind main`] }
|
|
204
|
+
return { state: 'diverged', why: ['develop and main have diverged — reported, never moved'] }
|
|
205
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `codelife setup prep` (K-1 — setup, R-149): everything /setup states before it asks — the mode,
|
|
3
|
+
// origin and the connection to its host, the guide when it is not connected, what the contract's
|
|
4
|
+
// values are inferred from, the prose to read, the default branch and the merge method, the former
|
|
5
|
+
// form, and each base item's state. Reads only; one `### setup prep` block; a `- stop:` line per
|
|
6
|
+
// thing that stops the run, and exit 1.
|
|
7
|
+
import '../node-floor.mjs'
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { check, contract } from '../contract.mjs'
|
|
11
|
+
import { detect } from '../former-form.mjs'
|
|
12
|
+
import { packageManager } from '../package-manager.mjs'
|
|
13
|
+
import { auth, forgeCall, git, installed, items, json, origin, read, said, say, sh, TOKENS, unmet, version } from './lib.mjs'
|
|
14
|
+
|
|
15
|
+
if (process.argv.length > 2) { console.error('usage: codelife setup prep'); process.exit(2) }
|
|
16
|
+
if (git('rev-parse', '--show-toplevel').status !== 0) { console.log('### setup prep\n- stop: not a git repository — /setup runs in one'); process.exit(1) }
|
|
17
|
+
const stops = []
|
|
18
|
+
// The rights a token is created with, in the forge's own words (connection.md R-81): each serves a
|
|
19
|
+
// verb, and none writes the repository's files.
|
|
20
|
+
const RIGHTS = {
|
|
21
|
+
glab: ['Work Item: read', 'Work Item: create, update', 'Label: read', 'Label: create', 'Merge Request: read',
|
|
22
|
+
'Merge Request: create, update', 'Merge Request: approve', 'Release: create, read', 'Project: read', 'User: read',
|
|
23
|
+
'Personal Access Token: read, under the **User** tab'],
|
|
24
|
+
gh: ['Issues: read and write', 'Pull requests: read and write', 'Metadata: read'],
|
|
25
|
+
}
|
|
26
|
+
const ls = (glob) => git('ls-files', '-z', '--', glob).stdout.split('\0').filter((f) => f && !f.includes('node_modules/'))
|
|
27
|
+
console.log('### setup prep')
|
|
28
|
+
|
|
29
|
+
// The mode (R-153): no contract is a first run; a valid one, an update; an invalid one stops.
|
|
30
|
+
const c = contract()
|
|
31
|
+
if (!c) say('mode', 'first run — no .claude/codelife.md')
|
|
32
|
+
else {
|
|
33
|
+
const { invalid, stale } = check(c)
|
|
34
|
+
if (invalid.length) stops.push(`the contract is invalid — ${invalid.join('; ')}: fix it, or say it may be rewritten`)
|
|
35
|
+
say('mode', `update — the contract is ${invalid.length ? 'invalid' : stale.length ? `stale: ${stale.join('; ')}` : 'current'}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// origin, its forge and the connection (R-150): the forge is the host's, never the machine's.
|
|
39
|
+
const o = origin()
|
|
40
|
+
const forge = c?.data.tracker?.kind ?? (o?.forge === '?' ? null : o?.forge ?? 'none')
|
|
41
|
+
say('origin', o ? `${o.host} — ${o.project}` : 'none — tracker.kind none, to confirm')
|
|
42
|
+
if (o?.forge === '?' && !c) say('tracker.kind', `to confirm — ${o.host} is neither GitLab's nor GitHub's: which CLI talks to it, glab or gh?`)
|
|
43
|
+
else say('tracker.kind', `${forge}${c ? ' — the contract' : o ? ` — inferred from origin's host ${o.host}` : ''}`)
|
|
44
|
+
let connected = false
|
|
45
|
+
if (forge === 'glab' || forge === 'gh') {
|
|
46
|
+
const onPath = sh(forge, ['--version']).status === 0
|
|
47
|
+
if (!onPath) say('cli', `${forge} is not on the path — install it on your yes: \`brew install ${forge}\``)
|
|
48
|
+
const a = onPath ? auth(forge, o.host) : { connected: false, line: `not connected: ${forge} is not on the path` }
|
|
49
|
+
connected = a.connected
|
|
50
|
+
const left = a.expires && a.expires !== 'never' ? Math.floor((Date.parse(a.expires) - Date.now()) / 86400000) : null
|
|
51
|
+
say('connection', `${a.line}${left !== null && left <= 30 ? ` — in ${left} days: create the next token before then, nothing stops meanwhile` : ''}`)
|
|
52
|
+
for (const v of TOKENS.filter((k) => process.env[k])) say('token variable', `${v} is set in this shell — ignored here, but your terminal's ${forge} uses it: remove it where your shell profile or settings.local.json sets it`)
|
|
53
|
+
if (!connected) {
|
|
54
|
+
const ns = o.project.split('/').slice(0, -1).join('/') || o.project
|
|
55
|
+
const rights = RIGHTS[forge]
|
|
56
|
+
say('guide', forge === 'glab' ? `open https://${o.host}/-/user_settings/personal_access_tokens — Preferences → Access tokens → Add new token`
|
|
57
|
+
: 'open https://github.com/settings/personal-access-tokens/new — Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token')
|
|
58
|
+
say('guide', `name it \`codelife — ${o.host}/${ns}\`, bound to ${ns} alone, the expiry you accept`)
|
|
59
|
+
say('guide', `grant, all at once — a GitLab token cannot be edited later: ${rights.join('; ')}`)
|
|
60
|
+
say('guide', `copy the token, then in your own terminal: \`pbpaste | ${forge === 'glab' ? `glab auth login --hostname ${o.host} --stdin` : `gh auth login --hostname ${o.host} --with-token`}\`${forge === 'gh' ? ' — add `--insecure-storage` for a file you can read and delete rather than the Keychain' : ''}`)
|
|
61
|
+
stops.push(`this machine is not connected to ${o.host}: follow the guide, then run /setup again — nothing is written before`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// What the contract's values are inferred from (R-149): each tagged by the file it comes from.
|
|
66
|
+
const manifests = ls('*package.json').map((f) => ({ f, j: json(f) })).filter((m) => m.j)
|
|
67
|
+
const libs = manifests.filter((m) => m.j.exports || m.j.main || m.j.module || m.j.types).map((m) => m.f)
|
|
68
|
+
const roots = new Set(['', ...manifests.map((m) => m.f.replace(/package\.json$/, ''))])
|
|
69
|
+
const apps = [...ls('*index.html').filter((f) => roots.has(f.replace(/(src\/|public\/)?index\.html$/, ''))), ...manifests.filter((m) => m.j.scripts?.start || m.j.scripts?.preview).map((m) => m.f)]
|
|
70
|
+
say('kind', `${libs.length && !apps.length ? 'lib' : apps.length && !libs.length ? 'app' : libs.length ? 'lib or app' : 'none'} — to confirm${libs.length ? `; a package entry in ${libs.join(', ')}` : ''}${apps.length ? `; an application entry in ${apps.join(', ')}` : ''}`)
|
|
71
|
+
if (c) say('kind', `the contract says ${c.data.kind}`)
|
|
72
|
+
const versioned = [...manifests.map((m) => m.f), ...ls('*plugin.json')].filter((f) => json(f)?.version)
|
|
73
|
+
const tags = git('tag', '--sort=-creatordate').stdout.split('\n').filter(Boolean).slice(0, 3)
|
|
74
|
+
say('release.version_file', versioned.length === 1 ? `${versioned[0]} — inferred` : versioned.length ? `to confirm, one of ${versioned.join(', ')}${tags.length ? ` — the last tags: ${tags.join(', ')}` : ''}` : 'missing — no manifest carries a version')
|
|
75
|
+
for (const dir of [...new Set(ls('*dist/*').concat(ls('*build/*'), ls('*api/*')).map((f) => f.match(/^(.*?(?:dist|build|api))\//)?.[1]).filter(Boolean))]) {
|
|
76
|
+
const owner = manifests.filter((m) => dir.startsWith(m.f.replace(/package\.json$/, ''))).sort((a, b) => b.f.length - a.f.length)[0]
|
|
77
|
+
const s = Object.entries(owner?.j.scripts ?? {}).filter(([k]) => /build|pack|generate/.test(k))
|
|
78
|
+
if (s.length) say('generated', `${dir}/ — tracked, and ${owner.f} builds it: ${s.map(([k, v]) => `${k}: ${v}`).join('; ')} — to confirm`)
|
|
79
|
+
}
|
|
80
|
+
const docs = existsSync('docs') ? readdirSync('docs').filter((f) => f.endsWith('.md')).map((f) => `docs/${f}`) : []
|
|
81
|
+
for (const f of ['CLAUDE.md', 'CONTRIBUTING.md', ...docs]) say('prose', existsSync(f) ? `${f}, ${read(f).split('\n').length} lines — read it whole for guards[] and the four prose sections` : `${f} absent`)
|
|
82
|
+
|
|
83
|
+
// The default branch and the merge method (R-155): what only you can change on the forge.
|
|
84
|
+
const dflt = git('symbolic-ref', '--short', 'refs/remotes/origin/HEAD').stdout.trim().replace(/^origin\//, '')
|
|
85
|
+
const settings = o && (forge === 'glab' ? `https://${o.host}/${o.project}/-/settings/repository` : `https://github.com/${o.project}/settings`)
|
|
86
|
+
if (dflt && dflt !== 'main') stops.push(`the default branch is ${dflt}, not main — rename it yourself at ${settings} (Branch defaults), then run /setup again`)
|
|
87
|
+
say('default branch', dflt || `not read — no origin/HEAD; the branch checked out is ${git('branch', '--show-current').stdout.trim() || 'detached'}`)
|
|
88
|
+
if (connected) {
|
|
89
|
+
const p = forgeCall(forge, forge === 'glab' ? ['api', 'projects/:id'] : ['api', `repos/${o.project}`])
|
|
90
|
+
if (p.status !== 0) say('merge method', `not read — ${said(p)}`)
|
|
91
|
+
else {
|
|
92
|
+
const off = unmet(forge, o.host, o.project, JSON.parse(p.stdout))
|
|
93
|
+
for (const x of off.length ? off : ['nothing to change']) say('merge method', x)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The package, the former form, the items (R-151, R-153).
|
|
98
|
+
const pm = packageManager('.') || 'npm'
|
|
99
|
+
const have = installed()
|
|
100
|
+
say('package', have ? `@ersbeth/codelife ${have}${have !== version() ? `, this plugin is ${version()}` : ''}` : `not installed — \`${pm} ${pm === 'npm' ? 'install' : 'add'} -D @ersbeth/codelife@${version()}\` on your yes, before the hooks item`)
|
|
101
|
+
const former = detect('.')
|
|
102
|
+
const left = [...former.hooks, ...former.changelog, ...(former.postinstall ? ['a postinstall'] : []), ...(former.marketplace ? ['the marketplace in .claude/settings.json'] : [])]
|
|
103
|
+
if (left.length) say('former form', `${left.join(', ')} — the hooks and changelog items migrate them`)
|
|
104
|
+
for (const i of items({ forge, connected })) say(`item ${i.item}`, `${i.state}${i.why.length ? ` — ${i.why.join('; ')}` : ''}`)
|
|
105
|
+
for (const s of stops) say('stop', s)
|
|
106
|
+
process.exit(stops.length ? 1 : 0)
|