kabanos 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] — 2026-06-23
9
+
10
+ ### Added
11
+
12
+ - Comment scanner for TypeScript/JavaScript/JSX/TSX, Python, Go, Rust, CSS/SCSS, and HTML with `.gitignore` support, project-root containment, file-size limits, incremental mtime/size caching, and a pluggable custom parser API.
13
+ - Board reconciliation lifecycle: create cards from new comments, update moved or changed comments, archive removed comments, resolve cards without touching source files, and suppress resolved fingerprints on future scans.
14
+ - SQL persistence layer via Kysely with SQLite (default), PostgreSQL, and MySQL adapters. Schema migrations run automatically and are versioned — safe to upgrade without manual intervention.
15
+ - Fetch-compatible core API with auth guard enforcement, actor resolution, JSON/origin mutation checks, source-context endpoint, settings and column management endpoints, and manual scan trigger.
16
+ - Framework adapters: Express, Fastify, Next.js route handler, and generic Node `http`.
17
+ - React kanban UI with light/dark Notion-derived theme tokens, drag-and-drop, keyboard column movement, card details panel, notes, assignee, labels, source context viewer, scan trigger, settings panel, column management, and scan path overrides.
18
+ - `kabanos init` CLI that generates `kanban.config.ts` and prints a framework-specific mounting snippet without modifying host application source files.
19
+ - Runnable demo (`pnpm demo`) with multi-language sample project (TypeScript, Python, Go, CSS, HTML).
20
+
21
+ ### Fixed
22
+
23
+ - Card position accounting in multi-column reconcile runs: positions are now tracked per-column so newly created cards in one column no longer inflate position indices for cards created in other columns in the same scan.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FP2003
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Kabanos
2
+
3
+ Kabanos turns `TODO` and `FIXME` comments into a kanban board mounted inside an existing Node.js application. Source files are always read-only: resolving a card records state in the configured database and never rewrites the comment.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20 or newer
8
+ - An explicit host authorization guard, or an explicit public-mode opt-out
9
+
10
+ ## Install and initialize
11
+
12
+ ```sh
13
+ npm install kabanos
14
+ npx kabanos init
15
+ ```
16
+
17
+ pnpm blocks native dependency install scripts unless explicitly approved. When pnpm reports that `better-sqlite3` was ignored, run `pnpm approve-builds` and approve that package before using the default SQLite adapter.
18
+
19
+ The initializer creates `kanban.config.ts` and `.kanban/`, then prints a framework-specific mounting snippet. It does not modify application source files. Add `.kanban/` to `.gitignore` when using SQLite.
20
+
21
+ ## Runnable demo
22
+
23
+ ```sh
24
+ npx pnpm demo
25
+ ```
26
+
27
+ Open `http://127.0.0.1:3000/admin/kb`. The demo scans sample TypeScript, Python, Go, CSS, and HTML comments and persists board state under the ignored `demo/project/.kanban/` directory.
28
+
29
+ To restore all demo cards to their initial state:
30
+
31
+ ```sh
32
+ npx pnpm demo:reset
33
+ ```
34
+
35
+ ## Express example
36
+
37
+ ```ts
38
+ import express from 'express';
39
+ import config from './kanban.config.js';
40
+ import { createKabanos } from 'kabanos';
41
+ import { createExpressMiddleware } from 'kabanos/express';
42
+
43
+ const app = express();
44
+ app.use(express.json());
45
+
46
+ const kabanos = await createKabanos(config);
47
+ app.use(config.mountPath, createExpressMiddleware(kabanos));
48
+
49
+ const server = app.listen(3000);
50
+ process.once('SIGTERM', async () => {
51
+ server.close();
52
+ await kabanos.close();
53
+ });
54
+ ```
55
+
56
+ Equivalent entrypoints are exported as `kabanos/fastify`, `kabanos/next`, and `kabanos/node`.
57
+
58
+ ## Configuration
59
+
60
+ ```ts
61
+ import { defineConfig } from 'kabanos';
62
+
63
+ export default defineConfig({
64
+ projectRoot: process.cwd(),
65
+ mountPath: '/admin/kb',
66
+ columns: [
67
+ { id: 'backlog', name: 'Backlog' },
68
+ { id: 'in-progress', name: 'In Progress' },
69
+ { id: 'done', name: 'Done', completion: true },
70
+ ],
71
+ scan: {
72
+ include: ['src/**/*.{ts,tsx,js,jsx,py,go,rs,css,scss,html}'],
73
+ exclude: ['**/*.test.*'],
74
+ tags: {
75
+ TODO: { column: 'backlog', priority: 'normal' },
76
+ FIXME: { column: 'in-progress', priority: 'high' },
77
+ },
78
+ watch: process.env.NODE_ENV === 'development',
79
+ },
80
+ storage: { adapter: 'sqlite', filename: '.kanban/board.db' },
81
+ theme: { default: 'system' },
82
+ auth: {
83
+ guard: request => request.user?.role === 'admin',
84
+ actor: request => request.user
85
+ ? { id: String(request.user.id), name: request.user.name }
86
+ : undefined,
87
+ },
88
+ });
89
+ ```
90
+
91
+ Exactly one column must have `completion: true`. String-only column arrays remain supported; the final string becomes the completion column. UI changes are stored as database overrides without rewriting the configuration file.
92
+
93
+ For PostgreSQL or MySQL, select the corresponding adapter and provide `connectionString`. The `pg` and `mysql2` drivers are optional dependencies. Schema migrations run automatically on startup — no manual migration step required.
94
+
95
+ ## Lifecycle and safety
96
+
97
+ - New tagged comments create cards.
98
+ - Moving or editing surrounding lines retains the card through content/context matching.
99
+ - Editing an open comment updates and flags its card.
100
+ - Deleting a comment archives its card on the next scan.
101
+ - Resolving a card stores its fingerprint and suppresses rediscovery.
102
+ - Editing a resolved comment changes its fingerprint and creates new work.
103
+ - Symlinks are not followed, configured ignores and `.gitignore` are honored, and source context access is confined to `projectRoot`.
104
+
105
+ The package does not contain a source-writing API.
106
+
107
+ ## Development
108
+
109
+ ```sh
110
+ pnpm install
111
+ pnpm typecheck
112
+ pnpm test
113
+ pnpm test:benchmark
114
+ PLAYWRIGHT_CHROMIUM_PATH=/usr/bin/chromium pnpm test:e2e
115
+ docker compose -f compose.test.yml up -d --wait
116
+ pnpm test:databases
117
+ docker compose -f compose.test.yml down -v
118
+ pnpm build
119
+ pnpm pack
120
+ ```
@@ -0,0 +1,7 @@
1
+ import { RequestHandler } from 'express';
2
+ import { K as KabanosInstance } from '../core-C0gUmthe.js';
3
+ import 'node:http';
4
+
5
+ declare function createExpressMiddleware(instance: KabanosInstance): RequestHandler;
6
+
7
+ export { createExpressMiddleware };
@@ -0,0 +1,22 @@
1
+ // src/adapters/express.ts
2
+ import { Readable } from "stream";
3
+ function createExpressMiddleware(instance) {
4
+ return async (req, res, next) => {
5
+ try {
6
+ const origin = `${req.protocol}://${req.get("host") ?? "localhost"}`;
7
+ const body = ["GET", "HEAD"].includes(req.method) ? void 0 : req.body !== void 0 ? JSON.stringify(req.body) : void 0;
8
+ const request = new globalThis.Request(new URL(req.originalUrl, origin), { method: req.method, headers: req.headers, body });
9
+ const response = await instance.handler(request, req);
10
+ res.status(response.status);
11
+ response.headers.forEach((value, key) => res.setHeader(key, value));
12
+ if (response.body) Readable.fromWeb(response.body).pipe(res);
13
+ else res.end();
14
+ } catch (error) {
15
+ next(error);
16
+ }
17
+ };
18
+ }
19
+ export {
20
+ createExpressMiddleware
21
+ };
22
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/express.ts"],"sourcesContent":["import type { Request, RequestHandler, Response } from 'express';\nimport { Readable } from 'node:stream';\nimport type { KabanosInstance } from '../core.js';\n\nexport function createExpressMiddleware(instance:KabanosInstance):RequestHandler{return async(req:Request,res:Response,next)=>{try{const origin=`${req.protocol}://${req.get('host')??'localhost'}`;const body=['GET','HEAD'].includes(req.method)?undefined:req.body!==undefined?JSON.stringify(req.body):undefined;const request=new globalThis.Request(new URL(req.originalUrl,origin),{method:req.method,headers:req.headers as HeadersInit,body});const response=await instance.handler(request,req);res.status(response.status);response.headers.forEach((value,key)=>res.setHeader(key,value));if(response.body)Readable.fromWeb(response.body as never).pipe(res);else res.end();}catch(error){next(error);}};}\n"],"mappings":";AACA,SAAS,gBAAgB;AAGlB,SAAS,wBAAwB,UAAwC;AAAC,SAAO,OAAM,KAAY,KAAa,SAAO;AAAC,QAAG;AAAC,YAAM,SAAO,GAAG,IAAI,QAAQ,MAAM,IAAI,IAAI,MAAM,KAAG,WAAW;AAAG,YAAM,OAAK,CAAC,OAAM,MAAM,EAAE,SAAS,IAAI,MAAM,IAAE,SAAU,IAAI,SAAO,SAAU,KAAK,UAAU,IAAI,IAAI,IAAE;AAAU,YAAM,UAAQ,IAAI,WAAW,QAAQ,IAAI,IAAI,IAAI,aAAY,MAAM,GAAE,EAAC,QAAO,IAAI,QAAO,SAAQ,IAAI,SAAuB,KAAI,CAAC;AAAE,YAAM,WAAS,MAAM,SAAS,QAAQ,SAAQ,GAAG;AAAE,UAAI,OAAO,SAAS,MAAM;AAAE,eAAS,QAAQ,QAAQ,CAAC,OAAM,QAAM,IAAI,UAAU,KAAI,KAAK,CAAC;AAAE,UAAG,SAAS,KAAK,UAAS,QAAQ,SAAS,IAAa,EAAE,KAAK,GAAG;AAAA,UAAO,KAAI,IAAI;AAAA,IAAE,SAAO,OAAM;AAAC,WAAK,KAAK;AAAA,IAAE;AAAA,EAAC;AAAE;","names":[]}
@@ -0,0 +1,7 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { K as KabanosInstance } from '../core-C0gUmthe.js';
3
+ import 'node:http';
4
+
5
+ declare function kabanosFastify(instance: KabanosInstance): FastifyPluginAsync;
6
+
7
+ export { kabanosFastify };
@@ -0,0 +1,19 @@
1
+ // src/adapters/fastify.ts
2
+ function kabanosFastify(instance) {
3
+ return async (fastify) => {
4
+ const handler = async (request, reply) => {
5
+ const origin = `${request.protocol}://${request.host}`;
6
+ const web = new Request(new URL(request.url, origin), { method: request.method, headers: request.headers, body: ["GET", "HEAD"].includes(request.method) ? void 0 : JSON.stringify(request.body) });
7
+ const response = await instance.handler(web, request);
8
+ reply.status(response.status);
9
+ response.headers.forEach((value, key) => reply.header(key, value));
10
+ return reply.send(Buffer.from(await response.arrayBuffer()));
11
+ };
12
+ fastify.all(instance.config.mountPath, handler);
13
+ fastify.all(`${instance.config.mountPath}/*`, handler);
14
+ };
15
+ }
16
+ export {
17
+ kabanosFastify
18
+ };
19
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/fastify.ts"],"sourcesContent":["import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';\nimport type { KabanosInstance } from '../core.js';\n\nexport function kabanosFastify(instance:KabanosInstance):FastifyPluginAsync{return async fastify=>{const handler=async(request:FastifyRequest,reply:FastifyReply)=>{const origin=`${request.protocol}://${request.host}`;const web=new Request(new URL(request.url,origin),{method:request.method,headers:request.headers as HeadersInit,body:['GET','HEAD'].includes(request.method)?undefined:JSON.stringify(request.body)});const response=await instance.handler(web,request);reply.status(response.status);response.headers.forEach((value,key)=>reply.header(key,value));return reply.send(Buffer.from(await response.arrayBuffer()));};fastify.all(instance.config.mountPath,handler);fastify.all(`${instance.config.mountPath}/*`,handler);};}\n"],"mappings":";AAGO,SAAS,eAAe,UAA4C;AAAC,SAAO,OAAM,YAAS;AAAC,UAAM,UAAQ,OAAM,SAAuB,UAAqB;AAAC,YAAM,SAAO,GAAG,QAAQ,QAAQ,MAAM,QAAQ,IAAI;AAAG,YAAM,MAAI,IAAI,QAAQ,IAAI,IAAI,QAAQ,KAAI,MAAM,GAAE,EAAC,QAAO,QAAQ,QAAO,SAAQ,QAAQ,SAAuB,MAAK,CAAC,OAAM,MAAM,EAAE,SAAS,QAAQ,MAAM,IAAE,SAAU,KAAK,UAAU,QAAQ,IAAI,EAAC,CAAC;AAAE,YAAM,WAAS,MAAM,SAAS,QAAQ,KAAI,OAAO;AAAE,YAAM,OAAO,SAAS,MAAM;AAAE,eAAS,QAAQ,QAAQ,CAAC,OAAM,QAAM,MAAM,OAAO,KAAI,KAAK,CAAC;AAAE,aAAO,MAAM,KAAK,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;AAAA,IAAE;AAAE,YAAQ,IAAI,SAAS,OAAO,WAAU,OAAO;AAAE,YAAQ,IAAI,GAAG,SAAS,OAAO,SAAS,MAAK,OAAO;AAAA,EAAE;AAAE;","names":[]}
@@ -0,0 +1,6 @@
1
+ import { K as KabanosInstance } from '../core-C0gUmthe.js';
2
+ import 'node:http';
3
+
4
+ declare function createNextHandler(instance: KabanosInstance): (request: Request) => Promise<Response>;
5
+
6
+ export { createNextHandler };
@@ -0,0 +1,8 @@
1
+ // src/adapters/next.ts
2
+ function createNextHandler(instance) {
3
+ return (request) => instance.handler(request, request);
4
+ }
5
+ export {
6
+ createNextHandler
7
+ };
8
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/next.ts"],"sourcesContent":["import type { KabanosInstance } from '../core.js';\nexport function createNextHandler(instance:KabanosInstance){return (request:Request)=>instance.handler(request,request);}\n"],"mappings":";AACO,SAAS,kBAAkB,UAAyB;AAAC,SAAO,CAAC,YAAkB,SAAS,QAAQ,SAAQ,OAAO;AAAE;","names":[]}
@@ -0,0 +1,6 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { K as KabanosInstance } from '../core-C0gUmthe.js';
3
+
4
+ declare function createNodeHandler(instance: KabanosInstance): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
5
+
6
+ export { createNodeHandler };
@@ -0,0 +1,19 @@
1
+ // src/adapters/node.ts
2
+ import { Readable } from "stream";
3
+ function createNodeHandler(instance) {
4
+ return async (req, res) => {
5
+ const forwarded = req.headers["x-forwarded-proto"];
6
+ const protocol = forwarded ? String(forwarded).split(",")[0].trim() : req.socket.encrypted ? "https" : "http";
7
+ const origin = `${protocol}://${req.headers.host ?? "localhost"}`;
8
+ const request = new Request(new URL(req.url ?? "/", origin), { method: req.method, headers: req.headers, body: ["GET", "HEAD"].includes(req.method ?? "GET") ? void 0 : Readable.toWeb(req), duplex: "half" });
9
+ const response = await instance.handler(request, req);
10
+ res.statusCode = response.status;
11
+ response.headers.forEach((value, key) => res.setHeader(key, value));
12
+ if (response.body) Readable.fromWeb(response.body).pipe(res);
13
+ else res.end();
14
+ };
15
+ }
16
+ export {
17
+ createNodeHandler
18
+ };
19
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/node.ts"],"sourcesContent":["import { Readable } from 'node:stream';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { KabanosInstance } from '../core.js';\n\nexport function createNodeHandler(instance:KabanosInstance){return async(req:IncomingMessage,res:ServerResponse)=>{\n const forwarded=req.headers['x-forwarded-proto'];const protocol=forwarded?String(forwarded).split(',')[0]!.trim():(req.socket as {encrypted?:boolean}).encrypted?'https':'http';\n const origin=`${protocol}://${req.headers.host??'localhost'}`;\n const request=new Request(new URL(req.url??'/',origin),{method:req.method,headers:req.headers as HeadersInit,body:['GET','HEAD'].includes(req.method??'GET')?undefined:Readable.toWeb(req) as ReadableStream<Uint8Array>,duplex:'half'} as RequestInit);\n const response=await instance.handler(request,req);res.statusCode=response.status;response.headers.forEach((value,key)=>res.setHeader(key,value));if(response.body)Readable.fromWeb(response.body as never).pipe(res);else res.end();\n};}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAIlB,SAAS,kBAAkB,UAAyB;AAAC,SAAO,OAAM,KAAoB,QAAqB;AAChH,UAAM,YAAU,IAAI,QAAQ,mBAAmB;AAAE,UAAM,WAAS,YAAU,OAAO,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK,IAAG,IAAI,OAAgC,YAAU,UAAQ;AACzK,UAAM,SAAO,GAAG,QAAQ,MAAM,IAAI,QAAQ,QAAM,WAAW;AAC3D,UAAM,UAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAK,KAAI,MAAM,GAAE,EAAC,QAAO,IAAI,QAAO,SAAQ,IAAI,SAAuB,MAAK,CAAC,OAAM,MAAM,EAAE,SAAS,IAAI,UAAQ,KAAK,IAAE,SAAU,SAAS,MAAM,GAAG,GAAgC,QAAO,OAAM,CAAgB;AACtP,UAAM,WAAS,MAAM,SAAS,QAAQ,SAAQ,GAAG;AAAE,QAAI,aAAW,SAAS;AAAO,aAAS,QAAQ,QAAQ,CAAC,OAAM,QAAM,IAAI,UAAU,KAAI,KAAK,CAAC;AAAE,QAAG,SAAS,KAAK,UAAS,QAAQ,SAAS,IAAa,EAAE,KAAK,GAAG;AAAA,QAAO,KAAI,IAAI;AAAA,EACrO;AAAE;","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { access, mkdir, writeFile } from "fs/promises";
5
+ import path from "path";
6
+ import prompts from "prompts";
7
+ import pc from "picocolors";
8
+ var command = process.argv[2];
9
+ if (!command || ["-h", "--help", "help"].includes(command)) {
10
+ console.log(`kabanos
11
+
12
+ Usage:
13
+ kabanos init Create a configuration and print mount instructions
14
+ kabanos --help Show this help`);
15
+ process.exit(0);
16
+ }
17
+ if (command !== "init") {
18
+ console.error(pc.red(`Unknown command: ${command}`));
19
+ process.exit(1);
20
+ }
21
+ var root = process.cwd();
22
+ var target = path.join(root, "kanban.config.ts");
23
+ var exists = await fileExists(target);
24
+ var answers = await prompts([
25
+ { type: "select", name: "framework", message: "Host framework", choices: [
26
+ { title: "Express", value: "express" },
27
+ { title: "Fastify", value: "fastify" },
28
+ { title: "Next.js route handler", value: "next" },
29
+ { title: "Node HTTP", value: "node" }
30
+ ] },
31
+ { type: "select", name: "storage", message: "Storage adapter", choices: [
32
+ { title: "SQLite (recommended)", value: "sqlite" },
33
+ { title: "PostgreSQL", value: "postgres" },
34
+ { title: "MySQL", value: "mysql" }
35
+ ] },
36
+ { type: "text", name: "mountPath", message: "Mount path", initial: "/admin/kb", validate: (value) => /^\/[\w/-]*$/.test(value) || "Enter an absolute URL path." },
37
+ { type: "confirm", name: "public", message: "Allow access without a host auth guard?", initial: false },
38
+ ...exists ? [{ type: "confirm", name: "overwrite", message: "kanban.config.ts exists. Replace it?", initial: false }] : []
39
+ ], { onCancel: () => {
40
+ console.log(pc.yellow("Initialization cancelled."));
41
+ process.exit(1);
42
+ } });
43
+ if (exists && !answers.overwrite) {
44
+ console.log(pc.yellow("No files changed."));
45
+ process.exit(0);
46
+ }
47
+ await mkdir(path.join(root, ".kanban"), { recursive: true });
48
+ await writeFile(target, configTemplate(answers.storage, answers.mountPath, answers.public));
49
+ console.log(pc.green("\nCreated kanban.config.ts and .kanban/"));
50
+ console.log(pc.bold("\nMount Kabanos in your application:\n"));
51
+ console.log(snippet(answers.framework, answers.mountPath));
52
+ console.log(pc.dim("\nAdd .kanban/ to .gitignore. The initializer did not modify host source files."));
53
+ async function fileExists(file) {
54
+ try {
55
+ await access(file);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ function configTemplate(storage, mountPath, isPublic) {
62
+ return `import { defineConfig } from 'kabanos';
63
+
64
+ export default defineConfig({
65
+ projectRoot: process.cwd(),
66
+ mountPath: ${JSON.stringify(mountPath)},
67
+ storage: {
68
+ adapter: '${storage}',${storage === "sqlite" ? "\n filename: '.kanban/board.db'," : "\n connectionString: process.env.KANBAN_DB_URL,"}
69
+ },
70
+ scan: {
71
+ include: ['src/**/*.{ts,tsx,js,jsx,py,go,rs,css,scss,html}'],
72
+ watch: process.env.NODE_ENV === 'development',
73
+ },
74
+ auth: {
75
+ enabled: ${!isPublic},${isPublic ? "" : "\n // Replace this with your host application authorization check.\n guard: (request) => Boolean(request.user),\n actor: (request) => request.user ? { id: String(request.user.id), name: request.user.name } : undefined,"}
76
+ },
77
+ });
78
+ `;
79
+ }
80
+ function snippet(framework, mountPath) {
81
+ if (framework === "express") return `import config from './kanban.config.js';
82
+ import { createKabanos } from 'kabanos';
83
+ import { createExpressMiddleware } from 'kabanos/express';
84
+
85
+ const kabanos = await createKabanos(config);
86
+ app.use(${JSON.stringify(mountPath)}, createExpressMiddleware(kabanos));`;
87
+ if (framework === "fastify") return `import config from './kanban.config.js';
88
+ import { createKabanos } from 'kabanos';
89
+ import { kabanosFastify } from 'kabanos/fastify';
90
+
91
+ const kabanos = await createKabanos(config);
92
+ await app.register(kabanosFastify(kabanos));`;
93
+ if (framework === "next") return `// app/admin/kb/[[...kabanos]]/route.ts
94
+ import config from '@/kanban.config';
95
+ import { createKabanos } from 'kabanos';
96
+ import { createNextHandler } from 'kabanos/next';
97
+
98
+ const handler = createNextHandler(await createKabanos(config));
99
+ export { handler as GET, handler as POST, handler as PATCH, handler as PUT };`;
100
+ return `import { createServer } from 'node:http';
101
+ import config from './kanban.config.js';
102
+ import { createKabanos } from 'kabanos';
103
+ import { createNodeHandler } from 'kabanos/node';
104
+
105
+ const kabanos = await createKabanos(config);
106
+ createServer(createNodeHandler(kabanos)).listen(3000);`;
107
+ }
108
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { access, mkdir, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport prompts from 'prompts';\nimport pc from 'picocolors';\n\nconst command = process.argv[2];\nif (!command || ['-h', '--help', 'help'].includes(command)) {\n console.log(`kabanos\\n\\nUsage:\\n kabanos init Create a configuration and print mount instructions\\n kabanos --help Show this help`);\n process.exit(0);\n}\nif (command !== 'init') { console.error(pc.red(`Unknown command: ${command}`)); process.exit(1); }\n\nconst root = process.cwd();\nconst target = path.join(root, 'kanban.config.ts');\nconst exists = await fileExists(target);\nconst answers = await prompts([\n { type: 'select', name: 'framework', message: 'Host framework', choices: [\n { title: 'Express', value: 'express' }, { title: 'Fastify', value: 'fastify' }, { title: 'Next.js route handler', value: 'next' }, { title: 'Node HTTP', value: 'node' },\n ] },\n { type: 'select', name: 'storage', message: 'Storage adapter', choices: [\n { title: 'SQLite (recommended)', value: 'sqlite' }, { title: 'PostgreSQL', value: 'postgres' }, { title: 'MySQL', value: 'mysql' },\n ] },\n { type: 'text', name: 'mountPath', message: 'Mount path', initial: '/admin/kb', validate: value => /^\\/[\\w/-]*$/.test(value) || 'Enter an absolute URL path.' },\n { type: 'confirm', name: 'public', message: 'Allow access without a host auth guard?', initial: false },\n ...(exists ? [{ type: 'confirm' as const, name: 'overwrite', message: 'kanban.config.ts exists. Replace it?', initial: false }] : []),\n], { onCancel: () => { console.log(pc.yellow('Initialization cancelled.')); process.exit(1); } });\n\nif (exists && !answers.overwrite) { console.log(pc.yellow('No files changed.')); process.exit(0); }\nawait mkdir(path.join(root, '.kanban'), { recursive: true });\nawait writeFile(target, configTemplate(answers.storage, answers.mountPath, answers.public));\nconsole.log(pc.green('\\nCreated kanban.config.ts and .kanban/'));\nconsole.log(pc.bold('\\nMount Kabanos in your application:\\n'));\nconsole.log(snippet(answers.framework, answers.mountPath));\nconsole.log(pc.dim('\\nAdd .kanban/ to .gitignore. The initializer did not modify host source files.'));\n\nasync function fileExists(file:string){try{await access(file);return true;}catch{return false;}}\nfunction configTemplate(storage:string,mountPath:string,isPublic:boolean){return `import { defineConfig } from 'kabanos';\\n\\nexport default defineConfig({\\n projectRoot: process.cwd(),\\n mountPath: ${JSON.stringify(mountPath)},\\n storage: {\\n adapter: '${storage}',${storage==='sqlite'?\"\\n filename: '.kanban/board.db',\":\"\\n connectionString: process.env.KANBAN_DB_URL,\"}\\n },\\n scan: {\\n include: ['src/**/*.{ts,tsx,js,jsx,py,go,rs,css,scss,html}'],\\n watch: process.env.NODE_ENV === 'development',\\n },\\n auth: {\\n enabled: ${!isPublic},${isPublic?'':'\\n // Replace this with your host application authorization check.\\n guard: (request) => Boolean(request.user),\\n actor: (request) => request.user ? { id: String(request.user.id), name: request.user.name } : undefined,'}\\n },\\n});\\n`;}\nfunction snippet(framework:string,mountPath:string){\n if(framework==='express')return `import config from './kanban.config.js';\\nimport { createKabanos } from 'kabanos';\\nimport { createExpressMiddleware } from 'kabanos/express';\\n\\nconst kabanos = await createKabanos(config);\\napp.use(${JSON.stringify(mountPath)}, createExpressMiddleware(kabanos));`;\n if(framework==='fastify')return `import config from './kanban.config.js';\\nimport { createKabanos } from 'kabanos';\\nimport { kabanosFastify } from 'kabanos/fastify';\\n\\nconst kabanos = await createKabanos(config);\\nawait app.register(kabanosFastify(kabanos));`;\n if(framework==='next')return `// app/admin/kb/[[...kabanos]]/route.ts\\nimport config from '@/kanban.config';\\nimport { createKabanos } from 'kabanos';\\nimport { createNextHandler } from 'kabanos/next';\\n\\nconst handler = createNextHandler(await createKabanos(config));\\nexport { handler as GET, handler as POST, handler as PATCH, handler as PUT };`;\n return `import { createServer } from 'node:http';\\nimport config from './kanban.config.js';\\nimport { createKabanos } from 'kabanos';\\nimport { createNodeHandler } from 'kabanos/node';\\n\\nconst kabanos = await createKabanos(config);\\ncreateServer(createNodeHandler(kabanos)).listen(3000);`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,156 @@
1
+ import { IncomingMessage } from 'node:http';
2
+
3
+ type ThemeMode = 'light' | 'dark' | 'system';
4
+ type Actor = {
5
+ id: string;
6
+ name?: string;
7
+ };
8
+ type CardStatus = 'open' | 'resolved' | 'archived' | 'changed' | 'orphaned';
9
+ interface ColumnConfig {
10
+ id: string;
11
+ name: string;
12
+ completion?: boolean;
13
+ colorToken?: string;
14
+ }
15
+ interface TagConfig {
16
+ column: string;
17
+ priority?: 'low' | 'normal' | 'high';
18
+ label?: string;
19
+ }
20
+ interface ThemeTokens {
21
+ canvas: string;
22
+ canvasSoft: string;
23
+ surface: string;
24
+ ink: string;
25
+ inkMuted: string;
26
+ hairline: string;
27
+ primary: string;
28
+ primaryActive: string;
29
+ }
30
+ interface AuthConfig<T = any> {
31
+ enabled?: boolean;
32
+ guard?: (request: T) => boolean | Promise<boolean>;
33
+ actor?: (request: T) => Actor | undefined | Promise<Actor | undefined>;
34
+ }
35
+ interface StorageConfig {
36
+ adapter?: 'sqlite' | 'postgres' | 'mysql';
37
+ connectionString?: string;
38
+ filename?: string;
39
+ }
40
+ interface ScanConfig {
41
+ include?: string[];
42
+ exclude?: string[];
43
+ tags?: Record<string, TagConfig>;
44
+ watch?: boolean;
45
+ intervalMs?: number;
46
+ maxFileSize?: number;
47
+ parsers?: CommentParser[];
48
+ }
49
+ interface CommentParser {
50
+ extensions: string[];
51
+ parse(filePath: string, source: string, tags: string[]): SourceComment[];
52
+ }
53
+ interface KanbanConfig<TRequest = any> {
54
+ projectRoot?: string;
55
+ mountPath?: string;
56
+ boardName?: string;
57
+ columns?: Array<string | ColumnConfig>;
58
+ scan?: ScanConfig;
59
+ storage?: StorageConfig;
60
+ theme?: {
61
+ default?: ThemeMode;
62
+ overrides?: Partial<ThemeTokens>;
63
+ };
64
+ auth?: AuthConfig<TRequest>;
65
+ }
66
+ interface SourceComment {
67
+ filePath: string;
68
+ line: number;
69
+ endLine: number;
70
+ tag: string;
71
+ text: string;
72
+ rawText: string;
73
+ contentHash: string;
74
+ contextHash: string;
75
+ occurrence: number;
76
+ }
77
+ interface Card {
78
+ id: string;
79
+ columnId: string;
80
+ position: number;
81
+ tag: string;
82
+ title: string;
83
+ body: string;
84
+ notes: string;
85
+ assignee?: string;
86
+ labels: string[];
87
+ priority: string;
88
+ sourceFilePath: string;
89
+ sourceLine: number;
90
+ sourceContentHash: string;
91
+ sourceContextHash: string;
92
+ sourceOccurrence: number;
93
+ status: CardStatus;
94
+ createdAt: string;
95
+ updatedAt: string;
96
+ }
97
+ interface BoardState {
98
+ id: string;
99
+ name: string;
100
+ columns: Array<ColumnConfig & {
101
+ position: number;
102
+ }>;
103
+ cards: Card[];
104
+ settings: Record<string, unknown>;
105
+ }
106
+ type NodeRequest = IncomingMessage;
107
+ interface StorageAdapter {
108
+ migrate(): Promise<void>;
109
+ initialize(config: ResolvedKanbanConfig): Promise<void>;
110
+ getBoard(): Promise<BoardState>;
111
+ reconcile(comments: SourceComment[]): Promise<{
112
+ created: number;
113
+ updated: number;
114
+ archived: number;
115
+ }>;
116
+ moveCard(id: string, columnId: string, position: number, actor?: Actor): Promise<void>;
117
+ updateCard(id: string, patch: {
118
+ notes?: string;
119
+ assignee?: string | null;
120
+ labels?: string[];
121
+ }, actor?: Actor): Promise<void>;
122
+ getActivity(cardId: string): Promise<unknown[]>;
123
+ updateSettings(patch: Record<string, unknown>): Promise<void>;
124
+ replaceColumns(columns: ColumnConfig[], destinationColumnId?: string): Promise<void>;
125
+ close(): Promise<void>;
126
+ }
127
+ interface ResolvedKanbanConfig extends Required<Omit<KanbanConfig, 'auth' | 'theme' | 'storage' | 'scan' | 'columns'>> {
128
+ columns: ColumnConfig[];
129
+ scan: Required<Omit<ScanConfig, 'parsers'>> & {
130
+ parsers: CommentParser[];
131
+ };
132
+ storage: Required<StorageConfig>;
133
+ theme: {
134
+ default: ThemeMode;
135
+ overrides: Partial<ThemeTokens>;
136
+ };
137
+ auth: AuthConfig;
138
+ }
139
+
140
+ declare function defineConfig<T = any>(config: KanbanConfig<T>): KanbanConfig<T>;
141
+ declare function resolveConfig(input?: KanbanConfig): ResolvedKanbanConfig;
142
+
143
+ interface KabanosInstance {
144
+ config: ReturnType<typeof resolveConfig>;
145
+ storage: StorageAdapter;
146
+ handler(request: Request, nativeRequest?: unknown): Promise<Response>;
147
+ scan(): Promise<{
148
+ created: number;
149
+ updated: number;
150
+ archived: number;
151
+ }>;
152
+ close(): Promise<void>;
153
+ }
154
+ declare function createKabanos<TRequest = any>(input: KanbanConfig<TRequest>): Promise<KabanosInstance>;
155
+
156
+ export { type Actor as A, type BoardState as B, type ColumnConfig as C, type KabanosInstance as K, type NodeRequest as N, type ResolvedKanbanConfig as R, type SourceComment as S, type TagConfig as T, type StorageAdapter as a, type AuthConfig as b, type Card as c, type CardStatus as d, type CommentParser as e, type KanbanConfig as f, type ScanConfig as g, type StorageConfig as h, type ThemeMode as i, type ThemeTokens as j, createKabanos as k, defineConfig as l, resolveConfig as r };