kanban-lite 1.0.32 → 1.0.33
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 +3 -0
- package/dist/cli.js +46 -11
- package/dist/extension.js +57 -15
- package/dist/mcp-server.js +34 -8
- package/dist/sdk/index.cjs +34 -8
- package/dist/sdk/index.mjs +34 -8
- package/dist/sdk/sdk/KanbanSDK.d.ts +17 -0
- package/package.json +1 -1
- package/src/extension/KanbanPanel.ts +18 -5
- package/src/sdk/KanbanSDK.ts +22 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +2 -2
- package/src/sdk/__tests__/storage-markdown.test.ts +1 -1
- package/src/sdk/fileUtils.ts +5 -2
- package/src/sdk/storage/markdown.ts +7 -6
- package/src/standalone/server.ts +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- **Attachments subfolder**: attachments for the markdown storage engine are now stored in an `attachments/` subdirectory inside each column folder (e.g. `.kanban/boards/default/backlog/attachments/`) instead of alongside the card `.md` files
|
|
12
|
+
- **Browser-viewable attachments**: PDFs and other binary attachments now open with the OS/browser default viewer in the VS Code extension; the standalone server now serves PDF, JPEG, GIF, WebP, CSV, plain-text, and XML attachments with correct `Content-Type` headers so browsers render them inline in a new tab
|
|
13
|
+
- **KanbanSDK.getAttachmentDir(cardId, boardId?)**: new public SDK method that returns the absolute path to the attachment directory for a card (delegates to the active storage engine)
|
|
11
14
|
- **Pluggable storage engine**: new `StorageEngine` interface (`src/sdk/storage/types.ts`) decouples all card I/O from the SDK business logic
|
|
12
15
|
- **SQLite storage engine**: `SqliteStorageEngine` stores cards and comments in a single `.kanban/kanban.db` file using `better-sqlite3`; config (boards, columns, labels, webhooks) always stays in `.kanban.json`
|
|
13
16
|
- **Markdown storage engine**: `MarkdownStorageEngine` wraps the existing file-based I/O, unchanged default behavior
|
package/dist/cli.js
CHANGED
|
@@ -506,11 +506,14 @@ async function moveCardFile(currentPath, kanbanDir, newStatus, attachments) {
|
|
|
506
506
|
await fs2.rename(currentPath, targetPath);
|
|
507
507
|
if (attachments && attachments.length > 0) {
|
|
508
508
|
const sourceDir = path2.dirname(currentPath);
|
|
509
|
+
const sourceAttachmentsDir = path2.join(sourceDir, "attachments");
|
|
510
|
+
const destAttachmentsDir = path2.join(targetDir, "attachments");
|
|
509
511
|
for (const attachment of attachments) {
|
|
510
|
-
const srcAttach = path2.join(
|
|
511
|
-
const destAttach = path2.join(
|
|
512
|
+
const srcAttach = path2.join(sourceAttachmentsDir, attachment);
|
|
513
|
+
const destAttach = path2.join(destAttachmentsDir, attachment);
|
|
512
514
|
try {
|
|
513
515
|
await fs2.access(srcAttach);
|
|
516
|
+
await fs2.mkdir(destAttachmentsDir, { recursive: true });
|
|
514
517
|
await fs2.rename(srcAttach, destAttach);
|
|
515
518
|
} catch {
|
|
516
519
|
}
|
|
@@ -3516,15 +3519,16 @@ var init_markdown = __esm({
|
|
|
3516
3519
|
}
|
|
3517
3520
|
// --- Attachments ---
|
|
3518
3521
|
getCardDir(card) {
|
|
3519
|
-
return path5.dirname(card.filePath);
|
|
3522
|
+
return path5.join(path5.dirname(card.filePath), "attachments");
|
|
3520
3523
|
}
|
|
3521
3524
|
async copyAttachment(sourcePath, card) {
|
|
3522
3525
|
const filename = path5.basename(sourcePath);
|
|
3523
|
-
const
|
|
3524
|
-
|
|
3525
|
-
const
|
|
3526
|
-
|
|
3527
|
-
|
|
3526
|
+
const attachmentDir = this.getCardDir(card);
|
|
3527
|
+
await fs4.mkdir(attachmentDir, { recursive: true });
|
|
3528
|
+
const destPath = path5.join(attachmentDir, filename);
|
|
3529
|
+
const resolvedSource = path5.resolve(sourcePath);
|
|
3530
|
+
if (path5.dirname(resolvedSource) !== attachmentDir) {
|
|
3531
|
+
await fs4.copyFile(resolvedSource, destPath);
|
|
3528
3532
|
}
|
|
3529
3533
|
}
|
|
3530
3534
|
// --- Private helpers ---
|
|
@@ -5916,6 +5920,28 @@ var init_KanbanSDK = __esm({
|
|
|
5916
5920
|
throw new Error(`Card not found: ${cardId}`);
|
|
5917
5921
|
return card.attachments;
|
|
5918
5922
|
}
|
|
5923
|
+
/**
|
|
5924
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
5925
|
+
*
|
|
5926
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
5927
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
5928
|
+
*
|
|
5929
|
+
* @param cardId - The ID of the card.
|
|
5930
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
5931
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
5932
|
+
*
|
|
5933
|
+
* @example
|
|
5934
|
+
* ```ts
|
|
5935
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
5936
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
5937
|
+
* ```
|
|
5938
|
+
*/
|
|
5939
|
+
async getAttachmentDir(cardId, boardId) {
|
|
5940
|
+
const card = await this.getCard(cardId, boardId);
|
|
5941
|
+
if (!card)
|
|
5942
|
+
return null;
|
|
5943
|
+
return this._storage.getCardDir(card);
|
|
5944
|
+
}
|
|
5919
5945
|
// --- Comment management ---
|
|
5920
5946
|
/**
|
|
5921
5947
|
* Lists all comments on a card.
|
|
@@ -51778,7 +51804,8 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
51778
51804
|
const card = cards.find((f) => f.id === cardId);
|
|
51779
51805
|
if (!card)
|
|
51780
51806
|
return false;
|
|
51781
|
-
const cardDir =
|
|
51807
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
51808
|
+
fs7.mkdirSync(cardDir, { recursive: true });
|
|
51782
51809
|
fs7.writeFileSync(path10.join(cardDir, filename), fileData);
|
|
51783
51810
|
migrating = true;
|
|
51784
51811
|
try {
|
|
@@ -52525,7 +52552,7 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
52525
52552
|
const card = cards.find((f) => f.id === id);
|
|
52526
52553
|
if (!card)
|
|
52527
52554
|
return jsonError(res, 404, "Task not found");
|
|
52528
|
-
const cardDir =
|
|
52555
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
52529
52556
|
const attachmentPath = path10.resolve(cardDir, attachName);
|
|
52530
52557
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
52531
52558
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -52844,7 +52871,7 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
52844
52871
|
res.end("Card not found");
|
|
52845
52872
|
return;
|
|
52846
52873
|
}
|
|
52847
|
-
const cardDir =
|
|
52874
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
52848
52875
|
const attachmentPath = path10.resolve(cardDir, filename);
|
|
52849
52876
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
52850
52877
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -52989,8 +53016,16 @@ var init_server3 = __esm({
|
|
|
52989
53016
|
".css": "text/css",
|
|
52990
53017
|
".json": "application/json",
|
|
52991
53018
|
".png": "image/png",
|
|
53019
|
+
".jpg": "image/jpeg",
|
|
53020
|
+
".jpeg": "image/jpeg",
|
|
53021
|
+
".gif": "image/gif",
|
|
53022
|
+
".webp": "image/webp",
|
|
52992
53023
|
".svg": "image/svg+xml",
|
|
52993
53024
|
".ico": "image/x-icon",
|
|
53025
|
+
".pdf": "application/pdf",
|
|
53026
|
+
".txt": "text/plain",
|
|
53027
|
+
".xml": "text/xml",
|
|
53028
|
+
".csv": "text/csv",
|
|
52994
53029
|
".map": "application/json"
|
|
52995
53030
|
};
|
|
52996
53031
|
}
|
package/dist/extension.js
CHANGED
|
@@ -318,11 +318,14 @@ async function moveCardFile(currentPath, kanbanDir, newStatus, attachments) {
|
|
|
318
318
|
await fs2.rename(currentPath, targetPath);
|
|
319
319
|
if (attachments && attachments.length > 0) {
|
|
320
320
|
const sourceDir = path2.dirname(currentPath);
|
|
321
|
+
const sourceAttachmentsDir = path2.join(sourceDir, "attachments");
|
|
322
|
+
const destAttachmentsDir = path2.join(targetDir, "attachments");
|
|
321
323
|
for (const attachment of attachments) {
|
|
322
|
-
const srcAttach = path2.join(
|
|
323
|
-
const destAttach = path2.join(
|
|
324
|
+
const srcAttach = path2.join(sourceAttachmentsDir, attachment);
|
|
325
|
+
const destAttach = path2.join(destAttachmentsDir, attachment);
|
|
324
326
|
try {
|
|
325
327
|
await fs2.access(srcAttach);
|
|
328
|
+
await fs2.mkdir(destAttachmentsDir, { recursive: true });
|
|
326
329
|
await fs2.rename(srcAttach, destAttach);
|
|
327
330
|
} catch {
|
|
328
331
|
}
|
|
@@ -3328,15 +3331,16 @@ var init_markdown = __esm({
|
|
|
3328
3331
|
}
|
|
3329
3332
|
// --- Attachments ---
|
|
3330
3333
|
getCardDir(card) {
|
|
3331
|
-
return path5.dirname(card.filePath);
|
|
3334
|
+
return path5.join(path5.dirname(card.filePath), "attachments");
|
|
3332
3335
|
}
|
|
3333
3336
|
async copyAttachment(sourcePath, card) {
|
|
3334
3337
|
const filename = path5.basename(sourcePath);
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
const
|
|
3338
|
-
|
|
3339
|
-
|
|
3338
|
+
const attachmentDir = this.getCardDir(card);
|
|
3339
|
+
await fs4.mkdir(attachmentDir, { recursive: true });
|
|
3340
|
+
const destPath = path5.join(attachmentDir, filename);
|
|
3341
|
+
const resolvedSource = path5.resolve(sourcePath);
|
|
3342
|
+
if (path5.dirname(resolvedSource) !== attachmentDir) {
|
|
3343
|
+
await fs4.copyFile(resolvedSource, destPath);
|
|
3340
3344
|
}
|
|
3341
3345
|
}
|
|
3342
3346
|
// --- Private helpers ---
|
|
@@ -9640,6 +9644,28 @@ var KanbanSDK = class {
|
|
|
9640
9644
|
throw new Error(`Card not found: ${cardId}`);
|
|
9641
9645
|
return card.attachments;
|
|
9642
9646
|
}
|
|
9647
|
+
/**
|
|
9648
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
9649
|
+
*
|
|
9650
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
9651
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
9652
|
+
*
|
|
9653
|
+
* @param cardId - The ID of the card.
|
|
9654
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
9655
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
9656
|
+
*
|
|
9657
|
+
* @example
|
|
9658
|
+
* ```ts
|
|
9659
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
9660
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
9661
|
+
* ```
|
|
9662
|
+
*/
|
|
9663
|
+
async getAttachmentDir(cardId, boardId) {
|
|
9664
|
+
const card = await this.getCard(cardId, boardId);
|
|
9665
|
+
if (!card)
|
|
9666
|
+
return null;
|
|
9667
|
+
return this._storage.getCardDir(card);
|
|
9668
|
+
}
|
|
9643
9669
|
// --- Comment management ---
|
|
9644
9670
|
/**
|
|
9645
9671
|
* Lists all comments on a card.
|
|
@@ -11960,8 +11986,16 @@ var MIME_TYPES = {
|
|
|
11960
11986
|
".css": "text/css",
|
|
11961
11987
|
".json": "application/json",
|
|
11962
11988
|
".png": "image/png",
|
|
11989
|
+
".jpg": "image/jpeg",
|
|
11990
|
+
".jpeg": "image/jpeg",
|
|
11991
|
+
".gif": "image/gif",
|
|
11992
|
+
".webp": "image/webp",
|
|
11963
11993
|
".svg": "image/svg+xml",
|
|
11964
11994
|
".ico": "image/x-icon",
|
|
11995
|
+
".pdf": "application/pdf",
|
|
11996
|
+
".txt": "text/plain",
|
|
11997
|
+
".xml": "text/xml",
|
|
11998
|
+
".csv": "text/csv",
|
|
11965
11999
|
".map": "application/json"
|
|
11966
12000
|
};
|
|
11967
12001
|
function startServer(kanbanDir, port, webviewDir) {
|
|
@@ -12228,7 +12262,8 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
12228
12262
|
const card = cards.find((f) => f.id === cardId);
|
|
12229
12263
|
if (!card)
|
|
12230
12264
|
return false;
|
|
12231
|
-
const cardDir =
|
|
12265
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
12266
|
+
fs6.mkdirSync(cardDir, { recursive: true });
|
|
12232
12267
|
fs6.writeFileSync(path9.join(cardDir, filename), fileData);
|
|
12233
12268
|
migrating = true;
|
|
12234
12269
|
try {
|
|
@@ -12975,7 +13010,7 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
12975
13010
|
const card = cards.find((f) => f.id === id);
|
|
12976
13011
|
if (!card)
|
|
12977
13012
|
return jsonError(res, 404, "Task not found");
|
|
12978
|
-
const cardDir =
|
|
13013
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
12979
13014
|
const attachmentPath = path9.resolve(cardDir, attachName);
|
|
12980
13015
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
12981
13016
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -13294,7 +13329,7 @@ function startServer(kanbanDir, port, webviewDir) {
|
|
|
13294
13329
|
res.end("Card not found");
|
|
13295
13330
|
return;
|
|
13296
13331
|
}
|
|
13297
|
-
const cardDir =
|
|
13332
|
+
const cardDir = sdk.storageEngine.getCardDir(card);
|
|
13298
13333
|
const attachmentPath = path9.resolve(cardDir, filename);
|
|
13299
13334
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
13300
13335
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -14076,15 +14111,22 @@ var KanbanPanel = class _KanbanPanel {
|
|
|
14076
14111
|
}
|
|
14077
14112
|
}
|
|
14078
14113
|
async _openAttachment(cardId, attachment) {
|
|
14114
|
+
const sdk = this._getSDK();
|
|
14079
14115
|
const card = this._cards.find((f) => f.id === cardId);
|
|
14080
14116
|
if (!card)
|
|
14081
14117
|
return;
|
|
14082
|
-
const
|
|
14083
|
-
const attachmentPath = path10.resolve(
|
|
14118
|
+
const attachmentDir = sdk ? await sdk.getAttachmentDir(cardId, this._currentBoardId) ?? path10.join(path10.dirname(card.filePath), "attachments") : path10.join(path10.dirname(card.filePath), "attachments");
|
|
14119
|
+
const attachmentPath = path10.resolve(attachmentDir, attachment);
|
|
14120
|
+
const ext = path10.extname(attachment).toLowerCase();
|
|
14121
|
+
const isTextFile = [".json", ".txt", ".md", ".html", ".xml", ".csv", ".ts", ".js", ".py", ".yaml", ".yml", ".toml", ".log"].includes(ext);
|
|
14084
14122
|
try {
|
|
14085
14123
|
await vscode.workspace.fs.stat(vscode.Uri.file(attachmentPath));
|
|
14086
|
-
|
|
14087
|
-
|
|
14124
|
+
if (isTextFile) {
|
|
14125
|
+
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(attachmentPath));
|
|
14126
|
+
await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.Beside });
|
|
14127
|
+
} else {
|
|
14128
|
+
await vscode.env.openExternal(vscode.Uri.file(attachmentPath));
|
|
14129
|
+
}
|
|
14088
14130
|
} catch {
|
|
14089
14131
|
await vscode.env.openExternal(vscode.Uri.file(attachmentPath));
|
|
14090
14132
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -291,11 +291,14 @@ async function moveCardFile(currentPath, kanbanDir, newStatus, attachments) {
|
|
|
291
291
|
await fs2.rename(currentPath, targetPath);
|
|
292
292
|
if (attachments && attachments.length > 0) {
|
|
293
293
|
const sourceDir = path2.dirname(currentPath);
|
|
294
|
+
const sourceAttachmentsDir = path2.join(sourceDir, "attachments");
|
|
295
|
+
const destAttachmentsDir = path2.join(targetDir, "attachments");
|
|
294
296
|
for (const attachment of attachments) {
|
|
295
|
-
const srcAttach = path2.join(
|
|
296
|
-
const destAttach = path2.join(
|
|
297
|
+
const srcAttach = path2.join(sourceAttachmentsDir, attachment);
|
|
298
|
+
const destAttach = path2.join(destAttachmentsDir, attachment);
|
|
297
299
|
try {
|
|
298
300
|
await fs2.access(srcAttach);
|
|
301
|
+
await fs2.mkdir(destAttachmentsDir, { recursive: true });
|
|
299
302
|
await fs2.rename(srcAttach, destAttach);
|
|
300
303
|
} catch {
|
|
301
304
|
}
|
|
@@ -3301,15 +3304,16 @@ var init_markdown = __esm({
|
|
|
3301
3304
|
}
|
|
3302
3305
|
// --- Attachments ---
|
|
3303
3306
|
getCardDir(card) {
|
|
3304
|
-
return path5.dirname(card.filePath);
|
|
3307
|
+
return path5.join(path5.dirname(card.filePath), "attachments");
|
|
3305
3308
|
}
|
|
3306
3309
|
async copyAttachment(sourcePath, card) {
|
|
3307
3310
|
const filename = path5.basename(sourcePath);
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3311
|
+
const attachmentDir = this.getCardDir(card);
|
|
3312
|
+
await fs4.mkdir(attachmentDir, { recursive: true });
|
|
3313
|
+
const destPath = path5.join(attachmentDir, filename);
|
|
3314
|
+
const resolvedSource = path5.resolve(sourcePath);
|
|
3315
|
+
if (path5.dirname(resolvedSource) !== attachmentDir) {
|
|
3316
|
+
await fs4.copyFile(resolvedSource, destPath);
|
|
3313
3317
|
}
|
|
3314
3318
|
}
|
|
3315
3319
|
// --- Private helpers ---
|
|
@@ -5899,6 +5903,28 @@ var KanbanSDK = class {
|
|
|
5899
5903
|
throw new Error(`Card not found: ${cardId}`);
|
|
5900
5904
|
return card.attachments;
|
|
5901
5905
|
}
|
|
5906
|
+
/**
|
|
5907
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
5908
|
+
*
|
|
5909
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
5910
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
5911
|
+
*
|
|
5912
|
+
* @param cardId - The ID of the card.
|
|
5913
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
5914
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
5915
|
+
*
|
|
5916
|
+
* @example
|
|
5917
|
+
* ```ts
|
|
5918
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
5919
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
5920
|
+
* ```
|
|
5921
|
+
*/
|
|
5922
|
+
async getAttachmentDir(cardId, boardId) {
|
|
5923
|
+
const card = await this.getCard(cardId, boardId);
|
|
5924
|
+
if (!card)
|
|
5925
|
+
return null;
|
|
5926
|
+
return this._storage.getCardDir(card);
|
|
5927
|
+
}
|
|
5902
5928
|
// --- Comment management ---
|
|
5903
5929
|
/**
|
|
5904
5930
|
* Lists all comments on a card.
|
package/dist/sdk/index.cjs
CHANGED
|
@@ -295,11 +295,14 @@ async function moveCardFile(currentPath, kanbanDir, newStatus, attachments) {
|
|
|
295
295
|
await fs2.rename(currentPath, targetPath);
|
|
296
296
|
if (attachments && attachments.length > 0) {
|
|
297
297
|
const sourceDir = path2.dirname(currentPath);
|
|
298
|
+
const sourceAttachmentsDir = path2.join(sourceDir, "attachments");
|
|
299
|
+
const destAttachmentsDir = path2.join(targetDir, "attachments");
|
|
298
300
|
for (const attachment of attachments) {
|
|
299
|
-
const srcAttach = path2.join(
|
|
300
|
-
const destAttach = path2.join(
|
|
301
|
+
const srcAttach = path2.join(sourceAttachmentsDir, attachment);
|
|
302
|
+
const destAttach = path2.join(destAttachmentsDir, attachment);
|
|
301
303
|
try {
|
|
302
304
|
await fs2.access(srcAttach);
|
|
305
|
+
await fs2.mkdir(destAttachmentsDir, { recursive: true });
|
|
303
306
|
await fs2.rename(srcAttach, destAttach);
|
|
304
307
|
} catch {
|
|
305
308
|
}
|
|
@@ -3305,15 +3308,16 @@ var init_markdown = __esm({
|
|
|
3305
3308
|
}
|
|
3306
3309
|
// --- Attachments ---
|
|
3307
3310
|
getCardDir(card) {
|
|
3308
|
-
return path5.dirname(card.filePath);
|
|
3311
|
+
return path5.join(path5.dirname(card.filePath), "attachments");
|
|
3309
3312
|
}
|
|
3310
3313
|
async copyAttachment(sourcePath, card) {
|
|
3311
3314
|
const filename = path5.basename(sourcePath);
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3315
|
+
const attachmentDir = this.getCardDir(card);
|
|
3316
|
+
await fs4.mkdir(attachmentDir, { recursive: true });
|
|
3317
|
+
const destPath = path5.join(attachmentDir, filename);
|
|
3318
|
+
const resolvedSource = path5.resolve(sourcePath);
|
|
3319
|
+
if (path5.dirname(resolvedSource) !== attachmentDir) {
|
|
3320
|
+
await fs4.copyFile(resolvedSource, destPath);
|
|
3317
3321
|
}
|
|
3318
3322
|
}
|
|
3319
3323
|
// --- Private helpers ---
|
|
@@ -5922,6 +5926,28 @@ var KanbanSDK = class {
|
|
|
5922
5926
|
throw new Error(`Card not found: ${cardId}`);
|
|
5923
5927
|
return card.attachments;
|
|
5924
5928
|
}
|
|
5929
|
+
/**
|
|
5930
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
5931
|
+
*
|
|
5932
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
5933
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
5934
|
+
*
|
|
5935
|
+
* @param cardId - The ID of the card.
|
|
5936
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
5937
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
5938
|
+
*
|
|
5939
|
+
* @example
|
|
5940
|
+
* ```ts
|
|
5941
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
5942
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
5943
|
+
* ```
|
|
5944
|
+
*/
|
|
5945
|
+
async getAttachmentDir(cardId, boardId) {
|
|
5946
|
+
const card = await this.getCard(cardId, boardId);
|
|
5947
|
+
if (!card)
|
|
5948
|
+
return null;
|
|
5949
|
+
return this._storage.getCardDir(card);
|
|
5950
|
+
}
|
|
5925
5951
|
// --- Comment management ---
|
|
5926
5952
|
/**
|
|
5927
5953
|
* Lists all comments on a card.
|
package/dist/sdk/index.mjs
CHANGED
|
@@ -302,11 +302,14 @@ async function moveCardFile(currentPath, kanbanDir, newStatus, attachments) {
|
|
|
302
302
|
await fs2.rename(currentPath, targetPath);
|
|
303
303
|
if (attachments && attachments.length > 0) {
|
|
304
304
|
const sourceDir = path2.dirname(currentPath);
|
|
305
|
+
const sourceAttachmentsDir = path2.join(sourceDir, "attachments");
|
|
306
|
+
const destAttachmentsDir = path2.join(targetDir, "attachments");
|
|
305
307
|
for (const attachment of attachments) {
|
|
306
|
-
const srcAttach = path2.join(
|
|
307
|
-
const destAttach = path2.join(
|
|
308
|
+
const srcAttach = path2.join(sourceAttachmentsDir, attachment);
|
|
309
|
+
const destAttach = path2.join(destAttachmentsDir, attachment);
|
|
308
310
|
try {
|
|
309
311
|
await fs2.access(srcAttach);
|
|
312
|
+
await fs2.mkdir(destAttachmentsDir, { recursive: true });
|
|
310
313
|
await fs2.rename(srcAttach, destAttach);
|
|
311
314
|
} catch {
|
|
312
315
|
}
|
|
@@ -3307,15 +3310,16 @@ var init_markdown = __esm({
|
|
|
3307
3310
|
}
|
|
3308
3311
|
// --- Attachments ---
|
|
3309
3312
|
getCardDir(card) {
|
|
3310
|
-
return path5.dirname(card.filePath);
|
|
3313
|
+
return path5.join(path5.dirname(card.filePath), "attachments");
|
|
3311
3314
|
}
|
|
3312
3315
|
async copyAttachment(sourcePath, card) {
|
|
3313
3316
|
const filename = path5.basename(sourcePath);
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
const
|
|
3317
|
-
|
|
3318
|
-
|
|
3317
|
+
const attachmentDir = this.getCardDir(card);
|
|
3318
|
+
await fs4.mkdir(attachmentDir, { recursive: true });
|
|
3319
|
+
const destPath = path5.join(attachmentDir, filename);
|
|
3320
|
+
const resolvedSource = path5.resolve(sourcePath);
|
|
3321
|
+
if (path5.dirname(resolvedSource) !== attachmentDir) {
|
|
3322
|
+
await fs4.copyFile(resolvedSource, destPath);
|
|
3319
3323
|
}
|
|
3320
3324
|
}
|
|
3321
3325
|
// --- Private helpers ---
|
|
@@ -5898,6 +5902,28 @@ var KanbanSDK = class {
|
|
|
5898
5902
|
throw new Error(`Card not found: ${cardId}`);
|
|
5899
5903
|
return card.attachments;
|
|
5900
5904
|
}
|
|
5905
|
+
/**
|
|
5906
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
5907
|
+
*
|
|
5908
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
5909
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
5910
|
+
*
|
|
5911
|
+
* @param cardId - The ID of the card.
|
|
5912
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
5913
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
5914
|
+
*
|
|
5915
|
+
* @example
|
|
5916
|
+
* ```ts
|
|
5917
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
5918
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
5919
|
+
* ```
|
|
5920
|
+
*/
|
|
5921
|
+
async getAttachmentDir(cardId, boardId) {
|
|
5922
|
+
const card = await this.getCard(cardId, boardId);
|
|
5923
|
+
if (!card)
|
|
5924
|
+
return null;
|
|
5925
|
+
return this._storage.getCardDir(card);
|
|
5926
|
+
}
|
|
5901
5927
|
// --- Comment management ---
|
|
5902
5928
|
/**
|
|
5903
5929
|
* Lists all comments on a card.
|
|
@@ -619,6 +619,23 @@ export declare class KanbanSDK {
|
|
|
619
619
|
* ```
|
|
620
620
|
*/
|
|
621
621
|
listAttachments(cardId: string, boardId?: string): Promise<string[]>;
|
|
622
|
+
/**
|
|
623
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
624
|
+
*
|
|
625
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
626
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
627
|
+
*
|
|
628
|
+
* @param cardId - The ID of the card.
|
|
629
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
630
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
635
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
getAttachmentDir(cardId: string, boardId?: string): Promise<string | null>;
|
|
622
639
|
/**
|
|
623
640
|
* Lists all comments on a card.
|
|
624
641
|
*
|
package/package.json
CHANGED
|
@@ -744,18 +744,31 @@ export class KanbanPanel {
|
|
|
744
744
|
}
|
|
745
745
|
|
|
746
746
|
private async _openAttachment(cardId: string, attachment: string): Promise<void> {
|
|
747
|
+
const sdk = this._getSDK()
|
|
747
748
|
const card = this._cards.find(f => f.id === cardId)
|
|
748
749
|
if (!card) return
|
|
749
750
|
|
|
750
|
-
|
|
751
|
-
const
|
|
751
|
+
// Resolve attachment directory via SDK (handles both markdown and SQLite paths)
|
|
752
|
+
const attachmentDir = sdk
|
|
753
|
+
? (await sdk.getAttachmentDir(cardId, this._currentBoardId) ?? path.join(path.dirname(card.filePath), 'attachments'))
|
|
754
|
+
: path.join(path.dirname(card.filePath), 'attachments')
|
|
755
|
+
|
|
756
|
+
const attachmentPath = path.resolve(attachmentDir, attachment)
|
|
757
|
+
|
|
758
|
+
const ext = path.extname(attachment).toLowerCase()
|
|
759
|
+
// Text-based files open as VS Code editor tabs; everything else opens externally
|
|
760
|
+
// (PDF, images, etc. open via the OS default viewer or browser)
|
|
761
|
+
const isTextFile = ['.json', '.txt', '.md', '.html', '.xml', '.csv', '.ts', '.js', '.py', '.yaml', '.yml', '.toml', '.log'].includes(ext)
|
|
752
762
|
|
|
753
763
|
try {
|
|
754
764
|
await vscode.workspace.fs.stat(vscode.Uri.file(attachmentPath))
|
|
755
|
-
|
|
756
|
-
|
|
765
|
+
if (isTextFile) {
|
|
766
|
+
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(attachmentPath))
|
|
767
|
+
await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.Beside })
|
|
768
|
+
} else {
|
|
769
|
+
await vscode.env.openExternal(vscode.Uri.file(attachmentPath))
|
|
770
|
+
}
|
|
757
771
|
} catch {
|
|
758
|
-
// For binary files or files that can't be opened as text, reveal in OS
|
|
759
772
|
await vscode.env.openExternal(vscode.Uri.file(attachmentPath))
|
|
760
773
|
}
|
|
761
774
|
}
|
package/src/sdk/KanbanSDK.ts
CHANGED
|
@@ -1155,6 +1155,28 @@ export class KanbanSDK {
|
|
|
1155
1155
|
return card.attachments
|
|
1156
1156
|
}
|
|
1157
1157
|
|
|
1158
|
+
/**
|
|
1159
|
+
* Returns the absolute path to the attachment directory for a card.
|
|
1160
|
+
*
|
|
1161
|
+
* For the markdown engine this is `{column_dir}/attachments/`.
|
|
1162
|
+
* For the SQLite engine this is `.kanban/boards/{boardId}/attachments/{cardId}/`.
|
|
1163
|
+
*
|
|
1164
|
+
* @param cardId - The ID of the card.
|
|
1165
|
+
* @param boardId - Optional board ID. Defaults to the workspace's default board.
|
|
1166
|
+
* @returns A promise resolving to the absolute directory path, or `null` if the card is not found.
|
|
1167
|
+
*
|
|
1168
|
+
* @example
|
|
1169
|
+
* ```ts
|
|
1170
|
+
* const dir = await sdk.getAttachmentDir('42')
|
|
1171
|
+
* // '/workspace/.kanban/boards/default/backlog/attachments'
|
|
1172
|
+
* ```
|
|
1173
|
+
*/
|
|
1174
|
+
async getAttachmentDir(cardId: string, boardId?: string): Promise<string | null> {
|
|
1175
|
+
const card = await this.getCard(cardId, boardId)
|
|
1176
|
+
if (!card) return null
|
|
1177
|
+
return this._storage.getCardDir(card)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1158
1180
|
// --- Comment management ---
|
|
1159
1181
|
|
|
1160
1182
|
/**
|
|
@@ -470,8 +470,8 @@ describe('KanbanSDK', () => {
|
|
|
470
470
|
const updated = await sdk.addAttachment('card', srcFile)
|
|
471
471
|
expect(updated.attachments).toContain('test-attach.txt')
|
|
472
472
|
|
|
473
|
-
// Verify file was copied to the status
|
|
474
|
-
const destPath = path.join(tempDir, 'boards', 'default', 'backlog', 'test-attach.txt')
|
|
473
|
+
// Verify file was copied to the attachments subfolder inside the status dir
|
|
474
|
+
const destPath = path.join(tempDir, 'boards', 'default', 'backlog', 'attachments', 'test-attach.txt')
|
|
475
475
|
expect(fs.existsSync(destPath)).toBe(true)
|
|
476
476
|
|
|
477
477
|
fs.unlinkSync(srcFile)
|
|
@@ -167,7 +167,7 @@ describe('MarkdownStorageEngine', () => {
|
|
|
167
167
|
const card = makeCard({
|
|
168
168
|
filePath: path.join(boardDir, 'backlog', '1-test-card.md'),
|
|
169
169
|
})
|
|
170
|
-
expect(engine.getCardDir(card)).toBe(path.join(boardDir, 'backlog'))
|
|
170
|
+
expect(engine.getCardDir(card)).toBe(path.join(boardDir, 'backlog', 'attachments'))
|
|
171
171
|
})
|
|
172
172
|
})
|
|
173
173
|
})
|
package/src/sdk/fileUtils.ts
CHANGED
|
@@ -72,11 +72,14 @@ export async function moveCardFile(
|
|
|
72
72
|
|
|
73
73
|
if (attachments && attachments.length > 0) {
|
|
74
74
|
const sourceDir = path.dirname(currentPath)
|
|
75
|
+
const sourceAttachmentsDir = path.join(sourceDir, 'attachments')
|
|
76
|
+
const destAttachmentsDir = path.join(targetDir, 'attachments')
|
|
75
77
|
for (const attachment of attachments) {
|
|
76
|
-
const srcAttach = path.join(
|
|
77
|
-
const destAttach = path.join(
|
|
78
|
+
const srcAttach = path.join(sourceAttachmentsDir, attachment)
|
|
79
|
+
const destAttach = path.join(destAttachmentsDir, attachment)
|
|
78
80
|
try {
|
|
79
81
|
await fs.access(srcAttach)
|
|
82
|
+
await fs.mkdir(destAttachmentsDir, { recursive: true })
|
|
80
83
|
await fs.rename(srcAttach, destAttach)
|
|
81
84
|
} catch {
|
|
82
85
|
// Best effort -- skip failed attachment moves
|
|
@@ -161,16 +161,17 @@ export class MarkdownStorageEngine implements StorageEngine {
|
|
|
161
161
|
// --- Attachments ---
|
|
162
162
|
|
|
163
163
|
getCardDir(card: Card): string {
|
|
164
|
-
return path.dirname(card.filePath)
|
|
164
|
+
return path.join(path.dirname(card.filePath), 'attachments')
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async copyAttachment(sourcePath: string, card: Card): Promise<void> {
|
|
168
168
|
const filename = path.basename(sourcePath)
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
const attachmentDir = this.getCardDir(card)
|
|
170
|
+
await fs.mkdir(attachmentDir, { recursive: true })
|
|
171
|
+
const destPath = path.join(attachmentDir, filename)
|
|
172
|
+
const resolvedSource = path.resolve(sourcePath)
|
|
173
|
+
if (path.dirname(resolvedSource) !== attachmentDir) {
|
|
174
|
+
await fs.copyFile(resolvedSource, destPath)
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
package/src/standalone/server.ts
CHANGED
|
@@ -28,8 +28,16 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
28
28
|
'.css': 'text/css',
|
|
29
29
|
'.json': 'application/json',
|
|
30
30
|
'.png': 'image/png',
|
|
31
|
+
'.jpg': 'image/jpeg',
|
|
32
|
+
'.jpeg': 'image/jpeg',
|
|
33
|
+
'.gif': 'image/gif',
|
|
34
|
+
'.webp': 'image/webp',
|
|
31
35
|
'.svg': 'image/svg+xml',
|
|
32
36
|
'.ico': 'image/x-icon',
|
|
37
|
+
'.pdf': 'application/pdf',
|
|
38
|
+
'.txt': 'text/plain',
|
|
39
|
+
'.xml': 'text/xml',
|
|
40
|
+
'.csv': 'text/csv',
|
|
33
41
|
'.map': 'application/json'
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -334,8 +342,9 @@ export function startServer(kanbanDir: string, port: number, webviewDir?: string
|
|
|
334
342
|
const card = cards.find(f => f.id === cardId)
|
|
335
343
|
if (!card) return false
|
|
336
344
|
|
|
337
|
-
// Write file data to the card's directory
|
|
338
|
-
const cardDir =
|
|
345
|
+
// Write file data to the card's attachment directory
|
|
346
|
+
const cardDir = sdk.storageEngine.getCardDir(card)
|
|
347
|
+
fs.mkdirSync(cardDir, { recursive: true })
|
|
339
348
|
fs.writeFileSync(path.join(cardDir, filename), fileData)
|
|
340
349
|
|
|
341
350
|
// Register attachment via SDK (skips copy since file is already in place)
|
|
@@ -1100,7 +1109,7 @@ export function startServer(kanbanDir: string, port: number, webviewDir?: string
|
|
|
1100
1109
|
const { id, filename: attachName } = params
|
|
1101
1110
|
const card = cards.find(f => f.id === id)
|
|
1102
1111
|
if (!card) return jsonError(res, 404, 'Task not found')
|
|
1103
|
-
const cardDir =
|
|
1112
|
+
const cardDir = sdk.storageEngine.getCardDir(card)
|
|
1104
1113
|
const attachmentPath = path.resolve(cardDir, attachName)
|
|
1105
1114
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
1106
1115
|
res.writeHead(403, { 'Content-Type': 'text/plain' })
|
|
@@ -1426,7 +1435,7 @@ export function startServer(kanbanDir: string, port: number, webviewDir?: string
|
|
|
1426
1435
|
}
|
|
1427
1436
|
const card = cards.find(f => f.id === cardId)
|
|
1428
1437
|
if (!card) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Card not found'); return }
|
|
1429
|
-
const cardDir =
|
|
1438
|
+
const cardDir = sdk.storageEngine.getCardDir(card)
|
|
1430
1439
|
const attachmentPath = path.resolve(cardDir, filename)
|
|
1431
1440
|
if (!attachmentPath.startsWith(absoluteKanbanDir)) {
|
|
1432
1441
|
res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden'); return
|