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.
- package/.editorconfig +9 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/release.yml +75 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +4 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/settings.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +11 -0
- package/CHANGELOG.md +184 -0
- package/CLAUDE.md +58 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +22 -0
- package/README.md +482 -0
- package/SKILL.md +237 -0
- package/dist/cli.js +8716 -0
- package/dist/extension.js +8463 -0
- package/dist/mcp-server.js +1327 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/standalone-webview/index.js +85 -0
- package/dist/standalone-webview/index.js.map +1 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/standalone-webview/style.css +1 -0
- package/dist/standalone.js +7513 -0
- package/dist/webview/icons-Dx9MGYqN.js +180 -0
- package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/webview/index.js +85 -0
- package/dist/webview/index.js.map +1 -0
- package/dist/webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/webview/style.css +1 -0
- package/docs/images/board-overview.png +0 -0
- package/docs/images/editor-view.png +0 -0
- package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
- package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
- package/eslint.config.mjs +31 -0
- package/package.json +161 -0
- package/postcss.config.js +6 -0
- package/resources/icon-light.png +0 -0
- package/resources/icon-light.svg +105 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +105 -0
- package/resources/kanban-dark.svg +21 -0
- package/resources/kanban-light.svg +21 -0
- package/resources/kanban.svg +21 -0
- package/src/cli/index.ts +846 -0
- package/src/extension/FeatureHeaderProvider.ts +370 -0
- package/src/extension/KanbanPanel.ts +973 -0
- package/src/extension/SidebarViewProvider.ts +507 -0
- package/src/extension/featureFileUtils.ts +82 -0
- package/src/extension/index.ts +234 -0
- package/src/mcp-server/index.ts +632 -0
- package/src/sdk/KanbanSDK.ts +349 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
- package/src/sdk/__tests__/parser.test.ts +170 -0
- package/src/sdk/fileUtils.ts +76 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/parser.ts +70 -0
- package/src/sdk/types.ts +15 -0
- package/src/shared/config.ts +113 -0
- package/src/shared/editorTypes.ts +14 -0
- package/src/shared/types.ts +120 -0
- package/src/standalone/__tests__/server.integration.test.ts +1916 -0
- package/src/standalone/__tests__/webhooks.test.ts +357 -0
- package/src/standalone/fileUtils.ts +70 -0
- package/src/standalone/index.ts +71 -0
- package/src/standalone/server.ts +1046 -0
- package/src/standalone/webhooks.ts +135 -0
- package/src/webview/App.tsx +469 -0
- package/src/webview/assets/main.css +329 -0
- package/src/webview/assets/standalone-theme.css +130 -0
- package/src/webview/components/ColumnDialog.tsx +119 -0
- package/src/webview/components/CreateFeatureDialog.tsx +524 -0
- package/src/webview/components/DatePicker.tsx +185 -0
- package/src/webview/components/FeatureCard.tsx +186 -0
- package/src/webview/components/FeatureEditor.tsx +623 -0
- package/src/webview/components/KanbanBoard.tsx +144 -0
- package/src/webview/components/KanbanColumn.tsx +159 -0
- package/src/webview/components/MarkdownEditor.tsx +291 -0
- package/src/webview/components/PrioritySelect.tsx +39 -0
- package/src/webview/components/QuickAddInput.tsx +72 -0
- package/src/webview/components/SettingsPanel.tsx +284 -0
- package/src/webview/components/Toolbar.tsx +175 -0
- package/src/webview/components/UndoToast.tsx +70 -0
- package/src/webview/index.html +12 -0
- package/src/webview/lib/utils.ts +6 -0
- package/src/webview/main.tsx +11 -0
- package/src/webview/standalone-main.tsx +13 -0
- package/src/webview/standalone-shim.ts +132 -0
- package/src/webview/standalone.html +12 -0
- package/src/webview/store/index.ts +241 -0
- package/tailwind.config.js +53 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +36 -0
- package/vite.standalone.config.ts +62 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as net from 'node:net'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { generateKeyBetween } from 'fractional-indexing'
|
|
5
|
+
import * as vscode from 'vscode'
|
|
6
|
+
|
|
7
|
+
import type { Feature, FeatureStatus, Priority } from '../shared/types'
|
|
8
|
+
import { generateFeatureFilename } from '../shared/types'
|
|
9
|
+
import { readConfig, allocateCardId } from '../shared/config'
|
|
10
|
+
import { startServer } from '../standalone/server'
|
|
11
|
+
import { ensureStatusSubfolders, getFeatureFilePath } from './featureFileUtils'
|
|
12
|
+
import { KanbanPanel } from './KanbanPanel'
|
|
13
|
+
import { SidebarViewProvider } from './SidebarViewProvider'
|
|
14
|
+
|
|
15
|
+
async function createFeatureFromPrompts(): Promise<void> {
|
|
16
|
+
const workspaceFolders = vscode.workspace.workspaceFolders
|
|
17
|
+
if (!workspaceFolders || workspaceFolders.length === 0) {
|
|
18
|
+
vscode.window.showErrorMessage('No workspace folder open')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Ask for title
|
|
23
|
+
const title = await vscode.window.showInputBox({
|
|
24
|
+
prompt: 'Feature title',
|
|
25
|
+
placeHolder: 'Enter a title for the new feature'
|
|
26
|
+
})
|
|
27
|
+
if (!title) return
|
|
28
|
+
|
|
29
|
+
// Ask for status
|
|
30
|
+
const statusItems: vscode.QuickPickItem[] = [
|
|
31
|
+
{ label: 'Backlog', description: 'Not yet planned' },
|
|
32
|
+
{ label: 'To Do', description: 'Planned for development' },
|
|
33
|
+
{ label: 'In Progress', description: 'Currently being worked on' },
|
|
34
|
+
{ label: 'Review', description: 'Ready for review' },
|
|
35
|
+
{ label: 'Done', description: 'Completed' }
|
|
36
|
+
]
|
|
37
|
+
const statusPick = await vscode.window.showQuickPick(statusItems, {
|
|
38
|
+
placeHolder: 'Select initial status'
|
|
39
|
+
})
|
|
40
|
+
if (!statusPick) return
|
|
41
|
+
|
|
42
|
+
const statusMap: Record<string, FeatureStatus> = {
|
|
43
|
+
'Backlog': 'backlog',
|
|
44
|
+
'To Do': 'todo',
|
|
45
|
+
'In Progress': 'in-progress',
|
|
46
|
+
'Review': 'review',
|
|
47
|
+
'Done': 'done'
|
|
48
|
+
}
|
|
49
|
+
const status = statusMap[statusPick.label]
|
|
50
|
+
|
|
51
|
+
// Ask for priority
|
|
52
|
+
const priorityItems: vscode.QuickPickItem[] = [
|
|
53
|
+
{ label: 'Critical', description: 'Urgent, needs immediate attention' },
|
|
54
|
+
{ label: 'High', description: 'Important, should be done soon' },
|
|
55
|
+
{ label: 'Medium', description: 'Normal priority' },
|
|
56
|
+
{ label: 'Low', description: 'Nice to have, can wait' }
|
|
57
|
+
]
|
|
58
|
+
const priorityPick = await vscode.window.showQuickPick(priorityItems, {
|
|
59
|
+
placeHolder: 'Select priority'
|
|
60
|
+
})
|
|
61
|
+
if (!priorityPick) return
|
|
62
|
+
|
|
63
|
+
const priority = priorityPick.label.toLowerCase() as Priority
|
|
64
|
+
|
|
65
|
+
// Ask for description (optional)
|
|
66
|
+
const description = await vscode.window.showInputBox({
|
|
67
|
+
prompt: 'Description (optional)',
|
|
68
|
+
placeHolder: 'Enter a description for the feature'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Create the feature file
|
|
72
|
+
const root = workspaceFolders[0].uri.fsPath
|
|
73
|
+
const kanbanConfig = readConfig(root)
|
|
74
|
+
const featuresDir = path.join(root, kanbanConfig.featuresDirectory)
|
|
75
|
+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(featuresDir))
|
|
76
|
+
await ensureStatusSubfolders(featuresDir, kanbanConfig.columns.map(c => c.id))
|
|
77
|
+
|
|
78
|
+
const numericId = allocateCardId(root)
|
|
79
|
+
const filename = generateFeatureFilename(numericId, title)
|
|
80
|
+
const now = new Date().toISOString()
|
|
81
|
+
|
|
82
|
+
// Build content with title as first # heading
|
|
83
|
+
const content = `# ${title}${description ? '\n\n' + description : ''}`
|
|
84
|
+
|
|
85
|
+
const feature: Feature = {
|
|
86
|
+
id: String(numericId),
|
|
87
|
+
status,
|
|
88
|
+
priority,
|
|
89
|
+
assignee: null,
|
|
90
|
+
dueDate: null,
|
|
91
|
+
created: now,
|
|
92
|
+
modified: now,
|
|
93
|
+
completedAt: status === 'done' ? now : null,
|
|
94
|
+
labels: [],
|
|
95
|
+
attachments: [],
|
|
96
|
+
order: generateKeyBetween(null, null),
|
|
97
|
+
content,
|
|
98
|
+
filePath: getFeatureFilePath(featuresDir, status, filename)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fileContent = serializeFeature(feature)
|
|
102
|
+
await vscode.workspace.fs.writeFile(vscode.Uri.file(feature.filePath), new TextEncoder().encode(fileContent))
|
|
103
|
+
|
|
104
|
+
// Open the created file
|
|
105
|
+
const document = await vscode.workspace.openTextDocument(feature.filePath)
|
|
106
|
+
await vscode.window.showTextDocument(document)
|
|
107
|
+
|
|
108
|
+
vscode.window.showInformationMessage(`Created feature: ${title}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function serializeFeature(feature: Feature): string {
|
|
112
|
+
const frontmatter = [
|
|
113
|
+
'---',
|
|
114
|
+
`id: "${feature.id}"`,
|
|
115
|
+
`status: "${feature.status}"`,
|
|
116
|
+
`priority: "${feature.priority}"`,
|
|
117
|
+
`assignee: ${feature.assignee ? `"${feature.assignee}"` : 'null'}`,
|
|
118
|
+
`dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : 'null'}`,
|
|
119
|
+
`created: "${feature.created}"`,
|
|
120
|
+
`modified: "${feature.modified}"`,
|
|
121
|
+
`completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : 'null'}`,
|
|
122
|
+
`labels: [${feature.labels.map(l => `"${l}"`).join(', ')}]`,
|
|
123
|
+
`order: "${feature.order}"`,
|
|
124
|
+
'---',
|
|
125
|
+
''
|
|
126
|
+
].join('\n')
|
|
127
|
+
|
|
128
|
+
return frontmatter + feature.content
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let standaloneServer: ReturnType<typeof startServer> | undefined
|
|
132
|
+
let statusBarItem: vscode.StatusBarItem | undefined
|
|
133
|
+
|
|
134
|
+
function findFreePort(startPort: number, maxAttempts = 10): Promise<number> {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
let attempt = 0
|
|
137
|
+
function tryPort(port: number) {
|
|
138
|
+
const server = net.createServer()
|
|
139
|
+
server.once('error', () => {
|
|
140
|
+
attempt++
|
|
141
|
+
if (attempt >= maxAttempts) {
|
|
142
|
+
reject(new Error('No free port found'))
|
|
143
|
+
} else {
|
|
144
|
+
tryPort(port + 1)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
server.once('listening', () => {
|
|
148
|
+
server.close(() => resolve(port))
|
|
149
|
+
})
|
|
150
|
+
server.listen(port)
|
|
151
|
+
}
|
|
152
|
+
tryPort(startPort)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
157
|
+
// Start standalone HTTP server so the board is accessible in a browser
|
|
158
|
+
const workspaceFolders = vscode.workspace.workspaceFolders
|
|
159
|
+
if (workspaceFolders) {
|
|
160
|
+
const root = workspaceFolders[0].uri.fsPath
|
|
161
|
+
const kanbanConfig = readConfig(root)
|
|
162
|
+
const featuresDir = path.join(root, kanbanConfig.featuresDirectory)
|
|
163
|
+
const webviewDir = path.join(context.extensionPath, 'dist', 'standalone-webview')
|
|
164
|
+
|
|
165
|
+
findFreePort(3464).then(port => {
|
|
166
|
+
standaloneServer = startServer(featuresDir, port, webviewDir)
|
|
167
|
+
standaloneServer.on('error', () => {
|
|
168
|
+
standaloneServer = undefined
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 50)
|
|
172
|
+
statusBarItem.text = `kanban:${port}`
|
|
173
|
+
statusBarItem.tooltip = `Kanban board running at http://localhost:${port}`
|
|
174
|
+
statusBarItem.command = {
|
|
175
|
+
title: 'Open Kanban in Browser',
|
|
176
|
+
command: 'vscode.open',
|
|
177
|
+
arguments: [vscode.Uri.parse(`http://localhost:${port}`)]
|
|
178
|
+
}
|
|
179
|
+
statusBarItem.show()
|
|
180
|
+
context.subscriptions.push(statusBarItem)
|
|
181
|
+
}).catch(() => {
|
|
182
|
+
// No free port found — non-critical, skip
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sidebar webview in the activity bar
|
|
187
|
+
const sidebarProvider = new SidebarViewProvider(context.extensionUri, context)
|
|
188
|
+
context.subscriptions.push(
|
|
189
|
+
vscode.window.registerWebviewViewProvider(SidebarViewProvider.viewType, sidebarProvider)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
context.subscriptions.push(
|
|
193
|
+
vscode.commands.registerCommand('kanban-lite.open', () => {
|
|
194
|
+
const wasOpen = !!KanbanPanel.currentPanel
|
|
195
|
+
KanbanPanel.createOrShow(context.extensionUri, context)
|
|
196
|
+
if (!wasOpen && KanbanPanel.currentPanel) {
|
|
197
|
+
sidebarProvider.setBoardOpen(true)
|
|
198
|
+
KanbanPanel.currentPanel.onDispose(() => {
|
|
199
|
+
sidebarProvider.setBoardOpen(false)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
context.subscriptions.push(
|
|
206
|
+
vscode.commands.registerCommand('kanban-lite.addFeature', () => {
|
|
207
|
+
createFeatureFromPrompts()
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// If a panel already exists, revive it
|
|
212
|
+
if (vscode.window.registerWebviewPanelSerializer) {
|
|
213
|
+
vscode.window.registerWebviewPanelSerializer(KanbanPanel.viewType, {
|
|
214
|
+
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel) {
|
|
215
|
+
KanbanPanel.revive(webviewPanel, context.extensionUri, context)
|
|
216
|
+
sidebarProvider.setBoardOpen(true)
|
|
217
|
+
KanbanPanel.currentPanel?.onDispose(() => {
|
|
218
|
+
sidebarProvider.setBoardOpen(false)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function deactivate() {
|
|
226
|
+
if (standaloneServer) {
|
|
227
|
+
standaloneServer.close()
|
|
228
|
+
standaloneServer = undefined
|
|
229
|
+
}
|
|
230
|
+
if (statusBarItem) {
|
|
231
|
+
statusBarItem.dispose()
|
|
232
|
+
statusBarItem = undefined
|
|
233
|
+
}
|
|
234
|
+
}
|