@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.
Files changed (214) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +65 -2
  4. package/agents/evaluate.md +97 -0
  5. package/agents/ground.md +49 -0
  6. package/agents/implementer.md +49 -0
  7. package/agents/judge.md +128 -0
  8. package/agents/planner.md +45 -0
  9. package/agents/prerequisites.md +62 -0
  10. package/agents/reader.md +40 -0
  11. package/agents/reproduce.md +65 -0
  12. package/bin/codelife +4 -0
  13. package/package.json +38 -4
  14. package/presets/tsconfig.json +14 -0
  15. package/presets/vite.config.d.mts +3 -0
  16. package/presets/vite.config.mjs +49 -0
  17. package/scripts/audit/cadence.mjs +24 -0
  18. package/scripts/audit/close.mjs +57 -0
  19. package/scripts/audit/file.mjs +70 -0
  20. package/scripts/audit/fold.mjs +48 -0
  21. package/scripts/audit/lib.mjs +93 -0
  22. package/scripts/audit/prepare.mjs +180 -0
  23. package/scripts/audit/table.mjs +65 -0
  24. package/scripts/brainstorm/decided.mjs +6 -0
  25. package/scripts/brainstorm/lib.mjs +33 -0
  26. package/scripts/brainstorm/named.mjs +9 -0
  27. package/scripts/changelog.mjs +100 -0
  28. package/scripts/check-tag.mjs +19 -0
  29. package/scripts/checks.mjs +53 -0
  30. package/scripts/codelife.mjs +42 -0
  31. package/scripts/commit-msg.mjs +43 -0
  32. package/scripts/contract.mjs +179 -0
  33. package/scripts/former-form.mjs +170 -0
  34. package/scripts/git/branches.mjs +6 -0
  35. package/scripts/git/commit.mjs +40 -0
  36. package/scripts/git/diff.mjs +17 -0
  37. package/scripts/git/diff_lines.mjs +5 -0
  38. package/scripts/git/held.mjs +53 -0
  39. package/scripts/git/is_ancestor.mjs +28 -0
  40. package/scripts/git/lib.mjs +20 -0
  41. package/scripts/git/log_since.mjs +27 -0
  42. package/scripts/git/parent.mjs +51 -0
  43. package/scripts/git/show.mjs +4 -0
  44. package/scripts/git/status.mjs +25 -0
  45. package/scripts/git/submodules.mjs +5 -0
  46. package/scripts/init.mjs +21 -0
  47. package/scripts/install.mjs +32 -0
  48. package/scripts/lenses/A11Y.md +91 -0
  49. package/scripts/lenses/ARCH.md +90 -0
  50. package/scripts/lenses/BUG.md +52 -0
  51. package/scripts/lenses/BUILD.md +40 -0
  52. package/scripts/lenses/COMPAT.md +67 -0
  53. package/scripts/lenses/CONF.md +49 -0
  54. package/scripts/lenses/DEPS.md +43 -0
  55. package/scripts/lenses/DOC.md +73 -0
  56. package/scripts/lenses/FAIL.md +57 -0
  57. package/scripts/lenses/GOV.md +56 -0
  58. package/scripts/lenses/GUARD.md +47 -0
  59. package/scripts/lenses/INTENT.md +52 -0
  60. package/scripts/lenses/LEAN.md +49 -0
  61. package/scripts/lenses/LINES.md +48 -0
  62. package/scripts/lenses/PERF.md +61 -0
  63. package/scripts/lenses/PKG.md +58 -0
  64. package/scripts/lenses/SEC.md +49 -0
  65. package/scripts/lenses/STANDARDS.md +62 -0
  66. package/scripts/lenses/TYPE.md +56 -0
  67. package/scripts/lenses/VERIF.md +81 -0
  68. package/scripts/link.mjs +40 -0
  69. package/scripts/methods/cause.md +21 -0
  70. package/scripts/methods/comments.md +157 -0
  71. package/scripts/methods/description.md +67 -0
  72. package/scripts/methods/message.md +24 -0
  73. package/scripts/methods/red-green.md +28 -0
  74. package/scripts/methods/refusal.md +22 -0
  75. package/scripts/mr/open.mjs +219 -0
  76. package/scripts/node-floor.mjs +19 -0
  77. package/scripts/onboard/prep.mjs +93 -0
  78. package/scripts/overview/codelife-line.mjs +33 -0
  79. package/scripts/overview/next.mjs +76 -0
  80. package/scripts/overview/overview.mjs +308 -0
  81. package/scripts/package-manager.mjs +20 -0
  82. package/scripts/port/item.mjs +94 -0
  83. package/scripts/port/lib.mjs +194 -0
  84. package/scripts/port/prep.mjs +33 -0
  85. package/scripts/port/version.mjs +50 -0
  86. package/scripts/pre-commit.mjs +43 -0
  87. package/scripts/pre-push.mjs +53 -0
  88. package/scripts/prepare-lib.mjs +81 -0
  89. package/scripts/read-roadmap/lib.mjs +116 -0
  90. package/scripts/read-roadmap/moved.mjs +70 -0
  91. package/scripts/read-roadmap/range.mjs +15 -0
  92. package/scripts/release/cut.mjs +120 -0
  93. package/scripts/release/entry.mjs +52 -0
  94. package/scripts/release/lib.mjs +136 -0
  95. package/scripts/release/prep.mjs +58 -0
  96. package/scripts/review-mr/prepare.mjs +163 -0
  97. package/scripts/roadmap/assemble.mjs +269 -0
  98. package/scripts/roadmap/cut.mjs +88 -0
  99. package/scripts/roadmap/prepare.mjs +249 -0
  100. package/scripts/roadmap/roadmap.md +30 -0
  101. package/scripts/roadmap/write.mjs +53 -0
  102. package/scripts/search.mjs +29 -0
  103. package/scripts/setup/contract.mjs +25 -0
  104. package/scripts/setup/item.mjs +166 -0
  105. package/scripts/setup/lib.mjs +205 -0
  106. package/scripts/setup/prep.mjs +106 -0
  107. package/scripts/spec-form.mjs +444 -0
  108. package/scripts/spec.mjs +56 -0
  109. package/scripts/tags.mjs +27 -0
  110. package/scripts/target/target-app.md +134 -0
  111. package/scripts/target/target-base.md +127 -0
  112. package/scripts/target/target-lib.md +143 -0
  113. package/scripts/target/templates/audit/README.md +21 -0
  114. package/scripts/target/templates/characterisation/bundle.test.ts +43 -0
  115. package/scripts/target/templates/characterisation/declarations.test.ts +44 -0
  116. package/scripts/target/templates/characterisation/entries.ts +50 -0
  117. package/scripts/target/templates/characterisation/examples.test.ts +18 -0
  118. package/scripts/target/templates/characterisation/import.test.ts +22 -0
  119. package/scripts/target/templates/characterisation/surface.test.ts +68 -0
  120. package/scripts/target/templates/cliff.toml +61 -0
  121. package/scripts/target/templates/githooks/commit-msg +9 -0
  122. package/scripts/target/templates/githooks/pre-commit +9 -0
  123. package/scripts/target/templates/githooks/pre-push +9 -0
  124. package/scripts/target/templates/playground/index.html +11 -0
  125. package/scripts/target/templates/playground/main.ts +9 -0
  126. package/scripts/target/templates/spec/README.md +35 -0
  127. package/scripts/target/templates/spec/capability.md +36 -0
  128. package/scripts/target/templates/spec/glossary.md +5 -0
  129. package/scripts/target/templates/spec/journey.md +29 -0
  130. package/scripts/target/templates/spec/principles.md +15 -0
  131. package/scripts/target/templates/spec/users.md +15 -0
  132. package/scripts/target/templates/tsconfig.json +9 -0
  133. package/scripts/target/templates/vite.config.ts +4 -0
  134. package/scripts/text.mjs +6 -0
  135. package/scripts/tracker/account.mjs +5 -0
  136. package/scripts/tracker/auth.mjs +9 -0
  137. package/scripts/tracker/cli.version.mjs +5 -0
  138. package/scripts/tracker/close.mjs +20 -0
  139. package/scripts/tracker/create.mjs +47 -0
  140. package/scripts/tracker/issue.section.mjs +25 -0
  141. package/scripts/tracker/issue.update.mjs +13 -0
  142. package/scripts/tracker/issue.view.mjs +16 -0
  143. package/scripts/tracker/lib.mjs +130 -0
  144. package/scripts/tracker/list.mjs +14 -0
  145. package/scripts/tracker/list.whole.mjs +20 -0
  146. package/scripts/tracker/lot.row.mjs +42 -0
  147. package/scripts/tracker/mr.checkout_command.mjs +5 -0
  148. package/scripts/tracker/mr.comments.mjs +50 -0
  149. package/scripts/tracker/mr.create.mjs +12 -0
  150. package/scripts/tracker/mr.description.mjs +8 -0
  151. package/scripts/tracker/mr.fetch_head.mjs +5 -0
  152. package/scripts/tracker/mr.find_for_branch.mjs +9 -0
  153. package/scripts/tracker/mr.post.mjs +40 -0
  154. package/scripts/tracker/mr.update_description.mjs +8 -0
  155. package/scripts/tracker/mr.verdict.mjs +34 -0
  156. package/scripts/tracker/mr.view.mjs +20 -0
  157. package/scripts/tracker/none.mjs +79 -0
  158. package/scripts/tracker/note.mjs +12 -0
  159. package/scripts/tracker/reopen.mjs +8 -0
  160. package/scripts/tracker/search.mjs +8 -0
  161. package/scripts/tracker/search_id.mjs +18 -0
  162. package/scripts/tracker/templates/bug.md +35 -0
  163. package/scripts/tracker/templates/feature.md +40 -0
  164. package/scripts/tracker/templates/lot.md +46 -0
  165. package/scripts/tracker/title.join.mjs +6 -0
  166. package/scripts/tracker/title.leave.mjs +7 -0
  167. package/scripts/tracker/title.lot_of.mjs +8 -0
  168. package/scripts/tracker/title.search_id_filter.mjs +17 -0
  169. package/scripts/tracker/title.sentence_of.mjs +5 -0
  170. package/scripts/tracker/view.mjs +25 -0
  171. package/scripts/verbs.mjs +579 -0
  172. package/scripts/verify/BUILD.mjs +33 -0
  173. package/scripts/verify/DEPS.mjs +15 -0
  174. package/scripts/verify/GOV.mjs +34 -0
  175. package/scripts/verify/GUARD.mjs +34 -0
  176. package/scripts/verify/LINES.mjs +22 -0
  177. package/scripts/verify/PKG.mjs +56 -0
  178. package/scripts/verify/TYPE.mjs +34 -0
  179. package/scripts/verify/cut.mjs +73 -0
  180. package/scripts/verify/history.mjs +32 -0
  181. package/scripts/verify/lib.mjs +129 -0
  182. package/scripts/verify/report.mjs +104 -0
  183. package/scripts/verify/timing.mjs +19 -0
  184. package/scripts/verify.mjs +29 -0
  185. package/scripts/work/branch.mjs +81 -0
  186. package/scripts/work/close.mjs +74 -0
  187. package/scripts/work/member.mjs +73 -0
  188. package/scripts/work/prepare.mjs +116 -0
  189. package/scripts/work/reopen.mjs +40 -0
  190. package/scripts/work-feedback/prepare.mjs +96 -0
  191. package/skills/audit/SKILL.md +110 -0
  192. package/skills/audit/templates/index.md +36 -0
  193. package/skills/audit/templates/lens.md +44 -0
  194. package/skills/brainstorm/SKILL.md +144 -0
  195. package/skills/brainstorm/references/exit.md +57 -0
  196. package/skills/brainstorm/templates/note.md +79 -0
  197. package/skills/commit/SKILL.md +68 -0
  198. package/skills/mr/SKILL.md +82 -0
  199. package/skills/new-issue/SKILL.md +87 -0
  200. package/skills/onboard/SKILL.md +103 -0
  201. package/skills/onboard/templates/CLAUDE.md +31 -0
  202. package/skills/onboard/templates/CONTRIBUTING.md +34 -0
  203. package/skills/onboard/templates/README.md +30 -0
  204. package/skills/onboard/templates/SKILL.md +24 -0
  205. package/skills/onboard/templates/package-CLAUDE.md +15 -0
  206. package/skills/overview/SKILL.md +59 -0
  207. package/skills/port-toolchain/SKILL.md +127 -0
  208. package/skills/release/SKILL.md +78 -0
  209. package/skills/review-mr/SKILL.md +106 -0
  210. package/skills/review-mr/templates/pass.md +29 -0
  211. package/skills/roadmap/SKILL.md +137 -0
  212. package/skills/setup/SKILL.md +112 -0
  213. package/skills/work/SKILL.md +139 -0
  214. 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)