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 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` / `WIP` / `DONE` status
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: WIP
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`, `WIP`, or `DONE`
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 path4 from "path";
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 path3 from "path";
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
- var MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown"]);
32
- var REQUIRED_PRIORITY = ["MUST", "WANT"];
33
- var REQUIRED_STATUS = ["TODO", "WIP", "DONE"];
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 = path.basename(filePath, path.extname(filePath));
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 = REQUIRED_PRIORITY.includes(data.priority) ? data.priority : statsDefaults.priority;
74
- const status = REQUIRED_STATUS.includes(data.status) ? data.status : statsDefaults.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 = path.join(rootDir, relativePath);
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 = path.join(rootDir, CONFIG_FILE_NAME);
133
+ const configFilePath = path3.join(rootDir, CONFIG_FILE_NAME);
158
134
  try {
159
- const raw = await fs.readFile(configFilePath, "utf8");
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 = { version: 1, taskDirs: existing.taskDirs, ignorePaths: existing.ignorePaths, order: normalized, commands: existing.commands };
205
- await fs.writeFile(path.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
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 = { version: 1, taskDirs: validated, ignorePaths: validatedIgnorePaths, order: existing.order, commands: validatedCommands };
223
- await fs.writeFile(path.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
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 = path.join(rootDir, relativePath);
340
+ const absolutePath = path5.join(rootDir, relativePath);
341
+ let targetExists = false;
289
342
  try {
290
- await fs.access(absolutePath);
343
+ await fs4.access(absolutePath);
344
+ targetExists = true;
291
345
  } catch (error) {
292
346
  const maybeError = error;
293
- if (maybeError.code === "ENOENT") {
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: input.status ?? "TODO",
363
+ status,
310
364
  createdAt: now,
311
365
  updatedAt: now
312
366
  }
313
367
  };
314
- await fs.mkdir(path.dirname(absolutePath), { recursive: true });
315
- await fs.writeFile(absolutePath, serializeTask(record), "utf8");
316
- const current = await listTasks(rootDir);
317
- await saveOrder(rootDir, current.tasks.map((task) => task.path).concat(relativePath));
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 = path.join(rootDir, normalizedCurrentPath);
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 = path.join(rootDir, nextPath);
393
+ const absoluteNextPath = path5.join(rootDir, nextPath);
338
394
  if (nextPath !== normalizedCurrentPath) {
395
+ let targetExists = false;
339
396
  try {
340
- await fs.access(absoluteNextPath);
397
+ await fs4.access(absoluteNextPath);
398
+ targetExists = true;
341
399
  } catch (error) {
342
400
  const maybeError = error;
343
- if (maybeError.code === "ENOENT") {
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: input.status,
418
+ status,
361
419
  createdAt: existing.frontmatter.createdAt,
362
420
  updatedAt: asUtcISOString(/* @__PURE__ */ new Date())
363
421
  }
364
422
  };
365
- await fs.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
423
+ await fs4.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
366
424
  if (nextPath !== normalizedCurrentPath) {
367
- await fs.mkdir(path.dirname(absoluteNextPath), { recursive: true });
368
- await fs.rename(absoluteCurrentPath, absoluteNextPath);
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 = path.join(rootDir, normalizedPath);
445
+ const absolutePath = path5.join(rootDir, normalizedPath);
388
446
  try {
389
- await fs.unlink(absolutePath);
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 = path.join(rootDir, normalizedCurrentPath);
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 && REQUIRED_PRIORITY.includes(input.priority) ? input.priority : existing.frontmatter.priority;
417
- const status = input.status && REQUIRED_STATUS.includes(input.status) ? input.status : existing.frontmatter.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 fs.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
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 path2 from "path";
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 = path2.resolve(rootDir, task.path);
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 = path3.dirname(__filename);
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 path3.resolve(__dirname, "client");
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(`${path3.sep}.git`) || watchPath.includes(`${path3.sep}node_modules`)
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 = path3.basename(changedPath) === ".md-task-viewer.json";
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: path3.relative(options.rootDir, changedPath)
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 = path4.resolve(current);
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
- const shutdown = async () => {
784
- await app.close();
785
- process.exit(0);
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);