kanban-lite 1.0.9 → 1.0.13
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/dist/cli.js +1015 -407
- package/dist/extension.js +897 -347
- package/dist/mcp-server.js +550 -189
- package/dist/sdk/index.cjs +1223 -0
- package/dist/sdk/index.mjs +1168 -0
- package/dist/sdk/sdk/KanbanSDK.d.ts +39 -21
- package/dist/sdk/sdk/index.d.ts +4 -3
- package/dist/sdk/sdk/migration.d.ts +6 -0
- package/dist/sdk/sdk/types.d.ts +3 -5
- package/dist/sdk/shared/config.d.ts +23 -10
- package/dist/sdk/shared/types.d.ts +18 -4
- package/dist/standalone-webview/index.js +27 -27
- package/dist/standalone-webview/index.js.map +1 -1
- package/dist/standalone-webview/style.css +1 -1
- package/dist/standalone.js +831 -328
- package/dist/webview/index.js +45 -45
- package/dist/webview/index.js.map +1 -1
- package/dist/webview/style.css +1 -1
- package/package.json +4 -3
- package/src/cli/index.ts +217 -95
- package/src/extension/KanbanPanel.ts +49 -22
- package/src/mcp-server/index.ts +138 -62
- package/src/sdk/KanbanSDK.ts +283 -77
- package/src/sdk/__tests__/KanbanSDK.test.ts +5 -5
- package/src/sdk/__tests__/migration.test.ts +269 -0
- package/src/sdk/__tests__/multi-board.test.ts +449 -0
- package/src/sdk/index.ts +4 -3
- package/src/sdk/migration.ts +52 -0
- package/src/sdk/types.ts +3 -6
- package/src/shared/config.ts +144 -22
- package/src/shared/types.ts +14 -5
- package/src/standalone/__tests__/server.integration.test.ts +38 -37
- package/src/standalone/server.ts +239 -21
- package/src/webview/App.tsx +17 -7
- package/src/webview/components/Toolbar.tsx +99 -3
- package/src/webview/store/index.ts +11 -3
- package/.kanban/backlog/1-test1.md +0 -30
- package/.kanban.json +0 -42
package/src/shared/config.ts
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import * as fs from 'fs'
|
|
2
2
|
import * as path from 'path'
|
|
3
|
-
import type { KanbanColumn, CardDisplaySettings, Priority
|
|
3
|
+
import type { KanbanColumn, CardDisplaySettings, Priority } from './types'
|
|
4
|
+
import { DEFAULT_COLUMNS } from './types'
|
|
4
5
|
|
|
6
|
+
// Per-board configuration
|
|
7
|
+
export interface BoardConfig {
|
|
8
|
+
name: string
|
|
9
|
+
description?: string
|
|
10
|
+
columns: KanbanColumn[]
|
|
11
|
+
nextCardId: number
|
|
12
|
+
defaultStatus: string
|
|
13
|
+
defaultPriority: Priority
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// V2 config with multi-board support
|
|
5
17
|
export interface KanbanConfig {
|
|
18
|
+
version: 2
|
|
19
|
+
boards: Record<string, BoardConfig>
|
|
20
|
+
defaultBoard: string
|
|
6
21
|
featuresDirectory: string
|
|
22
|
+
aiAgent: string
|
|
23
|
+
// Global display settings (fallback defaults for defaultPriority/defaultStatus)
|
|
7
24
|
defaultPriority: Priority
|
|
8
|
-
defaultStatus:
|
|
25
|
+
defaultStatus: string
|
|
26
|
+
showPriorityBadges: boolean
|
|
27
|
+
showAssignee: boolean
|
|
28
|
+
showDueDate: boolean
|
|
29
|
+
showLabels: boolean
|
|
30
|
+
showBuildWithAI: boolean
|
|
31
|
+
showFileName: boolean
|
|
32
|
+
compactMode: boolean
|
|
33
|
+
markdownEditorMode: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Legacy v1 config (for migration)
|
|
37
|
+
interface KanbanConfigV1 {
|
|
38
|
+
version?: 1
|
|
39
|
+
featuresDirectory: string
|
|
40
|
+
defaultPriority: Priority
|
|
41
|
+
defaultStatus: string
|
|
9
42
|
columns: KanbanColumn[]
|
|
10
43
|
aiAgent: string
|
|
11
44
|
nextCardId: number
|
|
@@ -19,19 +52,24 @@ export interface KanbanConfig {
|
|
|
19
52
|
markdownEditorMode: boolean
|
|
20
53
|
}
|
|
21
54
|
|
|
55
|
+
const DEFAULT_BOARD_CONFIG: BoardConfig = {
|
|
56
|
+
name: 'Default',
|
|
57
|
+
columns: [...DEFAULT_COLUMNS],
|
|
58
|
+
nextCardId: 1,
|
|
59
|
+
defaultStatus: 'backlog',
|
|
60
|
+
defaultPriority: 'medium'
|
|
61
|
+
}
|
|
62
|
+
|
|
22
63
|
export const DEFAULT_CONFIG: KanbanConfig = {
|
|
64
|
+
version: 2,
|
|
65
|
+
boards: {
|
|
66
|
+
default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] }
|
|
67
|
+
},
|
|
68
|
+
defaultBoard: 'default',
|
|
23
69
|
featuresDirectory: '.kanban',
|
|
70
|
+
aiAgent: 'claude',
|
|
24
71
|
defaultPriority: 'medium',
|
|
25
72
|
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
73
|
showPriorityBadges: true,
|
|
36
74
|
showAssignee: true,
|
|
37
75
|
showDueDate: true,
|
|
@@ -48,12 +86,69 @@ export function configPath(workspaceRoot: string): string {
|
|
|
48
86
|
return path.join(workspaceRoot, CONFIG_FILENAME)
|
|
49
87
|
}
|
|
50
88
|
|
|
89
|
+
function migrateConfigV1ToV2(raw: Record<string, unknown>): KanbanConfig {
|
|
90
|
+
const v1Defaults: KanbanConfigV1 = {
|
|
91
|
+
featuresDirectory: '.kanban',
|
|
92
|
+
defaultPriority: 'medium',
|
|
93
|
+
defaultStatus: 'backlog',
|
|
94
|
+
columns: [...DEFAULT_COLUMNS],
|
|
95
|
+
aiAgent: 'claude',
|
|
96
|
+
nextCardId: 1,
|
|
97
|
+
showPriorityBadges: true,
|
|
98
|
+
showAssignee: true,
|
|
99
|
+
showDueDate: true,
|
|
100
|
+
showLabels: true,
|
|
101
|
+
showBuildWithAI: true,
|
|
102
|
+
showFileName: false,
|
|
103
|
+
compactMode: false,
|
|
104
|
+
markdownEditorMode: false
|
|
105
|
+
}
|
|
106
|
+
const v1 = { ...v1Defaults, ...raw } as KanbanConfigV1
|
|
107
|
+
return {
|
|
108
|
+
version: 2,
|
|
109
|
+
boards: {
|
|
110
|
+
default: {
|
|
111
|
+
name: 'Default',
|
|
112
|
+
columns: v1.columns,
|
|
113
|
+
nextCardId: v1.nextCardId,
|
|
114
|
+
defaultStatus: v1.defaultStatus,
|
|
115
|
+
defaultPriority: v1.defaultPriority
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
defaultBoard: 'default',
|
|
119
|
+
featuresDirectory: v1.featuresDirectory,
|
|
120
|
+
aiAgent: v1.aiAgent,
|
|
121
|
+
defaultPriority: v1.defaultPriority,
|
|
122
|
+
defaultStatus: v1.defaultStatus,
|
|
123
|
+
showPriorityBadges: v1.showPriorityBadges,
|
|
124
|
+
showAssignee: v1.showAssignee,
|
|
125
|
+
showDueDate: v1.showDueDate,
|
|
126
|
+
showLabels: v1.showLabels,
|
|
127
|
+
showBuildWithAI: v1.showBuildWithAI,
|
|
128
|
+
showFileName: v1.showFileName,
|
|
129
|
+
compactMode: v1.compactMode,
|
|
130
|
+
markdownEditorMode: v1.markdownEditorMode
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
51
134
|
export function readConfig(workspaceRoot: string): KanbanConfig {
|
|
52
135
|
const filePath = configPath(workspaceRoot)
|
|
53
|
-
const defaults = { ...DEFAULT_CONFIG, columns: [...
|
|
136
|
+
const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } }
|
|
54
137
|
try {
|
|
55
138
|
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
56
|
-
|
|
139
|
+
if (!raw.version || raw.version === 1) {
|
|
140
|
+
// Migrate v1 to v2 and persist
|
|
141
|
+
const v2 = migrateConfigV1ToV2(raw)
|
|
142
|
+
writeConfig(workspaceRoot, v2)
|
|
143
|
+
return v2
|
|
144
|
+
}
|
|
145
|
+
// Merge with defaults for any missing fields
|
|
146
|
+
const config = { ...defaults, ...raw }
|
|
147
|
+
// Ensure boards object exists with at least default board
|
|
148
|
+
if (!config.boards || Object.keys(config.boards).length === 0) {
|
|
149
|
+
config.boards = defaults.boards
|
|
150
|
+
}
|
|
151
|
+
return config
|
|
57
152
|
} catch {
|
|
58
153
|
return defaults
|
|
59
154
|
}
|
|
@@ -64,25 +159,52 @@ export function writeConfig(workspaceRoot: string, config: KanbanConfig): void {
|
|
|
64
159
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
|
|
65
160
|
}
|
|
66
161
|
|
|
67
|
-
/**
|
|
68
|
-
export function
|
|
162
|
+
/** Get the default board ID from config */
|
|
163
|
+
export function getDefaultBoardId(workspaceRoot: string): string {
|
|
69
164
|
const config = readConfig(workspaceRoot)
|
|
70
|
-
|
|
71
|
-
|
|
165
|
+
return config.defaultBoard
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Get board config, using default board if boardId is omitted */
|
|
169
|
+
export function getBoardConfig(workspaceRoot: string, boardId?: string): BoardConfig {
|
|
170
|
+
const config = readConfig(workspaceRoot)
|
|
171
|
+
const resolvedId = boardId || config.defaultBoard
|
|
172
|
+
const board = config.boards[resolvedId]
|
|
173
|
+
if (!board) {
|
|
174
|
+
throw new Error(`Board '${resolvedId}' not found`)
|
|
175
|
+
}
|
|
176
|
+
return board
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Read and increment the nextCardId counter for a board, returning the allocated ID */
|
|
180
|
+
export function allocateCardId(workspaceRoot: string, boardId?: string): number {
|
|
181
|
+
const config = readConfig(workspaceRoot)
|
|
182
|
+
const resolvedId = boardId || config.defaultBoard
|
|
183
|
+
const board = config.boards[resolvedId]
|
|
184
|
+
if (!board) {
|
|
185
|
+
throw new Error(`Board '${resolvedId}' not found`)
|
|
186
|
+
}
|
|
187
|
+
const id = board.nextCardId
|
|
188
|
+
board.nextCardId = id + 1
|
|
189
|
+
writeConfig(workspaceRoot, config)
|
|
72
190
|
return id
|
|
73
191
|
}
|
|
74
192
|
|
|
75
|
-
/** Ensure nextCardId is ahead of all existing numeric IDs */
|
|
76
|
-
export function syncCardIdCounter(workspaceRoot: string, existingIds: number[]): void {
|
|
193
|
+
/** Ensure nextCardId is ahead of all existing numeric IDs for a board */
|
|
194
|
+
export function syncCardIdCounter(workspaceRoot: string, boardId: string, existingIds: number[]): void {
|
|
77
195
|
if (existingIds.length === 0) return
|
|
78
196
|
const maxId = Math.max(...existingIds)
|
|
79
197
|
const config = readConfig(workspaceRoot)
|
|
80
|
-
|
|
81
|
-
|
|
198
|
+
const resolvedId = boardId || config.defaultBoard
|
|
199
|
+
const board = config.boards[resolvedId]
|
|
200
|
+
if (!board) return
|
|
201
|
+
if (board.nextCardId <= maxId) {
|
|
202
|
+
board.nextCardId = maxId + 1
|
|
203
|
+
writeConfig(workspaceRoot, config)
|
|
82
204
|
}
|
|
83
205
|
}
|
|
84
206
|
|
|
85
|
-
/** Extract CardDisplaySettings from a KanbanConfig */
|
|
207
|
+
/** Extract CardDisplaySettings from a KanbanConfig (global settings + fallback defaults) */
|
|
86
208
|
export function configToSettings(config: KanbanConfig): CardDisplaySettings {
|
|
87
209
|
return {
|
|
88
210
|
showPriorityBadges: config.showPriorityBadges,
|
package/src/shared/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Kanban types
|
|
2
2
|
|
|
3
3
|
export type Priority = 'critical' | 'high' | 'medium' | 'low'
|
|
4
|
-
export type FeatureStatus =
|
|
4
|
+
export type FeatureStatus = string
|
|
5
5
|
|
|
6
6
|
export interface Comment {
|
|
7
7
|
id: string
|
|
@@ -12,6 +12,7 @@ export interface Comment {
|
|
|
12
12
|
|
|
13
13
|
export interface Feature {
|
|
14
14
|
id: string
|
|
15
|
+
boardId?: string
|
|
15
16
|
status: FeatureStatus
|
|
16
17
|
priority: Priority
|
|
17
18
|
assignee: string | null
|
|
@@ -27,6 +28,12 @@ export interface Feature {
|
|
|
27
28
|
filePath: string
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface BoardInfo {
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
description?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
// Parse title from the first # heading in markdown content, falling back to the first line
|
|
31
38
|
export function getTitleFromContent(content: string): string {
|
|
32
39
|
const match = content.match(/^#\s+(.+)$/m)
|
|
@@ -82,12 +89,12 @@ export interface CardDisplaySettings {
|
|
|
82
89
|
compactMode: boolean
|
|
83
90
|
markdownEditorMode: boolean
|
|
84
91
|
defaultPriority: Priority
|
|
85
|
-
defaultStatus:
|
|
92
|
+
defaultStatus: string
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
// Messages between extension and webview
|
|
89
96
|
export type ExtensionMessage =
|
|
90
|
-
| { type: 'init'; features: Feature[]; columns: KanbanColumn[]; settings: CardDisplaySettings }
|
|
97
|
+
| { type: 'init'; features: Feature[]; columns: KanbanColumn[]; settings: CardDisplaySettings; boards?: BoardInfo[]; currentBoard?: string }
|
|
91
98
|
| { type: 'featuresUpdated'; features: Feature[] }
|
|
92
99
|
| { type: 'triggerCreateDialog' }
|
|
93
100
|
| { type: 'featureContent'; featureId: string; content: string; frontmatter: FeatureFrontmatter; comments: Comment[] }
|
|
@@ -96,7 +103,7 @@ export type ExtensionMessage =
|
|
|
96
103
|
// Frontmatter for editing
|
|
97
104
|
export interface FeatureFrontmatter {
|
|
98
105
|
id: string
|
|
99
|
-
status:
|
|
106
|
+
status: string
|
|
100
107
|
priority: Priority
|
|
101
108
|
assignee: string | null
|
|
102
109
|
dueDate: string | null
|
|
@@ -110,7 +117,7 @@ export interface FeatureFrontmatter {
|
|
|
110
117
|
|
|
111
118
|
export type WebviewMessage =
|
|
112
119
|
| { type: 'ready' }
|
|
113
|
-
| { type: 'createFeature'; data: { status:
|
|
120
|
+
| { type: 'createFeature'; data: { status: string; priority: Priority; content: string; assignee: string | null; dueDate: string | null; labels: string[] } }
|
|
114
121
|
| { type: 'moveFeature'; featureId: string; newStatus: string; newOrder: number }
|
|
115
122
|
| { type: 'deleteFeature'; featureId: string }
|
|
116
123
|
| { type: 'updateFeature'; featureId: string; updates: Partial<Feature> }
|
|
@@ -129,3 +136,5 @@ export type WebviewMessage =
|
|
|
129
136
|
| { type: 'addComment'; featureId: string; author: string; content: string }
|
|
130
137
|
| { type: 'updateComment'; featureId: string; commentId: string; content: string }
|
|
131
138
|
| { type: 'deleteComment'; featureId: string; commentId: string }
|
|
139
|
+
| { type: 'switchBoard'; boardId: string }
|
|
140
|
+
| { type: 'createBoard'; name: string }
|
|
@@ -12,8 +12,9 @@ function createTempDir(): string {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Helper: write a feature markdown file
|
|
15
|
+
// Files are stored under boards/default/{status}/ in the multi-board layout
|
|
15
16
|
function writeFeatureFile(dir: string, filename: string, content: string, subfolder?: string): string {
|
|
16
|
-
const targetDir = subfolder ? path.join(dir, subfolder) : dir
|
|
17
|
+
const targetDir = subfolder ? path.join(dir, 'boards', 'default', subfolder) : path.join(dir, 'boards', 'default')
|
|
17
18
|
fs.mkdirSync(targetDir, { recursive: true })
|
|
18
19
|
const filePath = path.join(targetDir, filename)
|
|
19
20
|
fs.writeFileSync(filePath, content, 'utf-8')
|
|
@@ -366,8 +367,8 @@ describe('Standalone Server Integration', () => {
|
|
|
366
367
|
expect(features[0].content).toBe('# My New Feature\n\nSome description')
|
|
367
368
|
expect(features[0].labels).toEqual(['frontend'])
|
|
368
369
|
|
|
369
|
-
// Verify file exists on disk in todo/ subfolder
|
|
370
|
-
const todoDir = path.join(tempDir, 'todo')
|
|
370
|
+
// Verify file exists on disk in boards/default/todo/ subfolder
|
|
371
|
+
const todoDir = path.join(tempDir, 'boards', 'default', 'todo')
|
|
371
372
|
const files = fs.readdirSync(todoDir).filter(f => f.endsWith('.md'))
|
|
372
373
|
expect(files.length).toBe(1)
|
|
373
374
|
|
|
@@ -402,8 +403,8 @@ describe('Standalone Server Integration', () => {
|
|
|
402
403
|
expect(features[0].status).toBe('done')
|
|
403
404
|
expect(features[0].completedAt).toBeTruthy()
|
|
404
405
|
|
|
405
|
-
// File should be in done/ subfolder
|
|
406
|
-
const doneFiles = fs.readdirSync(path.join(tempDir, 'done')).filter(f => f.endsWith('.md'))
|
|
406
|
+
// File should be in boards/default/done/ subfolder
|
|
407
|
+
const doneFiles = fs.readdirSync(path.join(tempDir, 'boards', 'default', 'done')).filter(f => f.endsWith('.md'))
|
|
407
408
|
expect(doneFiles.length).toBe(1)
|
|
408
409
|
})
|
|
409
410
|
|
|
@@ -436,7 +437,7 @@ describe('Standalone Server Integration', () => {
|
|
|
436
437
|
const backlogFeatures = features.filter(f => f.status === 'backlog')
|
|
437
438
|
expect(backlogFeatures.length).toBe(2)
|
|
438
439
|
// New feature should come after existing (order > 'a0')
|
|
439
|
-
expect(backlogFeatures[1].order > backlogFeatures[0].order).toBe(true)
|
|
440
|
+
expect((backlogFeatures[1].order as string) > (backlogFeatures[0].order as string)).toBe(true)
|
|
440
441
|
})
|
|
441
442
|
|
|
442
443
|
it('should preserve assignee and dueDate', async () => {
|
|
@@ -491,9 +492,9 @@ describe('Standalone Server Integration', () => {
|
|
|
491
492
|
const features = response.features as Array<Record<string, unknown>>
|
|
492
493
|
expect(features[0].status).toBe('in-progress')
|
|
493
494
|
|
|
494
|
-
// Verify file was moved to in-progress/ subfolder
|
|
495
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'move-me.md'))).toBe(false)
|
|
496
|
-
const fileContent = fs.readFileSync(path.join(tempDir, 'in-progress', 'move-me.md'), 'utf-8')
|
|
495
|
+
// Verify file was moved to boards/default/in-progress/ subfolder
|
|
496
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'move-me.md'))).toBe(false)
|
|
497
|
+
const fileContent = fs.readFileSync(path.join(tempDir, 'boards', 'default', 'in-progress', 'move-me.md'), 'utf-8')
|
|
497
498
|
expect(fileContent).toContain('status: "in-progress"')
|
|
498
499
|
})
|
|
499
500
|
|
|
@@ -521,9 +522,9 @@ describe('Standalone Server Integration', () => {
|
|
|
521
522
|
expect(features[0].status).toBe('done')
|
|
522
523
|
expect(features[0].completedAt).toBeTruthy()
|
|
523
524
|
|
|
524
|
-
// File should now be in done/ subfolder
|
|
525
|
-
expect(fs.existsSync(path.join(tempDir, 'review', 'finish-me.md'))).toBe(false)
|
|
526
|
-
expect(fs.existsSync(path.join(tempDir, 'done', 'finish-me.md'))).toBe(true)
|
|
525
|
+
// File should now be in boards/default/done/ subfolder
|
|
526
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'review', 'finish-me.md'))).toBe(false)
|
|
527
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'done', 'finish-me.md'))).toBe(true)
|
|
527
528
|
})
|
|
528
529
|
|
|
529
530
|
it('should move file from done/ to target status folder', async () => {
|
|
@@ -550,9 +551,9 @@ describe('Standalone Server Integration', () => {
|
|
|
550
551
|
expect(features[0].status).toBe('todo')
|
|
551
552
|
expect(features[0].completedAt).toBeNull()
|
|
552
553
|
|
|
553
|
-
// File should be in todo/ subfolder
|
|
554
|
-
expect(fs.existsSync(path.join(tempDir, 'done', 'reopen-me.md'))).toBe(false)
|
|
555
|
-
expect(fs.existsSync(path.join(tempDir, 'todo', 'reopen-me.md'))).toBe(true)
|
|
554
|
+
// File should be in boards/default/todo/ subfolder
|
|
555
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'done', 'reopen-me.md'))).toBe(false)
|
|
556
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'todo', 'reopen-me.md'))).toBe(true)
|
|
556
557
|
})
|
|
557
558
|
|
|
558
559
|
it('should compute correct fractional order between neighbors', async () => {
|
|
@@ -596,8 +597,8 @@ describe('Standalone Server Integration', () => {
|
|
|
596
597
|
expect(todoFeatures[1].id).toBe('feat-move')
|
|
597
598
|
expect(todoFeatures[2].id).toBe('feat-c')
|
|
598
599
|
// Verify order is between a0 and a2
|
|
599
|
-
expect(todoFeatures[1].order > todoFeatures[0].order).toBe(true)
|
|
600
|
-
expect(todoFeatures[1].order < todoFeatures[2].order).toBe(true)
|
|
600
|
+
expect((todoFeatures[1].order as string) > (todoFeatures[0].order as string)).toBe(true)
|
|
601
|
+
expect((todoFeatures[1].order as string) < (todoFeatures[2].order as string)).toBe(true)
|
|
601
602
|
})
|
|
602
603
|
})
|
|
603
604
|
|
|
@@ -625,7 +626,7 @@ describe('Standalone Server Integration', () => {
|
|
|
625
626
|
expect(features.length).toBe(0)
|
|
626
627
|
|
|
627
628
|
// File should be removed
|
|
628
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'delete-me.md'))).toBe(false)
|
|
629
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'delete-me.md'))).toBe(false)
|
|
629
630
|
})
|
|
630
631
|
|
|
631
632
|
it('should only delete the targeted feature', async () => {
|
|
@@ -646,7 +647,7 @@ describe('Standalone Server Integration', () => {
|
|
|
646
647
|
const features = response.features as Array<Record<string, unknown>>
|
|
647
648
|
expect(features.length).toBe(1)
|
|
648
649
|
expect(features[0].id).toBe('keep-me')
|
|
649
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'keep-me.md'))).toBe(true)
|
|
650
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'keep-me.md'))).toBe(true)
|
|
650
651
|
})
|
|
651
652
|
|
|
652
653
|
it('should handle deleting non-existent feature gracefully', async () => {
|
|
@@ -697,7 +698,7 @@ describe('Standalone Server Integration', () => {
|
|
|
697
698
|
expect(features[0].labels).toEqual(['urgent'])
|
|
698
699
|
|
|
699
700
|
// Verify persisted on disk
|
|
700
|
-
const fileContent = fs.readFileSync(path.join(tempDir, 'backlog', 'update-me.md'), 'utf-8')
|
|
701
|
+
const fileContent = fs.readFileSync(path.join(tempDir, 'boards', 'default', 'backlog', 'update-me.md'), 'utf-8')
|
|
701
702
|
expect(fileContent).toContain('priority: "critical"')
|
|
702
703
|
expect(fileContent).toContain('assignee: "alice"')
|
|
703
704
|
expect(fileContent).toContain('labels: ["urgent"]')
|
|
@@ -813,9 +814,9 @@ describe('Standalone Server Integration', () => {
|
|
|
813
814
|
expect(saved.assignee).toBe('charlie')
|
|
814
815
|
expect(saved.labels).toEqual(['updated'])
|
|
815
816
|
|
|
816
|
-
// Verify on disk — file moved from backlog/ to in-progress/
|
|
817
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'save-me.md'))).toBe(false)
|
|
818
|
-
const fileContent = fs.readFileSync(path.join(tempDir, 'in-progress', 'save-me.md'), 'utf-8')
|
|
817
|
+
// Verify on disk — file moved from boards/default/backlog/ to boards/default/in-progress/
|
|
818
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'save-me.md'))).toBe(false)
|
|
819
|
+
const fileContent = fs.readFileSync(path.join(tempDir, 'boards', 'default', 'in-progress', 'save-me.md'), 'utf-8')
|
|
819
820
|
expect(fileContent).toContain('status: "in-progress"')
|
|
820
821
|
expect(fileContent).toContain('# Save Me Updated')
|
|
821
822
|
expect(fileContent).toContain('assignee: "charlie"')
|
|
@@ -856,8 +857,8 @@ describe('Standalone Server Integration', () => {
|
|
|
856
857
|
}
|
|
857
858
|
}, 'init')
|
|
858
859
|
|
|
859
|
-
expect(fs.existsSync(path.join(tempDir, 'review', 'save-done.md'))).toBe(false)
|
|
860
|
-
expect(fs.existsSync(path.join(tempDir, 'done', 'save-done.md'))).toBe(true)
|
|
860
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'review', 'save-done.md'))).toBe(false)
|
|
861
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'done', 'save-done.md'))).toBe(true)
|
|
861
862
|
})
|
|
862
863
|
})
|
|
863
864
|
|
|
@@ -1088,8 +1089,8 @@ describe('Standalone Server Integration', () => {
|
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1090
1091
|
// Files on disk should be updated
|
|
1091
|
-
const file1 = fs.readFileSync(path.join(tempDir, 'backlog', 'legacy-1.md'), 'utf-8')
|
|
1092
|
-
const file2 = fs.readFileSync(path.join(tempDir, 'backlog', 'legacy-2.md'), 'utf-8')
|
|
1092
|
+
const file1 = fs.readFileSync(path.join(tempDir, 'boards', 'default', 'backlog', 'legacy-1.md'), 'utf-8')
|
|
1093
|
+
const file2 = fs.readFileSync(path.join(tempDir, 'boards', 'default', 'backlog', 'legacy-2.md'), 'utf-8')
|
|
1093
1094
|
const orderMatch1 = file1.match(/order: "(.+)"/)
|
|
1094
1095
|
const orderMatch2 = file2.match(/order: "(.+)"/)
|
|
1095
1096
|
expect(orderMatch1).toBeTruthy()
|
|
@@ -1118,9 +1119,9 @@ describe('Standalone Server Integration', () => {
|
|
|
1118
1119
|
|
|
1119
1120
|
await sendAndReceive(ws, { type: 'ready' }, 'init')
|
|
1120
1121
|
|
|
1121
|
-
// After load, file should have been migrated to done/
|
|
1122
|
-
expect(fs.existsSync(path.join(tempDir, 'misplaced-done.md'))).toBe(false)
|
|
1123
|
-
expect(fs.existsSync(path.join(tempDir, 'done', 'misplaced-done.md'))).toBe(true)
|
|
1122
|
+
// After load, file should have been migrated to boards/default/done/
|
|
1123
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'misplaced-done.md'))).toBe(false)
|
|
1124
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'done', 'misplaced-done.md'))).toBe(true)
|
|
1124
1125
|
})
|
|
1125
1126
|
|
|
1126
1127
|
it('should move mismatched file to correct status subfolder', async () => {
|
|
@@ -1137,9 +1138,9 @@ describe('Standalone Server Integration', () => {
|
|
|
1137
1138
|
|
|
1138
1139
|
await sendAndReceive(ws, { type: 'ready' }, 'init')
|
|
1139
1140
|
|
|
1140
|
-
// After load, file should have been moved to backlog/
|
|
1141
|
-
expect(fs.existsSync(path.join(tempDir, 'done', 'misplaced-active.md'))).toBe(false)
|
|
1142
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'misplaced-active.md'))).toBe(true)
|
|
1141
|
+
// After load, file should have been moved to boards/default/backlog/
|
|
1142
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'done', 'misplaced-active.md'))).toBe(false)
|
|
1143
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'misplaced-active.md'))).toBe(true)
|
|
1143
1144
|
})
|
|
1144
1145
|
})
|
|
1145
1146
|
|
|
@@ -1536,7 +1537,7 @@ describe('Standalone Server Integration', () => {
|
|
|
1536
1537
|
expect(json.data.filePath).toBeUndefined()
|
|
1537
1538
|
|
|
1538
1539
|
// Verify persisted on disk
|
|
1539
|
-
const todoDir = path.join(tempDir, 'todo')
|
|
1540
|
+
const todoDir = path.join(tempDir, 'boards', 'default', 'todo')
|
|
1540
1541
|
const files = fs.readdirSync(todoDir).filter(f => f.endsWith('.md'))
|
|
1541
1542
|
expect(files.length).toBe(1)
|
|
1542
1543
|
})
|
|
@@ -1597,8 +1598,8 @@ describe('Standalone Server Integration', () => {
|
|
|
1597
1598
|
expect(json.data.status).toBe('in-progress')
|
|
1598
1599
|
|
|
1599
1600
|
// File should be moved
|
|
1600
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'move-api.md'))).toBe(false)
|
|
1601
|
-
expect(fs.existsSync(path.join(tempDir, 'in-progress', 'move-api.md'))).toBe(true)
|
|
1601
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'move-api.md'))).toBe(false)
|
|
1602
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'in-progress', 'move-api.md'))).toBe(true)
|
|
1602
1603
|
})
|
|
1603
1604
|
|
|
1604
1605
|
it('DELETE /api/tasks/:id should delete a task', async () => {
|
|
@@ -1617,7 +1618,7 @@ describe('Standalone Server Integration', () => {
|
|
|
1617
1618
|
expect(json.ok).toBe(true)
|
|
1618
1619
|
|
|
1619
1620
|
// File should be gone
|
|
1620
|
-
expect(fs.existsSync(path.join(tempDir, 'backlog', 'delete-api.md'))).toBe(false)
|
|
1621
|
+
expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'delete-api.md'))).toBe(false)
|
|
1621
1622
|
})
|
|
1622
1623
|
|
|
1623
1624
|
it('DELETE /api/tasks/:id should return 404 for non-existent task', async () => {
|