kanban-lite 1.0.4

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 (99) hide show
  1. package/.editorconfig +9 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/release.yml +75 -0
  4. package/.prettierignore +6 -0
  5. package/.prettierrc.yaml +4 -0
  6. package/.vscode/extensions.json +3 -0
  7. package/.vscode/launch.json +17 -0
  8. package/.vscode/settings.json +21 -0
  9. package/.vscode/tasks.json +22 -0
  10. package/.vscodeignore +11 -0
  11. package/CHANGELOG.md +184 -0
  12. package/CLAUDE.md +58 -0
  13. package/CONTRIBUTING.md +114 -0
  14. package/LICENSE +22 -0
  15. package/README.md +482 -0
  16. package/SKILL.md +237 -0
  17. package/dist/cli.js +8716 -0
  18. package/dist/extension.js +8463 -0
  19. package/dist/mcp-server.js +1327 -0
  20. package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
  21. package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
  22. package/dist/standalone-webview/index.js +85 -0
  23. package/dist/standalone-webview/index.js.map +1 -0
  24. package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
  25. package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
  26. package/dist/standalone-webview/style.css +1 -0
  27. package/dist/standalone.js +7513 -0
  28. package/dist/webview/icons-Dx9MGYqN.js +180 -0
  29. package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
  30. package/dist/webview/index.js +85 -0
  31. package/dist/webview/index.js.map +1 -0
  32. package/dist/webview/react-vendor-DkYdDBET.js +25 -0
  33. package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
  34. package/dist/webview/style.css +1 -0
  35. package/docs/images/board-overview.png +0 -0
  36. package/docs/images/editor-view.png +0 -0
  37. package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
  38. package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
  39. package/eslint.config.mjs +31 -0
  40. package/package.json +161 -0
  41. package/postcss.config.js +6 -0
  42. package/resources/icon-light.png +0 -0
  43. package/resources/icon-light.svg +105 -0
  44. package/resources/icon.png +0 -0
  45. package/resources/icon.svg +105 -0
  46. package/resources/kanban-dark.svg +21 -0
  47. package/resources/kanban-light.svg +21 -0
  48. package/resources/kanban.svg +21 -0
  49. package/src/cli/index.ts +846 -0
  50. package/src/extension/FeatureHeaderProvider.ts +370 -0
  51. package/src/extension/KanbanPanel.ts +973 -0
  52. package/src/extension/SidebarViewProvider.ts +507 -0
  53. package/src/extension/featureFileUtils.ts +82 -0
  54. package/src/extension/index.ts +234 -0
  55. package/src/mcp-server/index.ts +632 -0
  56. package/src/sdk/KanbanSDK.ts +349 -0
  57. package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
  58. package/src/sdk/__tests__/parser.test.ts +170 -0
  59. package/src/sdk/fileUtils.ts +76 -0
  60. package/src/sdk/index.ts +6 -0
  61. package/src/sdk/parser.ts +70 -0
  62. package/src/sdk/types.ts +15 -0
  63. package/src/shared/config.ts +113 -0
  64. package/src/shared/editorTypes.ts +14 -0
  65. package/src/shared/types.ts +120 -0
  66. package/src/standalone/__tests__/server.integration.test.ts +1916 -0
  67. package/src/standalone/__tests__/webhooks.test.ts +357 -0
  68. package/src/standalone/fileUtils.ts +70 -0
  69. package/src/standalone/index.ts +71 -0
  70. package/src/standalone/server.ts +1046 -0
  71. package/src/standalone/webhooks.ts +135 -0
  72. package/src/webview/App.tsx +469 -0
  73. package/src/webview/assets/main.css +329 -0
  74. package/src/webview/assets/standalone-theme.css +130 -0
  75. package/src/webview/components/ColumnDialog.tsx +119 -0
  76. package/src/webview/components/CreateFeatureDialog.tsx +524 -0
  77. package/src/webview/components/DatePicker.tsx +185 -0
  78. package/src/webview/components/FeatureCard.tsx +186 -0
  79. package/src/webview/components/FeatureEditor.tsx +623 -0
  80. package/src/webview/components/KanbanBoard.tsx +144 -0
  81. package/src/webview/components/KanbanColumn.tsx +159 -0
  82. package/src/webview/components/MarkdownEditor.tsx +291 -0
  83. package/src/webview/components/PrioritySelect.tsx +39 -0
  84. package/src/webview/components/QuickAddInput.tsx +72 -0
  85. package/src/webview/components/SettingsPanel.tsx +284 -0
  86. package/src/webview/components/Toolbar.tsx +175 -0
  87. package/src/webview/components/UndoToast.tsx +70 -0
  88. package/src/webview/index.html +12 -0
  89. package/src/webview/lib/utils.ts +6 -0
  90. package/src/webview/main.tsx +11 -0
  91. package/src/webview/standalone-main.tsx +13 -0
  92. package/src/webview/standalone-shim.ts +132 -0
  93. package/src/webview/standalone.html +12 -0
  94. package/src/webview/store/index.ts +241 -0
  95. package/tailwind.config.js +53 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +36 -0
  98. package/vite.standalone.config.ts +62 -0
  99. package/vitest.config.ts +15 -0
@@ -0,0 +1,76 @@
1
+ import * as path from 'path'
2
+ import * as fs from 'fs/promises'
3
+
4
+ export function getFeatureFilePath(featuresDir: string, status: string, filename: string): string {
5
+ return path.join(featuresDir, status, `${filename}.md`)
6
+ }
7
+
8
+ export async function ensureDirectories(featuresDir: string): Promise<void> {
9
+ await fs.mkdir(featuresDir, { recursive: true })
10
+ }
11
+
12
+ export async function moveFeatureFile(
13
+ currentPath: string,
14
+ featuresDir: string,
15
+ newStatus: string,
16
+ attachments?: string[]
17
+ ): Promise<string> {
18
+ const filename = path.basename(currentPath)
19
+ const targetDir = path.join(featuresDir, newStatus)
20
+ let targetPath = path.join(targetDir, filename)
21
+
22
+ if (currentPath === targetPath) return currentPath
23
+
24
+ const ext = path.extname(filename)
25
+ const base = path.basename(filename, ext)
26
+ let counter = 1
27
+ while (await fileExists(targetPath)) {
28
+ targetPath = path.join(targetDir, `${base}-${counter}${ext}`)
29
+ counter++
30
+ }
31
+
32
+ await fs.mkdir(targetDir, { recursive: true })
33
+ await fs.rename(currentPath, targetPath)
34
+
35
+ if (attachments && attachments.length > 0) {
36
+ const sourceDir = path.dirname(currentPath)
37
+ for (const attachment of attachments) {
38
+ const srcAttach = path.join(sourceDir, attachment)
39
+ const destAttach = path.join(targetDir, attachment)
40
+ try {
41
+ await fs.access(srcAttach)
42
+ await fs.rename(srcAttach, destAttach)
43
+ } catch {
44
+ // Best effort -- skip failed attachment moves
45
+ }
46
+ }
47
+ }
48
+
49
+ return targetPath
50
+ }
51
+
52
+ export async function renameFeatureFile(currentPath: string, newFilename: string): Promise<string> {
53
+ const dir = path.dirname(currentPath)
54
+ const newPath = path.join(dir, `${newFilename}.md`)
55
+ if (currentPath === newPath) return currentPath
56
+ await fs.rename(currentPath, newPath)
57
+ return newPath
58
+ }
59
+
60
+ export function getStatusFromPath(filePath: string, featuresDir: string): string | null {
61
+ const relative = path.relative(featuresDir, filePath)
62
+ const parts = relative.split(path.sep)
63
+ if (parts.length === 2) {
64
+ return parts[0]
65
+ }
66
+ return null
67
+ }
68
+
69
+ async function fileExists(filePath: string): Promise<boolean> {
70
+ try {
71
+ await fs.access(filePath)
72
+ return true
73
+ } catch {
74
+ return false
75
+ }
76
+ }
@@ -0,0 +1,6 @@
1
+ export { KanbanSDK } from './KanbanSDK'
2
+ export { parseFeatureFile, serializeFeature } from './parser'
3
+ export { getFeatureFilePath, ensureDirectories, moveFeatureFile, getStatusFromPath } from './fileUtils'
4
+ export type { CreateCardInput, BoardConfig } from './types'
5
+ export type { Feature, FeatureStatus, Priority, KanbanColumn } from '../shared/types'
6
+ export { getTitleFromContent, generateFeatureFilename, DEFAULT_COLUMNS } from '../shared/types'
@@ -0,0 +1,70 @@
1
+ import * as path from 'path'
2
+ import type { Feature, FeatureStatus, Priority } from '../shared/types'
3
+
4
+ function extractIdFromFilename(filePath: string): string {
5
+ const basename = path.basename(filePath, '.md')
6
+ // New format: "42-some-slug" → "42"
7
+ const numericMatch = basename.match(/^(\d+)-/)
8
+ if (numericMatch) return numericMatch[1]
9
+ // Legacy format: full basename is the ID
10
+ return basename
11
+ }
12
+
13
+ export function parseFeatureFile(content: string, filePath: string): Feature | null {
14
+ content = content.replace(/\r\n/g, '\n')
15
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
16
+ if (!frontmatterMatch) return null
17
+
18
+ const frontmatter = frontmatterMatch[1]
19
+ const body = frontmatterMatch[2] || ''
20
+
21
+ const getValue = (key: string): string => {
22
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))
23
+ if (!match) return ''
24
+ const value = match[1].trim().replace(/^["']|["']$/g, '')
25
+ return value === 'null' ? '' : value
26
+ }
27
+
28
+ const getArrayValue = (key: string): string[] => {
29
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, 'm'))
30
+ if (!match) return []
31
+ return match[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
32
+ }
33
+
34
+ return {
35
+ id: getValue('id') || extractIdFromFilename(filePath),
36
+ status: (getValue('status') as FeatureStatus) || 'backlog',
37
+ priority: (getValue('priority') as Priority) || 'medium',
38
+ assignee: getValue('assignee') || null,
39
+ dueDate: getValue('dueDate') || null,
40
+ created: getValue('created') || new Date().toISOString(),
41
+ modified: getValue('modified') || new Date().toISOString(),
42
+ completedAt: getValue('completedAt') || null,
43
+ labels: getArrayValue('labels'),
44
+ attachments: getArrayValue('attachments'),
45
+ order: getValue('order') || 'a0',
46
+ content: body.trim(),
47
+ filePath
48
+ }
49
+ }
50
+
51
+ export function serializeFeature(feature: Feature): string {
52
+ const frontmatter = [
53
+ '---',
54
+ `id: "${feature.id}"`,
55
+ `status: "${feature.status}"`,
56
+ `priority: "${feature.priority}"`,
57
+ `assignee: ${feature.assignee ? `"${feature.assignee}"` : 'null'}`,
58
+ `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : 'null'}`,
59
+ `created: "${feature.created}"`,
60
+ `modified: "${feature.modified}"`,
61
+ `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : 'null'}`,
62
+ `labels: [${feature.labels.map(l => `"${l}"`).join(', ')}]`,
63
+ `attachments: [${(feature.attachments || []).map(a => `"${a}"`).join(', ')}]`,
64
+ `order: "${feature.order}"`,
65
+ '---',
66
+ ''
67
+ ].join('\n')
68
+
69
+ return frontmatter + feature.content
70
+ }
@@ -0,0 +1,15 @@
1
+ import type { FeatureStatus, KanbanColumn, Priority } from '../shared/types'
2
+
3
+ export interface CreateCardInput {
4
+ content: string
5
+ status?: FeatureStatus
6
+ priority?: Priority
7
+ assignee?: string | null
8
+ dueDate?: string | null
9
+ labels?: string[]
10
+ attachments?: string[]
11
+ }
12
+
13
+ export interface BoardConfig {
14
+ columns: KanbanColumn[]
15
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import type { KanbanColumn, CardDisplaySettings, Priority, FeatureStatus } from './types'
4
+
5
+ export interface KanbanConfig {
6
+ featuresDirectory: string
7
+ defaultPriority: Priority
8
+ defaultStatus: FeatureStatus
9
+ columns: KanbanColumn[]
10
+ aiAgent: string
11
+ nextCardId: number
12
+ showPriorityBadges: boolean
13
+ showAssignee: boolean
14
+ showDueDate: boolean
15
+ showLabels: boolean
16
+ showBuildWithAI: boolean
17
+ showFileName: boolean
18
+ compactMode: boolean
19
+ markdownEditorMode: boolean
20
+ }
21
+
22
+ export const DEFAULT_CONFIG: KanbanConfig = {
23
+ featuresDirectory: '.kanban',
24
+ defaultPriority: 'medium',
25
+ defaultStatus: 'backlog',
26
+ columns: [
27
+ { id: 'backlog', name: 'Backlog', color: '#6b7280' },
28
+ { id: 'todo', name: 'To Do', color: '#3b82f6' },
29
+ { id: 'in-progress', name: 'In Progress', color: '#f59e0b' },
30
+ { id: 'review', name: 'Review', color: '#8b5cf6' },
31
+ { id: 'done', name: 'Done', color: '#22c55e' }
32
+ ],
33
+ aiAgent: 'claude',
34
+ nextCardId: 1,
35
+ showPriorityBadges: true,
36
+ showAssignee: true,
37
+ showDueDate: true,
38
+ showLabels: true,
39
+ showBuildWithAI: true,
40
+ showFileName: false,
41
+ compactMode: false,
42
+ markdownEditorMode: false
43
+ }
44
+
45
+ export const CONFIG_FILENAME = '.kanban.json'
46
+
47
+ export function configPath(workspaceRoot: string): string {
48
+ return path.join(workspaceRoot, CONFIG_FILENAME)
49
+ }
50
+
51
+ export function readConfig(workspaceRoot: string): KanbanConfig {
52
+ const filePath = configPath(workspaceRoot)
53
+ try {
54
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
55
+ return { ...DEFAULT_CONFIG, ...raw }
56
+ } catch {
57
+ return { ...DEFAULT_CONFIG }
58
+ }
59
+ }
60
+
61
+ export function writeConfig(workspaceRoot: string, config: KanbanConfig): void {
62
+ const filePath = configPath(workspaceRoot)
63
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
64
+ }
65
+
66
+ /** Read and increment the nextCardId counter, returning the allocated ID */
67
+ export function allocateCardId(workspaceRoot: string): number {
68
+ const config = readConfig(workspaceRoot)
69
+ const id = config.nextCardId
70
+ writeConfig(workspaceRoot, { ...config, nextCardId: id + 1 })
71
+ return id
72
+ }
73
+
74
+ /** Ensure nextCardId is ahead of all existing numeric IDs */
75
+ export function syncCardIdCounter(workspaceRoot: string, existingIds: number[]): void {
76
+ if (existingIds.length === 0) return
77
+ const maxId = Math.max(...existingIds)
78
+ const config = readConfig(workspaceRoot)
79
+ if (config.nextCardId <= maxId) {
80
+ writeConfig(workspaceRoot, { ...config, nextCardId: maxId + 1 })
81
+ }
82
+ }
83
+
84
+ /** Extract CardDisplaySettings from a KanbanConfig */
85
+ export function configToSettings(config: KanbanConfig): CardDisplaySettings {
86
+ return {
87
+ showPriorityBadges: config.showPriorityBadges,
88
+ showAssignee: config.showAssignee,
89
+ showDueDate: config.showDueDate,
90
+ showLabels: config.showLabels,
91
+ showBuildWithAI: config.showBuildWithAI,
92
+ showFileName: config.showFileName,
93
+ compactMode: config.compactMode,
94
+ markdownEditorMode: config.markdownEditorMode,
95
+ defaultPriority: config.defaultPriority,
96
+ defaultStatus: config.defaultStatus
97
+ }
98
+ }
99
+
100
+ /** Merge CardDisplaySettings back into a KanbanConfig */
101
+ export function settingsToConfig(config: KanbanConfig, settings: CardDisplaySettings): KanbanConfig {
102
+ return {
103
+ ...config,
104
+ showPriorityBadges: settings.showPriorityBadges,
105
+ showAssignee: settings.showAssignee,
106
+ showDueDate: settings.showDueDate,
107
+ showLabels: settings.showLabels,
108
+ showFileName: settings.showFileName,
109
+ compactMode: settings.compactMode,
110
+ defaultPriority: settings.defaultPriority,
111
+ defaultStatus: settings.defaultStatus
112
+ }
113
+ }
@@ -0,0 +1,14 @@
1
+ import type { FeatureFrontmatter } from './types'
2
+
3
+ export type { FeatureFrontmatter }
4
+
5
+ // Messages from the extension to the editor webview
6
+ export type EditorExtensionMessage =
7
+ | { type: 'init'; content: string; frontmatter: FeatureFrontmatter | null; fileName: string }
8
+
9
+ // Messages from the editor webview to the extension
10
+ export type EditorWebviewMessage =
11
+ | { type: 'ready' }
12
+ | { type: 'frontmatterUpdate'; frontmatter: FeatureFrontmatter }
13
+ | { type: 'requestSave' }
14
+ | { type: 'startWithAI'; agent?: 'claude' | 'codex' | 'opencode'; permissionMode?: 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions' }
@@ -0,0 +1,120 @@
1
+ // Kanban types
2
+
3
+ export type Priority = 'critical' | 'high' | 'medium' | 'low'
4
+ export type FeatureStatus = 'backlog' | 'todo' | 'in-progress' | 'review' | 'done'
5
+
6
+ export interface Feature {
7
+ id: string
8
+ status: FeatureStatus
9
+ priority: Priority
10
+ assignee: string | null
11
+ dueDate: string | null
12
+ created: string
13
+ modified: string
14
+ completedAt: string | null
15
+ labels: string[]
16
+ attachments: string[]
17
+ order: string
18
+ content: string
19
+ filePath: string
20
+ }
21
+
22
+ // Parse title from the first # heading in markdown content, falling back to the first line
23
+ export function getTitleFromContent(content: string): string {
24
+ const match = content.match(/^#\s+(.+)$/m)
25
+ if (match) return match[1].trim()
26
+ const firstLine = content.split('\n').map(l => l.trim()).find(l => l.length > 0)
27
+ return firstLine || 'Untitled'
28
+ }
29
+
30
+ // Generate a filename-safe slug from a title
31
+ export function generateSlug(title: string): string {
32
+ return title
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
35
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
36
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
37
+ .replace(/^-|-$/g, '') // Trim hyphens from start/end
38
+ .slice(0, 50) || 'feature' // Limit length, fallback
39
+ }
40
+
41
+ // Generate a filename from an incremental ID and a title
42
+ export function generateFeatureFilename(id: number, title: string): string {
43
+ const slug = generateSlug(title)
44
+ return `${id}-${slug}`
45
+ }
46
+
47
+ // Extract the numeric ID prefix from a filename or ID string like "42-build-dashboard"
48
+ export function extractNumericId(filenameOrId: string): number | null {
49
+ const match = filenameOrId.match(/^(\d+)(?:-|$)/)
50
+ return match ? parseInt(match[1], 10) : null
51
+ }
52
+
53
+ export interface KanbanColumn {
54
+ id: string
55
+ name: string
56
+ color: string
57
+ }
58
+
59
+ export const DEFAULT_COLUMNS: KanbanColumn[] = [
60
+ { id: 'backlog', name: 'Backlog', color: '#6b7280' },
61
+ { id: 'todo', name: 'To Do', color: '#3b82f6' },
62
+ { id: 'in-progress', name: 'In Progress', color: '#f59e0b' },
63
+ { id: 'review', name: 'Review', color: '#8b5cf6' },
64
+ { id: 'done', name: 'Done', color: '#22c55e' }
65
+ ]
66
+
67
+ export interface CardDisplaySettings {
68
+ showPriorityBadges: boolean
69
+ showAssignee: boolean
70
+ showDueDate: boolean
71
+ showLabels: boolean
72
+ showBuildWithAI: boolean
73
+ showFileName: boolean
74
+ compactMode: boolean
75
+ markdownEditorMode: boolean
76
+ defaultPriority: Priority
77
+ defaultStatus: FeatureStatus
78
+ }
79
+
80
+ // Messages between extension and webview
81
+ export type ExtensionMessage =
82
+ | { type: 'init'; features: Feature[]; columns: KanbanColumn[]; settings: CardDisplaySettings }
83
+ | { type: 'featuresUpdated'; features: Feature[] }
84
+ | { type: 'triggerCreateDialog' }
85
+ | { type: 'featureContent'; featureId: string; content: string; frontmatter: FeatureFrontmatter }
86
+ | { type: 'showSettings'; settings: CardDisplaySettings }
87
+
88
+ // Frontmatter for editing
89
+ export interface FeatureFrontmatter {
90
+ id: string
91
+ status: FeatureStatus
92
+ priority: Priority
93
+ assignee: string | null
94
+ dueDate: string | null
95
+ created: string
96
+ modified: string
97
+ completedAt: string | null
98
+ labels: string[]
99
+ attachments: string[]
100
+ order: string
101
+ }
102
+
103
+ export type WebviewMessage =
104
+ | { type: 'ready' }
105
+ | { type: 'createFeature'; data: { status: FeatureStatus; priority: Priority; content: string; assignee: string | null; dueDate: string | null; labels: string[] } }
106
+ | { type: 'moveFeature'; featureId: string; newStatus: string; newOrder: number }
107
+ | { type: 'deleteFeature'; featureId: string }
108
+ | { type: 'updateFeature'; featureId: string; updates: Partial<Feature> }
109
+ | { type: 'openFeature'; featureId: string }
110
+ | { type: 'saveFeatureContent'; featureId: string; content: string; frontmatter: FeatureFrontmatter }
111
+ | { type: 'closeFeature' }
112
+ | { type: 'openFile'; featureId: string }
113
+ | { type: 'addAttachment'; featureId: string }
114
+ | { type: 'openAttachment'; featureId: string; attachment: string }
115
+ | { type: 'removeAttachment'; featureId: string; attachment: string }
116
+ | { type: 'openSettings' }
117
+ | { type: 'saveSettings'; settings: CardDisplaySettings }
118
+ | { type: 'addColumn'; column: { name: string; color: string } }
119
+ | { type: 'editColumn'; columnId: string; updates: { name: string; color: string } }
120
+ | { type: 'removeColumn'; columnId: string }