ai4kanban 0.4.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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # ai4kanban
2
+
3
+ Set up and update [AI4Kanban](https://ai4kanban.dev/) in one command.
4
+
5
+ AI4Kanban is AI project management that grows with you: you give the agent a vague idea, it
6
+ breaks the idea down, settles what it can on its own, asks you the rest, and keeps going
7
+ until the spec is clear enough to build. The board is plain Markdown in `docs/kanban/`,
8
+ versioned in git.
9
+
10
+ This package is the setup command. The board and the skill are the product.
11
+
12
+ ## Install
13
+
14
+ From your project root:
15
+
16
+ ```bash
17
+ npx ai4kanban install
18
+ ```
19
+
20
+ That copies the skill into `.claude/skills/kanban/` (Claude Code) and
21
+ `.agents/skills/kanban/` (Codex), then scaffolds `docs/kanban/` — the track folders, the
22
+ board index, the memory set, and a blank `config.md`.
23
+
24
+ Pass the tracks your project actually splits into:
25
+
26
+ ```bash
27
+ npx ai4kanban install --tracks feature,bug,research
28
+ ```
29
+
30
+ Normally you don't run this by hand. You paste the install prompt from
31
+ <https://ai4kanban.dev/INSTALL_PROMPT.txt> and your agent reads the repo, picks the tracks,
32
+ runs this command, and fills in the config afterwards.
33
+
34
+ ## Update
35
+
36
+ ```bash
37
+ npx ai4kanban update
38
+ ```
39
+
40
+ Overwrites every skill folder it finds with this version, repairs a board written by an
41
+ older release, and prints which version you moved from and to with a link to everything
42
+ that changed in between. Your cards, config, and memory are never touched.
43
+
44
+ ## What it won't do
45
+
46
+ Reading your repo, filling in `docs/kanban/config.md`, writing the module map, proposing
47
+ tasks — all of that needs a judgement call, so it stays the agent's job. When this command
48
+ hits something it can't decide, it prints it under **Needs your attention** and leaves it
49
+ alone.
50
+
51
+ Both commands are safe to run twice.
52
+
53
+ ## Also
54
+
55
+ - The board UI: `npx ai4kanban-ui` — a local page over the same Markdown files.
56
+ - Source and docs: <https://github.com/ai4kanban/ai4kanban>
57
+
58
+ Node 18+. No dependencies.
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // ai4kanban — the one command that sets up and updates the board.
4
+ //
5
+ // npx ai4kanban install [--tracks a,b,c] copy the skill in, scaffold docs/kanban/
6
+ // npx ai4kanban update overwrite every installed skill folder, repair the board
7
+ // npx ai4kanban version print this package's version
8
+ //
9
+ // Why this exists: setup used to be a list of shell commands (`git clone`, `mkdir`, `cp -R`,
10
+ // a `printf`) that the user had to approve one at a time. This is one command instead.
11
+ //
12
+ // What it deliberately does NOT do: anything that needs a judgement call. Reading the repo,
13
+ // filling in `docs/kanban/config.md`, writing the module map, proposing the first tasks —
14
+ // those stay the agent's job. When this script meets something it can't decide, it says so
15
+ // under "Needs your attention" and leaves it alone.
16
+ //
17
+ // Node 18+, no dependencies, same behaviour on macOS, Linux and Windows.
18
+
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { spawnSync } from 'node:child_process'
23
+
24
+ const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
25
+ const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_DIR, 'package.json'), 'utf8')).version
26
+ const REPO = 'https://github.com/ai4kanban/ai4kanban'
27
+
28
+ // Every harness that reads skills from a folder in the repo. Install writes both, so the
29
+ // same board works whichever agent the user opens tomorrow; update only touches the ones
30
+ // that are already there.
31
+ const SKILL_TARGETS = [
32
+ { rel: path.join('.claude', 'skills', 'kanban'), agent: 'Claude Code' },
33
+ { rel: path.join('.agents', 'skills', 'kanban'), agent: 'Codex' },
34
+ ]
35
+
36
+ // The memory set used to sit at the board root before it moved into `memory/`.
37
+ const MEMORY_FILES = ['readme.md', 'goal.md', 'decisions.md', 'redesign.md', 'rejected.md']
38
+
39
+ // Both templates ship with this marker until someone fills them in.
40
+ const UNFILLED = '_(not filled in yet'
41
+
42
+ // ---- output ----------------------------------------------------------------
43
+
44
+ const did = []
45
+ const notes = []
46
+
47
+ function say(line) {
48
+ console.log(line)
49
+ }
50
+
51
+ function fail(msg) {
52
+ console.error(`ai4kanban: ${msg}`)
53
+ process.exit(1)
54
+ }
55
+
56
+ // ---- files -----------------------------------------------------------------
57
+
58
+ function copyDir(src, dest) {
59
+ fs.mkdirSync(dest, { recursive: true })
60
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
61
+ const from = path.join(src, entry.name)
62
+ const to = path.join(dest, entry.name)
63
+ if (entry.isDirectory()) copyDir(from, to)
64
+ else fs.copyFileSync(from, to)
65
+ }
66
+ }
67
+
68
+ function read(file) {
69
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null
70
+ }
71
+
72
+ function statOf(p) {
73
+ try {
74
+ return fs.lstatSync(p)
75
+ } catch {
76
+ return null
77
+ }
78
+ }
79
+
80
+ // The skill folder to copy from: `skill/` inside the published package, or the repo's own
81
+ // `skill/` when this file is run straight out of a source checkout.
82
+ function bundledSkill() {
83
+ for (const dir of [path.join(PKG_DIR, 'skill'), path.join(PKG_DIR, '..', 'skill')]) {
84
+ if (fs.existsSync(path.join(dir, 'SKILL.md'))) return dir
85
+ }
86
+ fail('no skill folder inside this package — the published tarball is incomplete')
87
+ }
88
+
89
+ // The released version baked into an installed `kanban.mjs`, so update can say where the
90
+ // user is coming from. Null for an install made before the version was baked in.
91
+ function installedVersion(skillDir) {
92
+ const src = read(path.join(skillDir, 'kanban.mjs'))
93
+ const m = src && src.match(/const SKILL_VERSION = '([^']+)'/)
94
+ return m ? m[1] : null
95
+ }
96
+
97
+ // ---- placing the skill -----------------------------------------------------
98
+
99
+ // Copy the skill into each target folder, wholesale: the old folder is removed first, so a
100
+ // file upstream deleted doesn't linger. `mode` is 'install' (write every target) or
101
+ // 'update' (only refresh what's already there).
102
+ function placeSkill(root, mode) {
103
+ const source = bundledSkill()
104
+ const placed = []
105
+ for (const target of SKILL_TARGETS) {
106
+ const dest = path.join(root, target.rel)
107
+ const st = statOf(dest)
108
+ if (st && st.isSymbolicLink()) {
109
+ // A source checkout of this repo symlinks its skill folder at the real `skill/`.
110
+ // Copying over it would overwrite the source, so leave it alone and say so.
111
+ notes.push(`${target.rel} is a symlink — left untouched (it points at a source checkout)`)
112
+ continue
113
+ }
114
+ if (!st && mode === 'update') continue
115
+ const before = st ? installedVersion(dest) : null
116
+ if (st) rescueSkillConfig(root, dest)
117
+ if (st) fs.rmSync(dest, { recursive: true, force: true })
118
+ copyDir(source, dest)
119
+ did.push(`${st ? 'replaced' : 'installed'} the skill in ${target.rel}/ (${target.agent})`)
120
+ placed.push({ dest, before, existed: Boolean(st) })
121
+ }
122
+ return placed
123
+ }
124
+
125
+ // An install made before the config moved out of the skill folder keeps the user's filled-in
126
+ // `config.md` there — and the folder is about to be wiped. Move it to where it belongs now.
127
+ // It's the one file in the skill folder that was ever the user's.
128
+ function rescueSkillConfig(root, skillDir) {
129
+ const old = read(path.join(skillDir, 'config.md'))
130
+ if (!old || old.includes('{{')) return // still the blank template — nothing of the user's in it
131
+ const boardConfig = path.join(root, 'docs', 'kanban', 'config.md')
132
+ const current = read(boardConfig)
133
+ if (current === null) {
134
+ fs.mkdirSync(path.dirname(boardConfig), { recursive: true })
135
+ fs.writeFileSync(boardConfig, old)
136
+ did.push('moved your filled-in config.md out of the skill folder to docs/kanban/config.md')
137
+ return
138
+ }
139
+ if (current === old) return
140
+ const aside = path.join(root, 'docs', 'kanban', 'config.from-skill.md')
141
+ if (fs.existsSync(aside)) return
142
+ fs.writeFileSync(aside, old)
143
+ notes.push(
144
+ 'the old skill folder held a different filled-in config.md — saved as docs/kanban/config.from-skill.md;' +
145
+ ' fold anything worth keeping into docs/kanban/config.md and delete it',
146
+ )
147
+ }
148
+
149
+ // ---- the board -------------------------------------------------------------
150
+
151
+ function runKanban(skillDir, root, args) {
152
+ const result = spawnSync(process.execPath, [path.join(skillDir, 'kanban.mjs'), ...args], {
153
+ cwd: root,
154
+ stdio: 'inherit',
155
+ })
156
+ if (result.status !== 0) fail(`\`kanban.mjs ${args.join(' ')}\` failed — nothing else was changed`)
157
+ }
158
+
159
+ // The repair steps an update has to run on a board written by an older version. Each one is
160
+ // mechanical; anything with a choice in it becomes a note instead.
161
+ function repairBoard(skillDir, root) {
162
+ const board = path.join(root, 'docs', 'kanban')
163
+ if (!fs.existsSync(board)) {
164
+ notes.push('no docs/kanban/ here — run `npx ai4kanban install` to scaffold the board')
165
+ return
166
+ }
167
+ moveLegacyMemory(board)
168
+ sayDid()
169
+ // `init` on an existing board is the repair step: it adds what an older version never
170
+ // wrote (config.md, modules.md, the memory paths, the goal's `reviewed:` field) and never
171
+ // touches a file that's already filled in.
172
+ runKanban(skillDir, root, ['init'])
173
+ dropModuleGoals(board)
174
+ checkConfig(board)
175
+ checkModules(board)
176
+ }
177
+
178
+ // An older layout kept the memory set at the board root. Move each file into `memory/`.
179
+ function moveLegacyMemory(board) {
180
+ const memory = path.join(board, 'memory')
181
+ const moved = []
182
+ for (const name of MEMORY_FILES) {
183
+ const from = path.join(board, name)
184
+ const st = statOf(from)
185
+ if (!st || !st.isFile()) continue
186
+ fs.mkdirSync(memory, { recursive: true })
187
+ const to = path.join(memory, name)
188
+ const there = read(to)
189
+ if (there === null) {
190
+ fs.renameSync(from, to)
191
+ moved.push(name)
192
+ } else if (there === read(from)) {
193
+ fs.rmSync(from)
194
+ moved.push(name)
195
+ } else {
196
+ notes.push(`both docs/kanban/${name} and docs/kanban/memory/${name} exist and differ — merge them by hand`)
197
+ }
198
+ }
199
+ if (moved.length) did.push(`moved the memory set into docs/kanban/memory/: ${moved.join(', ')}`)
200
+ }
201
+
202
+ // `goal.md` lives at the board root of `memory/` only. An older layout gave every module a
203
+ // copy; drop the ones that say nothing the root one doesn't, and report the rest.
204
+ function dropModuleGoals(board) {
205
+ const memory = path.join(board, 'memory')
206
+ if (!fs.existsSync(memory)) return
207
+ const root = read(path.join(memory, 'goal.md'))
208
+ let dropped = 0
209
+ for (const entry of fs.readdirSync(memory, { withFileTypes: true })) {
210
+ if (!entry.isDirectory()) continue
211
+ const file = path.join(memory, entry.name, 'goal.md')
212
+ const text = read(file)
213
+ if (text === null) continue
214
+ if (text === root || text.includes(UNFILLED)) {
215
+ fs.rmSync(file)
216
+ dropped++
217
+ } else {
218
+ notes.push(
219
+ `docs/kanban/memory/${entry.name}/goal.md says something the root goal doesn't —` +
220
+ ' fold that into docs/kanban/memory/goal.md, then delete it',
221
+ )
222
+ }
223
+ }
224
+ if (dropped) did.push(`removed ${dropped} leftover per-module goal.md (the goal lives at docs/kanban/memory/goal.md)`)
225
+ }
226
+
227
+ // A setting this release ships that the user's config has never heard of. Naming it is all
228
+ // a script can do — the value is the user's to choose.
229
+ function checkConfig(board) {
230
+ const template = read(path.join(bundledSkill(), 'config.md'))
231
+ const current = read(path.join(board, 'config.md'))
232
+ if (!template || !current) return
233
+ const keys = (text) => [...text.matchAll(/^- \*\*(.+?)\*\*/gm)].map((m) => m[1])
234
+ const added = keys(template).filter((k) => !keys(current).includes(k))
235
+ if (added.length) {
236
+ notes.push(`this release adds ${added.map((k) => `**${k}**`).join(', ')} to the config — add the line to docs/kanban/config.md and fill it in`)
237
+ }
238
+ }
239
+
240
+ function checkModules(board) {
241
+ const map = read(path.join(board, 'modules.md'))
242
+ if (map && map.includes(UNFILLED)) {
243
+ notes.push('docs/kanban/modules.md is still blank — write it from the repo (the skill\'s references/module-map.md), then re-run this command so every module gets a memory path')
244
+ }
245
+ }
246
+
247
+ // ---- commands --------------------------------------------------------------
248
+
249
+ function cmdInstall(root, tracks) {
250
+ say(`ai4kanban ${VERSION} — installing into ${root}`)
251
+ const placed = placeSkill(root, 'install')
252
+ if (!placed.length) {
253
+ sayNotes()
254
+ fail('nothing to install — every skill folder here is a symlink')
255
+ }
256
+ say('')
257
+ sayDid()
258
+ say('')
259
+ runKanban(placed[0].dest, root, ['init', ...tracks])
260
+ sayNotes()
261
+ say('')
262
+ say('Next, and only an agent can do these:')
263
+ say(' 1. fill in docs/kanban/config.md from what the repo tells you')
264
+ say(' 2. write docs/kanban/modules.md (the skill\'s references/module-map.md says how)')
265
+ say(' 3. propose the first 3 tasks')
266
+ }
267
+
268
+ function cmdUpdate(root) {
269
+ say(`ai4kanban ${VERSION} — updating ${root}`)
270
+ const placed = placeSkill(root, 'update')
271
+ const from = placed.map((p) => p.before).find(Boolean)
272
+ if (!placed.length) {
273
+ // Nothing copied is a normal outcome: a plugin install keeps the skill in a read-only
274
+ // cache and a source checkout symlinks it. The board still needs repairing either way,
275
+ // so carry on with the skill this package ships.
276
+ notes.push('no skill folder to overwrite here — expected for a plugin install; repairing the board only')
277
+ }
278
+ say('')
279
+ sayDid()
280
+ repairBoard(placed[0]?.dest || bundledSkill(), root)
281
+ sayDid()
282
+ sayNotes()
283
+ say('')
284
+ if (!placed.length) {
285
+ say(`This package is ${VERSION}. Every release: ${REPO}/releases`)
286
+ } else if (!from) {
287
+ say(`Now at ${VERSION}. Every release: ${REPO}/releases`)
288
+ } else if (from === VERSION) {
289
+ say(`Already at ${VERSION} — the files were re-copied anyway, which changes nothing.`)
290
+ } else {
291
+ say(`Moved from ${from} to ${VERSION}.`)
292
+ say(`Everything that changed: ${REPO}/compare/v${from}...v${VERSION}`)
293
+ }
294
+ say('Your cards, config, and memory were left alone. Review `git diff` before committing.')
295
+ }
296
+
297
+ // Print the changes made since the last call, so each block of output sits next to the step
298
+ // that produced it instead of all landing at the end.
299
+ let reported = 0
300
+ function sayDid() {
301
+ for (const line of did.slice(reported)) say(` · ${line}`)
302
+ reported = did.length
303
+ }
304
+
305
+ function sayNotes() {
306
+ if (!notes.length) return
307
+ say('')
308
+ say('Needs your attention:')
309
+ for (const line of notes) say(` ! ${line}`)
310
+ }
311
+
312
+ const HELP = `ai4kanban ${VERSION} — set up and update the AI4Kanban board.
313
+
314
+ npx ai4kanban install [--tracks a,b,c] copy the skill into .claude/skills/kanban/ and
315
+ .agents/skills/kanban/, then scaffold docs/kanban/
316
+ npx ai4kanban update overwrite every skill folder that's already here
317
+ and repair a board written by an older version
318
+ npx ai4kanban version print this version
319
+ npx ai4kanban help this text
320
+
321
+ Options
322
+ --tracks a,b,c the board's tracks (install only). Default: feature,bug,research
323
+ --dir <path> the project to work on. Default: the current folder
324
+
325
+ Both commands are safe to run twice. Neither one edits your cards, your config, or your
326
+ memory — filling those in is the agent's job.
327
+
328
+ Docs: ${REPO}
329
+ `
330
+
331
+ // ---- entry -----------------------------------------------------------------
332
+
333
+ function parse(argv) {
334
+ const opts = { tracks: [], dir: process.cwd() }
335
+ const rest = []
336
+ for (let i = 0; i < argv.length; i++) {
337
+ const arg = argv[i]
338
+ if (arg === '--tracks' || arg === '--track') {
339
+ opts.tracks = String(argv[++i] || '')
340
+ .split(',')
341
+ .map((t) => t.trim())
342
+ .filter(Boolean)
343
+ } else if (arg.startsWith('--tracks=')) {
344
+ opts.tracks = arg.slice('--tracks='.length).split(',').map((t) => t.trim()).filter(Boolean)
345
+ } else if (arg === '--dir') {
346
+ opts.dir = path.resolve(String(argv[++i] || '.'))
347
+ } else if (arg.startsWith('--dir=')) {
348
+ opts.dir = path.resolve(arg.slice('--dir='.length))
349
+ } else {
350
+ rest.push(arg)
351
+ }
352
+ }
353
+ return { opts, rest }
354
+ }
355
+
356
+ function main() {
357
+ const { opts, rest } = parse(process.argv.slice(2))
358
+ const command = rest[0]
359
+ if (!fs.existsSync(opts.dir)) fail(`no such folder: ${opts.dir}`)
360
+ switch (command) {
361
+ case 'install':
362
+ return cmdInstall(opts.dir, opts.tracks)
363
+ case 'update':
364
+ return cmdUpdate(opts.dir)
365
+ case 'version':
366
+ case '--version':
367
+ case '-v':
368
+ return say(VERSION)
369
+ case undefined:
370
+ case 'help':
371
+ case '--help':
372
+ case '-h':
373
+ return say(HELP)
374
+ default:
375
+ fail(`unknown command "${command}" — try \`npx ai4kanban help\``)
376
+ }
377
+ }
378
+
379
+ main()
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "ai4kanban",
3
+ "version": "0.4.1",
4
+ "description": "Set up and update AI4Kanban in one command — copies the skill into your project and scaffolds the Markdown board under docs/kanban/.",
5
+ "keywords": [
6
+ "kanban",
7
+ "claude",
8
+ "claude-code",
9
+ "codex",
10
+ "agent",
11
+ "skill",
12
+ "markdown",
13
+ "board"
14
+ ],
15
+ "license": "Apache-2.0",
16
+ "homepage": "https://ai4kanban.dev/",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ai4kanban/ai4kanban.git",
20
+ "directory": "cli"
21
+ },
22
+ "type": "module",
23
+ "bin": {
24
+ "ai4kanban": "bin/ai4kanban.mjs"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "skill/"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "scripts": {
34
+ "bundle": "node scripts/bundle-skill.mjs",
35
+ "prepublishOnly": "node ../scripts/sync-version.mjs --check && npm run bundle"
36
+ }
37
+ }