md-task-viewer 0.1.8 → 0.1.10
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/README.md +5 -3
- package/dist/cli.js +212 -138
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-CSNRoovR.css +1 -0
- package/dist/client/assets/index-j5ntXWRS.js +78 -0
- package/dist/client/index.html +2 -2
- package/dist/server.js +185 -133
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/index-BfBV_hRv.css +0 -1
- package/dist/client/assets/index-DDUTU9wi.js +0 -77
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Each Markdown file (`1 file = 1 task`) is managed through a browser UI, and all
|
|
|
8
8
|
|
|
9
9
|
- List Markdown tasks
|
|
10
10
|
- Create, edit, and delete tasks
|
|
11
|
-
- Frontmatter-based `MUST` / `WANT` priority and `TODO` / `
|
|
11
|
+
- Frontmatter-based `MUST` / `WANT` priority and `TODO` / `DONE` status
|
|
12
12
|
- Drag-and-drop reordering
|
|
13
13
|
- Persistent ordering via a dedicated metadata file
|
|
14
14
|
- Auto-reload on external file changes
|
|
@@ -46,7 +46,7 @@ Each Markdown file should have frontmatter with the following keys:
|
|
|
46
46
|
---
|
|
47
47
|
title: Release notes
|
|
48
48
|
priority: MUST
|
|
49
|
-
status:
|
|
49
|
+
status: TODO
|
|
50
50
|
createdAt: 2026-03-15T08:00:00.000Z
|
|
51
51
|
updatedAt: 2026-03-15T09:30:00.000Z
|
|
52
52
|
---
|
|
@@ -60,7 +60,7 @@ Free-form body text.
|
|
|
60
60
|
|
|
61
61
|
- `title`
|
|
62
62
|
- `priority`: `MUST` or `WANT`
|
|
63
|
-
- `status`: `TODO
|
|
63
|
+
- `status`: `TODO` or `DONE`
|
|
64
64
|
- `createdAt`: UTC ISO 8601
|
|
65
65
|
- `updatedAt`: UTC ISO 8601
|
|
66
66
|
|
|
@@ -68,6 +68,8 @@ Unknown frontmatter keys are preserved as-is.
|
|
|
68
68
|
|
|
69
69
|
Files missing required keys are displayed with default values and normalized on save.
|
|
70
70
|
|
|
71
|
+
Legacy `status: WIP` is treated as `TODO` when loaded and will be replaced with `TODO` on save.
|
|
72
|
+
|
|
71
73
|
Files with unparseable YAML frontmatter are excluded from the list and shown in the error panel.
|
|
72
74
|
|
|
73
75
|
## Ordering Metadata
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path8 from "path";
|
|
5
5
|
import process from "process";
|
|
6
6
|
import open from "open";
|
|
7
7
|
|
|
@@ -9,32 +9,25 @@ import open from "open";
|
|
|
9
9
|
import Fastify from "fastify";
|
|
10
10
|
import fastifyStatic from "@fastify/static";
|
|
11
11
|
import chokidar from "chokidar";
|
|
12
|
-
import
|
|
12
|
+
import path7 from "path";
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
|
|
15
|
-
// src/taskStore.ts
|
|
16
|
-
import matter from "gray-matter";
|
|
17
|
-
import picomatch from "picomatch";
|
|
18
|
-
import path from "path";
|
|
19
|
-
import { promises as fs } from "fs";
|
|
20
|
-
|
|
21
|
-
// src/types.ts
|
|
22
|
-
var CONFIG_FILE_NAME = ".md-task-viewer.json";
|
|
23
|
-
|
|
24
15
|
// src/slugify.ts
|
|
25
16
|
function slugify(value) {
|
|
26
17
|
const slug = value.normalize("NFC").replace(/[\s\u3000]+/g, "-").replace(/[^\p{L}\p{N}-]+/gu, "").replace(/^-+|-+$/g, "");
|
|
27
18
|
return slug || "untitled-task";
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
// src/taskStore.ts
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
// src/taskStore/paths.ts
|
|
22
|
+
import path from "path";
|
|
23
|
+
|
|
24
|
+
// src/taskStore/errors.ts
|
|
34
25
|
var ConflictError = class extends Error {
|
|
35
26
|
};
|
|
36
27
|
var ValidationError = class extends Error {
|
|
37
28
|
};
|
|
29
|
+
|
|
30
|
+
// src/taskStore/paths.ts
|
|
38
31
|
function toPosixPath(filePath) {
|
|
39
32
|
return filePath.split(path.sep).join("/");
|
|
40
33
|
}
|
|
@@ -48,11 +41,30 @@ function normalizeRelativePath(candidate) {
|
|
|
48
41
|
function ensureMarkdownExtension(filePath) {
|
|
49
42
|
return path.posix.extname(filePath) ? filePath : `${filePath}.md`;
|
|
50
43
|
}
|
|
44
|
+
|
|
45
|
+
// src/taskStore/frontmatter.ts
|
|
46
|
+
import matter from "gray-matter";
|
|
47
|
+
import path2 from "path";
|
|
48
|
+
import { promises as fs } from "fs";
|
|
49
|
+
var REQUIRED_PRIORITY = ["MUST", "WANT"];
|
|
50
|
+
var REQUIRED_STATUS = ["TODO", "DONE"];
|
|
51
|
+
function isValidPriority(value) {
|
|
52
|
+
return REQUIRED_PRIORITY.includes(value);
|
|
53
|
+
}
|
|
54
|
+
function isValidStatus(value) {
|
|
55
|
+
return REQUIRED_STATUS.includes(value);
|
|
56
|
+
}
|
|
57
|
+
function ensureRequiredStatus(status) {
|
|
58
|
+
if (!isValidStatus(status)) {
|
|
59
|
+
throw new ValidationError("Status must be TODO or DONE.");
|
|
60
|
+
}
|
|
61
|
+
return status;
|
|
62
|
+
}
|
|
51
63
|
function asUtcISOString(date) {
|
|
52
64
|
return date.toISOString();
|
|
53
65
|
}
|
|
54
66
|
function buildDefaults(filePath, stats) {
|
|
55
|
-
const basename =
|
|
67
|
+
const basename = path2.basename(filePath, path2.extname(filePath));
|
|
56
68
|
const title = basename.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
57
69
|
return {
|
|
58
70
|
title,
|
|
@@ -70,8 +82,8 @@ function splitFrontmatter(data, statsDefaults) {
|
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
const title = typeof data.title === "string" && data.title.trim() ? data.title : statsDefaults.title;
|
|
73
|
-
const priority =
|
|
74
|
-
const status =
|
|
85
|
+
const priority = isValidPriority(data.priority) ? data.priority : statsDefaults.priority;
|
|
86
|
+
const status = isValidStatus(data.status) ? data.status : statsDefaults.status;
|
|
75
87
|
const createdAt = typeof data.createdAt === "string" && !Number.isNaN(Date.parse(data.createdAt)) ? new Date(data.createdAt).toISOString() : statsDefaults.createdAt;
|
|
76
88
|
const updatedAt = typeof data.updatedAt === "string" && !Number.isNaN(Date.parse(data.updatedAt)) ? new Date(data.updatedAt).toISOString() : statsDefaults.updatedAt;
|
|
77
89
|
const normalized = title !== data.title || priority !== data.priority || status !== data.status || createdAt !== data.createdAt || updatedAt !== data.updatedAt;
|
|
@@ -92,53 +104,8 @@ function serializeTask(record) {
|
|
|
92
104
|
};
|
|
93
105
|
return matter.stringify(record.content, data);
|
|
94
106
|
}
|
|
95
|
-
async function readDirectoryRecursive(rootDir, currentDir, results) {
|
|
96
|
-
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
if (entry.name === ".git" || entry.name === "node_modules") {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const absolutePath = path.join(currentDir, entry.name);
|
|
102
|
-
if (entry.isDirectory()) {
|
|
103
|
-
await readDirectoryRecursive(rootDir, absolutePath, results);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (entry.name === CONFIG_FILE_NAME) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (!MARKDOWN_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
results.push(toPosixPath(path.relative(rootDir, absolutePath)));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function listMarkdownFiles(rootDir, taskDirs, ignorePaths) {
|
|
116
|
-
const results = [];
|
|
117
|
-
const seen = /* @__PURE__ */ new Set();
|
|
118
|
-
const isIgnored = ignorePaths.length > 0 ? picomatch(ignorePaths) : null;
|
|
119
|
-
for (const taskDir of taskDirs) {
|
|
120
|
-
const scanDir = path.resolve(rootDir, taskDir);
|
|
121
|
-
try {
|
|
122
|
-
await fs.access(scanDir);
|
|
123
|
-
} catch {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const dirResults = [];
|
|
127
|
-
await readDirectoryRecursive(rootDir, scanDir, dirResults);
|
|
128
|
-
for (const filePath of dirResults) {
|
|
129
|
-
if (!seen.has(filePath)) {
|
|
130
|
-
seen.add(filePath);
|
|
131
|
-
if (isIgnored && isIgnored(filePath)) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
results.push(filePath);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return results.sort();
|
|
139
|
-
}
|
|
140
107
|
async function parseTask(rootDir, relativePath) {
|
|
141
|
-
const absolutePath =
|
|
108
|
+
const absolutePath = path2.join(rootDir, relativePath);
|
|
142
109
|
const raw = await fs.readFile(absolutePath, "utf8");
|
|
143
110
|
const stats = await fs.stat(absolutePath);
|
|
144
111
|
const parsed = matter(raw);
|
|
@@ -153,10 +120,19 @@ async function parseTask(rootDir, relativePath) {
|
|
|
153
120
|
normalized
|
|
154
121
|
};
|
|
155
122
|
}
|
|
123
|
+
|
|
124
|
+
// src/taskStore/config.ts
|
|
125
|
+
import path3 from "path";
|
|
126
|
+
import { promises as fs2 } from "fs";
|
|
127
|
+
|
|
128
|
+
// src/types.ts
|
|
129
|
+
var CONFIG_FILE_NAME = ".md-task-viewer.json";
|
|
130
|
+
|
|
131
|
+
// src/taskStore/config.ts
|
|
156
132
|
async function readConfig(rootDir) {
|
|
157
|
-
const configFilePath =
|
|
133
|
+
const configFilePath = path3.join(rootDir, CONFIG_FILE_NAME);
|
|
158
134
|
try {
|
|
159
|
-
const raw = await
|
|
135
|
+
const raw = await fs2.readFile(configFilePath, "utf8");
|
|
160
136
|
const parsed = JSON.parse(raw);
|
|
161
137
|
const taskDirs = Array.isArray(parsed.taskDirs) ? parsed.taskDirs.filter((item) => typeof item === "string") : ["."];
|
|
162
138
|
const ignorePaths = Array.isArray(parsed.ignorePaths) ? parsed.ignorePaths.filter((item) => typeof item === "string") : [];
|
|
@@ -201,8 +177,14 @@ async function saveOrder(rootDir, order) {
|
|
|
201
177
|
)
|
|
202
178
|
);
|
|
203
179
|
const existing = await readConfig(rootDir);
|
|
204
|
-
const payload = {
|
|
205
|
-
|
|
180
|
+
const payload = {
|
|
181
|
+
version: 1,
|
|
182
|
+
taskDirs: existing.taskDirs,
|
|
183
|
+
ignorePaths: existing.ignorePaths,
|
|
184
|
+
order: normalized,
|
|
185
|
+
commands: existing.commands
|
|
186
|
+
};
|
|
187
|
+
await fs2.writeFile(path3.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
|
|
206
188
|
`, "utf8");
|
|
207
189
|
}
|
|
208
190
|
async function saveConfig(rootDir, taskDirs, ignorePaths, commands) {
|
|
@@ -219,11 +201,102 @@ async function saveConfig(rootDir, taskDirs, ignorePaths, commands) {
|
|
|
219
201
|
const existing = await readConfig(rootDir);
|
|
220
202
|
const validatedIgnorePaths = ignorePaths ?? existing.ignorePaths;
|
|
221
203
|
const validatedCommands = commands !== void 0 ? commands : existing.commands;
|
|
222
|
-
const payload = {
|
|
223
|
-
|
|
204
|
+
const payload = {
|
|
205
|
+
version: 1,
|
|
206
|
+
taskDirs: validated,
|
|
207
|
+
ignorePaths: validatedIgnorePaths,
|
|
208
|
+
order: existing.order,
|
|
209
|
+
commands: validatedCommands
|
|
210
|
+
};
|
|
211
|
+
await fs2.writeFile(path3.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
|
|
224
212
|
`, "utf8");
|
|
225
213
|
return payload;
|
|
226
214
|
}
|
|
215
|
+
function parseOrderPayload(input) {
|
|
216
|
+
if (!Array.isArray(input)) {
|
|
217
|
+
throw new ValidationError("Order payload must be an array.");
|
|
218
|
+
}
|
|
219
|
+
return input.map((item) => ensureMarkdownExtension(normalizeRelativePath(String(item))));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/taskStore/tasks.ts
|
|
223
|
+
import path5 from "path";
|
|
224
|
+
import { promises as fs4 } from "fs";
|
|
225
|
+
|
|
226
|
+
// src/taskStore/scanner.ts
|
|
227
|
+
import picomatch from "picomatch";
|
|
228
|
+
import path4 from "path";
|
|
229
|
+
import { promises as fs3 } from "fs";
|
|
230
|
+
var MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
231
|
+
async function readDirectoryRecursive(rootDir, currentDir, results) {
|
|
232
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (entry.name === ".git" || entry.name === "node_modules") {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const absolutePath = path4.join(currentDir, entry.name);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
await readDirectoryRecursive(rootDir, absolutePath, results);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (entry.name === CONFIG_FILE_NAME) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!MARKDOWN_EXTENSIONS.has(path4.extname(entry.name).toLowerCase())) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
results.push(toPosixPath(path4.relative(rootDir, absolutePath)));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function listMarkdownFiles(rootDir, taskDirs, ignorePaths) {
|
|
252
|
+
const results = [];
|
|
253
|
+
const seen = /* @__PURE__ */ new Set();
|
|
254
|
+
const isIgnored = ignorePaths.length > 0 ? picomatch(ignorePaths) : null;
|
|
255
|
+
for (const taskDir of taskDirs) {
|
|
256
|
+
const scanDir = path4.resolve(rootDir, taskDir);
|
|
257
|
+
try {
|
|
258
|
+
await fs3.access(scanDir);
|
|
259
|
+
} catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const dirResults = [];
|
|
263
|
+
await readDirectoryRecursive(rootDir, scanDir, dirResults);
|
|
264
|
+
for (const filePath of dirResults) {
|
|
265
|
+
if (!seen.has(filePath)) {
|
|
266
|
+
seen.add(filePath);
|
|
267
|
+
if (isIgnored && isIgnored(filePath)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
results.push(filePath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return results.sort();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/taskStore/tasks.ts
|
|
278
|
+
async function ensureDirectoryForFile(rootDir, relativeFilePath) {
|
|
279
|
+
const normalized = ensureMarkdownExtension(normalizeRelativePath(relativeFilePath));
|
|
280
|
+
const absolutePath = path5.join(rootDir, normalized);
|
|
281
|
+
const directory = path5.dirname(absolutePath);
|
|
282
|
+
await fs4.mkdir(directory, { recursive: true });
|
|
283
|
+
return normalized;
|
|
284
|
+
}
|
|
285
|
+
async function nextAvailablePath(rootDir, directory, title) {
|
|
286
|
+
const safeDirectory = directory ? normalizeRelativePath(directory) : "";
|
|
287
|
+
const slug = slugify(title);
|
|
288
|
+
const base = safeDirectory ? `${safeDirectory}/${slug}` : slug;
|
|
289
|
+
let attempt = 0;
|
|
290
|
+
while (true) {
|
|
291
|
+
const candidate = ensureMarkdownExtension(attempt === 0 ? base : `${base}-${attempt + 1}`);
|
|
292
|
+
try {
|
|
293
|
+
await fs4.access(path5.join(rootDir, candidate));
|
|
294
|
+
attempt += 1;
|
|
295
|
+
} catch {
|
|
296
|
+
return candidate;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
227
300
|
async function listTasks(rootDir) {
|
|
228
301
|
const config = await readConfig(rootDir);
|
|
229
302
|
const files = await listMarkdownFiles(rootDir, config.taskDirs, config.ignorePaths);
|
|
@@ -257,46 +330,27 @@ async function listTasks(rootDir) {
|
|
|
257
330
|
});
|
|
258
331
|
return { tasks: taskRecords, errors };
|
|
259
332
|
}
|
|
260
|
-
async function ensureDirectoryForFile(rootDir, relativeFilePath) {
|
|
261
|
-
const normalized = ensureMarkdownExtension(normalizeRelativePath(relativeFilePath));
|
|
262
|
-
const absolutePath = path.join(rootDir, normalized);
|
|
263
|
-
const directory = path.dirname(absolutePath);
|
|
264
|
-
await fs.mkdir(directory, { recursive: true });
|
|
265
|
-
return normalized;
|
|
266
|
-
}
|
|
267
|
-
async function nextAvailablePath(rootDir, directory, title) {
|
|
268
|
-
const safeDirectory = directory ? normalizeRelativePath(directory) : "";
|
|
269
|
-
const slug = slugify(title);
|
|
270
|
-
const base = safeDirectory ? `${safeDirectory}/${slug}` : slug;
|
|
271
|
-
let attempt = 0;
|
|
272
|
-
while (true) {
|
|
273
|
-
const candidate = ensureMarkdownExtension(attempt === 0 ? base : `${base}-${attempt + 1}`);
|
|
274
|
-
try {
|
|
275
|
-
await fs.access(path.join(rootDir, candidate));
|
|
276
|
-
attempt += 1;
|
|
277
|
-
} catch {
|
|
278
|
-
return candidate;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
333
|
async function createTask(rootDir, input) {
|
|
283
334
|
if (!input.title.trim()) {
|
|
284
335
|
throw new ValidationError("Title is required.");
|
|
285
336
|
}
|
|
337
|
+
const status = input.status !== void 0 ? ensureRequiredStatus(input.status) : "TODO";
|
|
286
338
|
const now = asUtcISOString(/* @__PURE__ */ new Date());
|
|
287
339
|
const relativePath = input.path?.trim() ? await ensureDirectoryForFile(rootDir, input.path) : await nextAvailablePath(rootDir, input.directory ?? "", input.title);
|
|
288
|
-
const absolutePath =
|
|
340
|
+
const absolutePath = path5.join(rootDir, relativePath);
|
|
341
|
+
let targetExists = false;
|
|
289
342
|
try {
|
|
290
|
-
await
|
|
343
|
+
await fs4.access(absolutePath);
|
|
344
|
+
targetExists = true;
|
|
291
345
|
} catch (error) {
|
|
292
346
|
const maybeError = error;
|
|
293
|
-
if (maybeError.code
|
|
294
|
-
} else if (maybeError.code) {
|
|
347
|
+
if (maybeError.code !== "ENOENT") {
|
|
295
348
|
throw error;
|
|
296
|
-
} else {
|
|
297
|
-
throw new ValidationError("A task already exists at that path.");
|
|
298
349
|
}
|
|
299
350
|
}
|
|
351
|
+
if (targetExists) {
|
|
352
|
+
throw new ValidationError("A task already exists at that path.");
|
|
353
|
+
}
|
|
300
354
|
const record = {
|
|
301
355
|
path: relativePath,
|
|
302
356
|
content: input.content ?? "",
|
|
@@ -306,20 +360,21 @@ async function createTask(rootDir, input) {
|
|
|
306
360
|
frontmatter: {
|
|
307
361
|
title: input.title.trim(),
|
|
308
362
|
priority: input.priority ?? "MUST",
|
|
309
|
-
status
|
|
363
|
+
status,
|
|
310
364
|
createdAt: now,
|
|
311
365
|
updatedAt: now
|
|
312
366
|
}
|
|
313
367
|
};
|
|
314
|
-
await
|
|
315
|
-
await
|
|
316
|
-
const
|
|
317
|
-
|
|
368
|
+
await fs4.mkdir(path5.dirname(absolutePath), { recursive: true });
|
|
369
|
+
await fs4.writeFile(absolutePath, serializeTask(record), "utf8");
|
|
370
|
+
const config = await readConfig(rootDir);
|
|
371
|
+
const filteredOrder = config.order.filter((item) => item !== relativePath);
|
|
372
|
+
await saveOrder(rootDir, [relativePath, ...filteredOrder]);
|
|
318
373
|
return parseTask(rootDir, relativePath);
|
|
319
374
|
}
|
|
320
375
|
async function updateTask(rootDir, currentPath, input) {
|
|
321
376
|
const normalizedCurrentPath = ensureMarkdownExtension(normalizeRelativePath(currentPath));
|
|
322
|
-
const absoluteCurrentPath =
|
|
377
|
+
const absoluteCurrentPath = path5.join(rootDir, normalizedCurrentPath);
|
|
323
378
|
let existing;
|
|
324
379
|
try {
|
|
325
380
|
existing = await parseTask(rootDir, normalizedCurrentPath);
|
|
@@ -333,20 +388,23 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
333
388
|
if (input.baseUpdatedAt && existing.frontmatter.updatedAt !== input.baseUpdatedAt) {
|
|
334
389
|
throw new ConflictError("The task changed on disk. Reload before saving.");
|
|
335
390
|
}
|
|
391
|
+
const status = ensureRequiredStatus(input.status);
|
|
336
392
|
const nextPath = input.path?.trim() ? await ensureDirectoryForFile(rootDir, input.path) : normalizedCurrentPath;
|
|
337
|
-
const absoluteNextPath =
|
|
393
|
+
const absoluteNextPath = path5.join(rootDir, nextPath);
|
|
338
394
|
if (nextPath !== normalizedCurrentPath) {
|
|
395
|
+
let targetExists = false;
|
|
339
396
|
try {
|
|
340
|
-
await
|
|
397
|
+
await fs4.access(absoluteNextPath);
|
|
398
|
+
targetExists = true;
|
|
341
399
|
} catch (error) {
|
|
342
400
|
const maybeError = error;
|
|
343
|
-
if (maybeError.code
|
|
344
|
-
} else if (maybeError.code) {
|
|
401
|
+
if (maybeError.code !== "ENOENT") {
|
|
345
402
|
throw error;
|
|
346
|
-
} else {
|
|
347
|
-
throw new ValidationError("A task already exists at the target path.");
|
|
348
403
|
}
|
|
349
404
|
}
|
|
405
|
+
if (targetExists) {
|
|
406
|
+
throw new ValidationError("A task already exists at the target path.");
|
|
407
|
+
}
|
|
350
408
|
}
|
|
351
409
|
const record = {
|
|
352
410
|
path: nextPath,
|
|
@@ -357,15 +415,15 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
357
415
|
frontmatter: {
|
|
358
416
|
title: input.title.trim(),
|
|
359
417
|
priority: input.priority,
|
|
360
|
-
status
|
|
418
|
+
status,
|
|
361
419
|
createdAt: existing.frontmatter.createdAt,
|
|
362
420
|
updatedAt: asUtcISOString(/* @__PURE__ */ new Date())
|
|
363
421
|
}
|
|
364
422
|
};
|
|
365
|
-
await
|
|
423
|
+
await fs4.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
|
|
366
424
|
if (nextPath !== normalizedCurrentPath) {
|
|
367
|
-
await
|
|
368
|
-
await
|
|
425
|
+
await fs4.mkdir(path5.dirname(absoluteNextPath), { recursive: true });
|
|
426
|
+
await fs4.rename(absoluteCurrentPath, absoluteNextPath);
|
|
369
427
|
}
|
|
370
428
|
if (nextPath !== normalizedCurrentPath) {
|
|
371
429
|
const config = await readConfig(rootDir);
|
|
@@ -384,9 +442,9 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
384
442
|
}
|
|
385
443
|
async function deleteTask(rootDir, relativePath) {
|
|
386
444
|
const normalizedPath = ensureMarkdownExtension(normalizeRelativePath(relativePath));
|
|
387
|
-
const absolutePath =
|
|
445
|
+
const absolutePath = path5.join(rootDir, normalizedPath);
|
|
388
446
|
try {
|
|
389
|
-
await
|
|
447
|
+
await fs4.unlink(absolutePath);
|
|
390
448
|
} catch (error) {
|
|
391
449
|
const maybeError = error;
|
|
392
450
|
if (maybeError.code === "ENOENT") {
|
|
@@ -402,7 +460,7 @@ async function deleteTask(rootDir, relativePath) {
|
|
|
402
460
|
}
|
|
403
461
|
async function patchTaskFields(rootDir, currentPath, input) {
|
|
404
462
|
const normalizedCurrentPath = ensureMarkdownExtension(normalizeRelativePath(currentPath));
|
|
405
|
-
const absoluteCurrentPath =
|
|
463
|
+
const absoluteCurrentPath = path5.join(rootDir, normalizedCurrentPath);
|
|
406
464
|
let existing;
|
|
407
465
|
try {
|
|
408
466
|
existing = await parseTask(rootDir, normalizedCurrentPath);
|
|
@@ -413,8 +471,8 @@ async function patchTaskFields(rootDir, currentPath, input) {
|
|
|
413
471
|
}
|
|
414
472
|
throw error;
|
|
415
473
|
}
|
|
416
|
-
const priority = input.priority &&
|
|
417
|
-
const status = input.status &&
|
|
474
|
+
const priority = input.priority && isValidPriority(input.priority) ? input.priority : existing.frontmatter.priority;
|
|
475
|
+
const status = input.status && isValidStatus(input.status) ? input.status : existing.frontmatter.status;
|
|
418
476
|
if (priority === existing.frontmatter.priority && status === existing.frontmatter.status) {
|
|
419
477
|
return existing;
|
|
420
478
|
}
|
|
@@ -431,19 +489,13 @@ async function patchTaskFields(rootDir, currentPath, input) {
|
|
|
431
489
|
updatedAt: asUtcISOString(/* @__PURE__ */ new Date())
|
|
432
490
|
}
|
|
433
491
|
};
|
|
434
|
-
await
|
|
492
|
+
await fs4.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
|
|
435
493
|
return parseTask(rootDir, normalizedCurrentPath);
|
|
436
494
|
}
|
|
437
|
-
function parseOrderPayload(input) {
|
|
438
|
-
if (!Array.isArray(input)) {
|
|
439
|
-
throw new ValidationError("Order payload must be an array.");
|
|
440
|
-
}
|
|
441
|
-
return input.map((item) => ensureMarkdownExtension(normalizeRelativePath(String(item))));
|
|
442
|
-
}
|
|
443
495
|
|
|
444
496
|
// src/commandExecutor.ts
|
|
445
497
|
import { spawn } from "child_process";
|
|
446
|
-
import
|
|
498
|
+
import path6 from "path";
|
|
447
499
|
var TIMEOUT_MS = 3e4;
|
|
448
500
|
var VARIABLE_PATTERN = /\$\{?(TASK_TITLE|TASK_FILEPATH|TASK_BODY)\}?/g;
|
|
449
501
|
function substituteVariables(command, vars) {
|
|
@@ -453,7 +505,7 @@ async function executeCommandPipeline(rootDir, steps, task) {
|
|
|
453
505
|
if (steps.length === 0) {
|
|
454
506
|
return { stdout: "", stderr: "", exitCode: 0, duration: 0 };
|
|
455
507
|
}
|
|
456
|
-
const absoluteFilePath =
|
|
508
|
+
const absoluteFilePath = path6.resolve(rootDir, task.path);
|
|
457
509
|
const vars = {
|
|
458
510
|
TASK_TITLE: task.frontmatter.title,
|
|
459
511
|
TASK_FILEPATH: absoluteFilePath,
|
|
@@ -531,7 +583,7 @@ async function executeCommandPipeline(rootDir, steps, task) {
|
|
|
531
583
|
|
|
532
584
|
// src/server.ts
|
|
533
585
|
var __filename = fileURLToPath(import.meta.url);
|
|
534
|
-
var __dirname =
|
|
586
|
+
var __dirname = path7.dirname(__filename);
|
|
535
587
|
function resolveClientDir(explicitClientDir) {
|
|
536
588
|
if (explicitClientDir === null) {
|
|
537
589
|
return null;
|
|
@@ -539,7 +591,7 @@ function resolveClientDir(explicitClientDir) {
|
|
|
539
591
|
if (explicitClientDir) {
|
|
540
592
|
return explicitClientDir;
|
|
541
593
|
}
|
|
542
|
-
return
|
|
594
|
+
return path7.resolve(__dirname, "client");
|
|
543
595
|
}
|
|
544
596
|
function sendJsonError(reply, error) {
|
|
545
597
|
if (error instanceof ValidationError) {
|
|
@@ -553,7 +605,7 @@ function sendJsonError(reply, error) {
|
|
|
553
605
|
reply.code(500).send({ error: error instanceof Error ? error.message : "Internal server error" });
|
|
554
606
|
}
|
|
555
607
|
async function createServer(options) {
|
|
556
|
-
const app = Fastify({ logger: false });
|
|
608
|
+
const app = Fastify({ logger: false, forceCloseConnections: true });
|
|
557
609
|
const listeners = /* @__PURE__ */ new Set();
|
|
558
610
|
const clientDir = resolveClientDir(options.clientDir);
|
|
559
611
|
app.addHook("onClose", async () => {
|
|
@@ -702,18 +754,18 @@ async function createServer(options) {
|
|
|
702
754
|
});
|
|
703
755
|
const watcher = chokidar.watch(options.rootDir, {
|
|
704
756
|
ignoreInitial: true,
|
|
705
|
-
ignored: (watchPath) => watchPath.includes(`${
|
|
757
|
+
ignored: (watchPath) => watchPath.includes(`${path7.sep}.git`) || watchPath.includes(`${path7.sep}node_modules`)
|
|
706
758
|
});
|
|
707
759
|
watcher.on("all", (eventName, changedPath) => {
|
|
708
760
|
const isMarkdown = changedPath.endsWith(".md") || changedPath.endsWith(".markdown");
|
|
709
|
-
const isConfigFile =
|
|
761
|
+
const isConfigFile = path7.basename(changedPath) === ".md-task-viewer.json";
|
|
710
762
|
if (!isMarkdown && !isConfigFile) {
|
|
711
763
|
return;
|
|
712
764
|
}
|
|
713
765
|
const payload = JSON.stringify({
|
|
714
766
|
type: "tasks-changed",
|
|
715
767
|
eventName,
|
|
716
|
-
path:
|
|
768
|
+
path: path7.relative(options.rootDir, changedPath)
|
|
717
769
|
});
|
|
718
770
|
for (const listener of listeners) {
|
|
719
771
|
listener.send(payload);
|
|
@@ -760,7 +812,7 @@ function parseArgs(argv) {
|
|
|
760
812
|
continue;
|
|
761
813
|
}
|
|
762
814
|
if (!current.startsWith("--")) {
|
|
763
|
-
rootDir =
|
|
815
|
+
rootDir = path8.resolve(current);
|
|
764
816
|
}
|
|
765
817
|
}
|
|
766
818
|
return { rootDir, port, host, shouldOpen };
|
|
@@ -780,9 +832,31 @@ URL: ${browserUrl}
|
|
|
780
832
|
if (options.shouldOpen) {
|
|
781
833
|
await open(browserUrl);
|
|
782
834
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
835
|
+
let shuttingDown = false;
|
|
836
|
+
const SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
837
|
+
const shutdown = (signal) => {
|
|
838
|
+
if (shuttingDown) {
|
|
839
|
+
process.stderr.write(`
|
|
840
|
+
Received ${signal} again \u2014 forcing exit.
|
|
841
|
+
`);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
shuttingDown = true;
|
|
845
|
+
const timer = setTimeout(() => {
|
|
846
|
+
process.stderr.write(`
|
|
847
|
+
Graceful shutdown timed out after ${SHUTDOWN_TIMEOUT_MS}ms \u2014 forcing exit.
|
|
848
|
+
`);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
851
|
+
timer.unref();
|
|
852
|
+
app.close().then(
|
|
853
|
+
() => process.exit(0),
|
|
854
|
+
(error) => {
|
|
855
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
856
|
+
`);
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
);
|
|
786
860
|
};
|
|
787
861
|
process.on("SIGINT", shutdown);
|
|
788
862
|
process.on("SIGTERM", shutdown);
|