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/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Markdown Task Viewer</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-j5ntXWRS.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CSNRoovR.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/dist/server.js
CHANGED
|
@@ -2,32 +2,25 @@
|
|
|
2
2
|
import Fastify from "fastify";
|
|
3
3
|
import fastifyStatic from "@fastify/static";
|
|
4
4
|
import chokidar from "chokidar";
|
|
5
|
-
import
|
|
5
|
+
import path7 from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
|
-
// src/taskStore.ts
|
|
9
|
-
import matter from "gray-matter";
|
|
10
|
-
import picomatch from "picomatch";
|
|
11
|
-
import path from "path";
|
|
12
|
-
import { promises as fs } from "fs";
|
|
13
|
-
|
|
14
|
-
// src/types.ts
|
|
15
|
-
var CONFIG_FILE_NAME = ".md-task-viewer.json";
|
|
16
|
-
|
|
17
8
|
// src/slugify.ts
|
|
18
9
|
function slugify(value) {
|
|
19
10
|
const slug = value.normalize("NFC").replace(/[\s\u3000]+/g, "-").replace(/[^\p{L}\p{N}-]+/gu, "").replace(/^-+|-+$/g, "");
|
|
20
11
|
return slug || "untitled-task";
|
|
21
12
|
}
|
|
22
13
|
|
|
23
|
-
// src/taskStore.ts
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
// src/taskStore/paths.ts
|
|
15
|
+
import path from "path";
|
|
16
|
+
|
|
17
|
+
// src/taskStore/errors.ts
|
|
27
18
|
var ConflictError = class extends Error {
|
|
28
19
|
};
|
|
29
20
|
var ValidationError = class extends Error {
|
|
30
21
|
};
|
|
22
|
+
|
|
23
|
+
// src/taskStore/paths.ts
|
|
31
24
|
function toPosixPath(filePath) {
|
|
32
25
|
return filePath.split(path.sep).join("/");
|
|
33
26
|
}
|
|
@@ -41,11 +34,30 @@ function normalizeRelativePath(candidate) {
|
|
|
41
34
|
function ensureMarkdownExtension(filePath) {
|
|
42
35
|
return path.posix.extname(filePath) ? filePath : `${filePath}.md`;
|
|
43
36
|
}
|
|
37
|
+
|
|
38
|
+
// src/taskStore/frontmatter.ts
|
|
39
|
+
import matter from "gray-matter";
|
|
40
|
+
import path2 from "path";
|
|
41
|
+
import { promises as fs } from "fs";
|
|
42
|
+
var REQUIRED_PRIORITY = ["MUST", "WANT"];
|
|
43
|
+
var REQUIRED_STATUS = ["TODO", "DONE"];
|
|
44
|
+
function isValidPriority(value) {
|
|
45
|
+
return REQUIRED_PRIORITY.includes(value);
|
|
46
|
+
}
|
|
47
|
+
function isValidStatus(value) {
|
|
48
|
+
return REQUIRED_STATUS.includes(value);
|
|
49
|
+
}
|
|
50
|
+
function ensureRequiredStatus(status) {
|
|
51
|
+
if (!isValidStatus(status)) {
|
|
52
|
+
throw new ValidationError("Status must be TODO or DONE.");
|
|
53
|
+
}
|
|
54
|
+
return status;
|
|
55
|
+
}
|
|
44
56
|
function asUtcISOString(date) {
|
|
45
57
|
return date.toISOString();
|
|
46
58
|
}
|
|
47
59
|
function buildDefaults(filePath, stats) {
|
|
48
|
-
const basename =
|
|
60
|
+
const basename = path2.basename(filePath, path2.extname(filePath));
|
|
49
61
|
const title = basename.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
50
62
|
return {
|
|
51
63
|
title,
|
|
@@ -63,8 +75,8 @@ function splitFrontmatter(data, statsDefaults) {
|
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
const title = typeof data.title === "string" && data.title.trim() ? data.title : statsDefaults.title;
|
|
66
|
-
const priority =
|
|
67
|
-
const status =
|
|
78
|
+
const priority = isValidPriority(data.priority) ? data.priority : statsDefaults.priority;
|
|
79
|
+
const status = isValidStatus(data.status) ? data.status : statsDefaults.status;
|
|
68
80
|
const createdAt = typeof data.createdAt === "string" && !Number.isNaN(Date.parse(data.createdAt)) ? new Date(data.createdAt).toISOString() : statsDefaults.createdAt;
|
|
69
81
|
const updatedAt = typeof data.updatedAt === "string" && !Number.isNaN(Date.parse(data.updatedAt)) ? new Date(data.updatedAt).toISOString() : statsDefaults.updatedAt;
|
|
70
82
|
const normalized = title !== data.title || priority !== data.priority || status !== data.status || createdAt !== data.createdAt || updatedAt !== data.updatedAt;
|
|
@@ -85,53 +97,8 @@ function serializeTask(record) {
|
|
|
85
97
|
};
|
|
86
98
|
return matter.stringify(record.content, data);
|
|
87
99
|
}
|
|
88
|
-
async function readDirectoryRecursive(rootDir, currentDir, results) {
|
|
89
|
-
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
90
|
-
for (const entry of entries) {
|
|
91
|
-
if (entry.name === ".git" || entry.name === "node_modules") {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const absolutePath = path.join(currentDir, entry.name);
|
|
95
|
-
if (entry.isDirectory()) {
|
|
96
|
-
await readDirectoryRecursive(rootDir, absolutePath, results);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (entry.name === CONFIG_FILE_NAME) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (!MARKDOWN_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
results.push(toPosixPath(path.relative(rootDir, absolutePath)));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function listMarkdownFiles(rootDir, taskDirs, ignorePaths) {
|
|
109
|
-
const results = [];
|
|
110
|
-
const seen = /* @__PURE__ */ new Set();
|
|
111
|
-
const isIgnored = ignorePaths.length > 0 ? picomatch(ignorePaths) : null;
|
|
112
|
-
for (const taskDir of taskDirs) {
|
|
113
|
-
const scanDir = path.resolve(rootDir, taskDir);
|
|
114
|
-
try {
|
|
115
|
-
await fs.access(scanDir);
|
|
116
|
-
} catch {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const dirResults = [];
|
|
120
|
-
await readDirectoryRecursive(rootDir, scanDir, dirResults);
|
|
121
|
-
for (const filePath of dirResults) {
|
|
122
|
-
if (!seen.has(filePath)) {
|
|
123
|
-
seen.add(filePath);
|
|
124
|
-
if (isIgnored && isIgnored(filePath)) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
results.push(filePath);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return results.sort();
|
|
132
|
-
}
|
|
133
100
|
async function parseTask(rootDir, relativePath) {
|
|
134
|
-
const absolutePath =
|
|
101
|
+
const absolutePath = path2.join(rootDir, relativePath);
|
|
135
102
|
const raw = await fs.readFile(absolutePath, "utf8");
|
|
136
103
|
const stats = await fs.stat(absolutePath);
|
|
137
104
|
const parsed = matter(raw);
|
|
@@ -146,10 +113,19 @@ async function parseTask(rootDir, relativePath) {
|
|
|
146
113
|
normalized
|
|
147
114
|
};
|
|
148
115
|
}
|
|
116
|
+
|
|
117
|
+
// src/taskStore/config.ts
|
|
118
|
+
import path3 from "path";
|
|
119
|
+
import { promises as fs2 } from "fs";
|
|
120
|
+
|
|
121
|
+
// src/types.ts
|
|
122
|
+
var CONFIG_FILE_NAME = ".md-task-viewer.json";
|
|
123
|
+
|
|
124
|
+
// src/taskStore/config.ts
|
|
149
125
|
async function readConfig(rootDir) {
|
|
150
|
-
const configFilePath =
|
|
126
|
+
const configFilePath = path3.join(rootDir, CONFIG_FILE_NAME);
|
|
151
127
|
try {
|
|
152
|
-
const raw = await
|
|
128
|
+
const raw = await fs2.readFile(configFilePath, "utf8");
|
|
153
129
|
const parsed = JSON.parse(raw);
|
|
154
130
|
const taskDirs = Array.isArray(parsed.taskDirs) ? parsed.taskDirs.filter((item) => typeof item === "string") : ["."];
|
|
155
131
|
const ignorePaths = Array.isArray(parsed.ignorePaths) ? parsed.ignorePaths.filter((item) => typeof item === "string") : [];
|
|
@@ -194,8 +170,14 @@ async function saveOrder(rootDir, order) {
|
|
|
194
170
|
)
|
|
195
171
|
);
|
|
196
172
|
const existing = await readConfig(rootDir);
|
|
197
|
-
const payload = {
|
|
198
|
-
|
|
173
|
+
const payload = {
|
|
174
|
+
version: 1,
|
|
175
|
+
taskDirs: existing.taskDirs,
|
|
176
|
+
ignorePaths: existing.ignorePaths,
|
|
177
|
+
order: normalized,
|
|
178
|
+
commands: existing.commands
|
|
179
|
+
};
|
|
180
|
+
await fs2.writeFile(path3.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
|
|
199
181
|
`, "utf8");
|
|
200
182
|
}
|
|
201
183
|
async function saveConfig(rootDir, taskDirs, ignorePaths, commands) {
|
|
@@ -212,11 +194,102 @@ async function saveConfig(rootDir, taskDirs, ignorePaths, commands) {
|
|
|
212
194
|
const existing = await readConfig(rootDir);
|
|
213
195
|
const validatedIgnorePaths = ignorePaths ?? existing.ignorePaths;
|
|
214
196
|
const validatedCommands = commands !== void 0 ? commands : existing.commands;
|
|
215
|
-
const payload = {
|
|
216
|
-
|
|
197
|
+
const payload = {
|
|
198
|
+
version: 1,
|
|
199
|
+
taskDirs: validated,
|
|
200
|
+
ignorePaths: validatedIgnorePaths,
|
|
201
|
+
order: existing.order,
|
|
202
|
+
commands: validatedCommands
|
|
203
|
+
};
|
|
204
|
+
await fs2.writeFile(path3.join(rootDir, CONFIG_FILE_NAME), `${JSON.stringify(payload, null, 2)}
|
|
217
205
|
`, "utf8");
|
|
218
206
|
return payload;
|
|
219
207
|
}
|
|
208
|
+
function parseOrderPayload(input) {
|
|
209
|
+
if (!Array.isArray(input)) {
|
|
210
|
+
throw new ValidationError("Order payload must be an array.");
|
|
211
|
+
}
|
|
212
|
+
return input.map((item) => ensureMarkdownExtension(normalizeRelativePath(String(item))));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/taskStore/tasks.ts
|
|
216
|
+
import path5 from "path";
|
|
217
|
+
import { promises as fs4 } from "fs";
|
|
218
|
+
|
|
219
|
+
// src/taskStore/scanner.ts
|
|
220
|
+
import picomatch from "picomatch";
|
|
221
|
+
import path4 from "path";
|
|
222
|
+
import { promises as fs3 } from "fs";
|
|
223
|
+
var MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
224
|
+
async function readDirectoryRecursive(rootDir, currentDir, results) {
|
|
225
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entry.name === ".git" || entry.name === "node_modules") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const absolutePath = path4.join(currentDir, entry.name);
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
await readDirectoryRecursive(rootDir, absolutePath, results);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (entry.name === CONFIG_FILE_NAME) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!MARKDOWN_EXTENSIONS.has(path4.extname(entry.name).toLowerCase())) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
results.push(toPosixPath(path4.relative(rootDir, absolutePath)));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function listMarkdownFiles(rootDir, taskDirs, ignorePaths) {
|
|
245
|
+
const results = [];
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
const isIgnored = ignorePaths.length > 0 ? picomatch(ignorePaths) : null;
|
|
248
|
+
for (const taskDir of taskDirs) {
|
|
249
|
+
const scanDir = path4.resolve(rootDir, taskDir);
|
|
250
|
+
try {
|
|
251
|
+
await fs3.access(scanDir);
|
|
252
|
+
} catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const dirResults = [];
|
|
256
|
+
await readDirectoryRecursive(rootDir, scanDir, dirResults);
|
|
257
|
+
for (const filePath of dirResults) {
|
|
258
|
+
if (!seen.has(filePath)) {
|
|
259
|
+
seen.add(filePath);
|
|
260
|
+
if (isIgnored && isIgnored(filePath)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
results.push(filePath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return results.sort();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/taskStore/tasks.ts
|
|
271
|
+
async function ensureDirectoryForFile(rootDir, relativeFilePath) {
|
|
272
|
+
const normalized = ensureMarkdownExtension(normalizeRelativePath(relativeFilePath));
|
|
273
|
+
const absolutePath = path5.join(rootDir, normalized);
|
|
274
|
+
const directory = path5.dirname(absolutePath);
|
|
275
|
+
await fs4.mkdir(directory, { recursive: true });
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
async function nextAvailablePath(rootDir, directory, title) {
|
|
279
|
+
const safeDirectory = directory ? normalizeRelativePath(directory) : "";
|
|
280
|
+
const slug = slugify(title);
|
|
281
|
+
const base = safeDirectory ? `${safeDirectory}/${slug}` : slug;
|
|
282
|
+
let attempt = 0;
|
|
283
|
+
while (true) {
|
|
284
|
+
const candidate = ensureMarkdownExtension(attempt === 0 ? base : `${base}-${attempt + 1}`);
|
|
285
|
+
try {
|
|
286
|
+
await fs4.access(path5.join(rootDir, candidate));
|
|
287
|
+
attempt += 1;
|
|
288
|
+
} catch {
|
|
289
|
+
return candidate;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
220
293
|
async function listTasks(rootDir) {
|
|
221
294
|
const config = await readConfig(rootDir);
|
|
222
295
|
const files = await listMarkdownFiles(rootDir, config.taskDirs, config.ignorePaths);
|
|
@@ -250,46 +323,27 @@ async function listTasks(rootDir) {
|
|
|
250
323
|
});
|
|
251
324
|
return { tasks: taskRecords, errors };
|
|
252
325
|
}
|
|
253
|
-
async function ensureDirectoryForFile(rootDir, relativeFilePath) {
|
|
254
|
-
const normalized = ensureMarkdownExtension(normalizeRelativePath(relativeFilePath));
|
|
255
|
-
const absolutePath = path.join(rootDir, normalized);
|
|
256
|
-
const directory = path.dirname(absolutePath);
|
|
257
|
-
await fs.mkdir(directory, { recursive: true });
|
|
258
|
-
return normalized;
|
|
259
|
-
}
|
|
260
|
-
async function nextAvailablePath(rootDir, directory, title) {
|
|
261
|
-
const safeDirectory = directory ? normalizeRelativePath(directory) : "";
|
|
262
|
-
const slug = slugify(title);
|
|
263
|
-
const base = safeDirectory ? `${safeDirectory}/${slug}` : slug;
|
|
264
|
-
let attempt = 0;
|
|
265
|
-
while (true) {
|
|
266
|
-
const candidate = ensureMarkdownExtension(attempt === 0 ? base : `${base}-${attempt + 1}`);
|
|
267
|
-
try {
|
|
268
|
-
await fs.access(path.join(rootDir, candidate));
|
|
269
|
-
attempt += 1;
|
|
270
|
-
} catch {
|
|
271
|
-
return candidate;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
326
|
async function createTask(rootDir, input) {
|
|
276
327
|
if (!input.title.trim()) {
|
|
277
328
|
throw new ValidationError("Title is required.");
|
|
278
329
|
}
|
|
330
|
+
const status = input.status !== void 0 ? ensureRequiredStatus(input.status) : "TODO";
|
|
279
331
|
const now = asUtcISOString(/* @__PURE__ */ new Date());
|
|
280
332
|
const relativePath = input.path?.trim() ? await ensureDirectoryForFile(rootDir, input.path) : await nextAvailablePath(rootDir, input.directory ?? "", input.title);
|
|
281
|
-
const absolutePath =
|
|
333
|
+
const absolutePath = path5.join(rootDir, relativePath);
|
|
334
|
+
let targetExists = false;
|
|
282
335
|
try {
|
|
283
|
-
await
|
|
336
|
+
await fs4.access(absolutePath);
|
|
337
|
+
targetExists = true;
|
|
284
338
|
} catch (error) {
|
|
285
339
|
const maybeError = error;
|
|
286
|
-
if (maybeError.code
|
|
287
|
-
} else if (maybeError.code) {
|
|
340
|
+
if (maybeError.code !== "ENOENT") {
|
|
288
341
|
throw error;
|
|
289
|
-
} else {
|
|
290
|
-
throw new ValidationError("A task already exists at that path.");
|
|
291
342
|
}
|
|
292
343
|
}
|
|
344
|
+
if (targetExists) {
|
|
345
|
+
throw new ValidationError("A task already exists at that path.");
|
|
346
|
+
}
|
|
293
347
|
const record = {
|
|
294
348
|
path: relativePath,
|
|
295
349
|
content: input.content ?? "",
|
|
@@ -299,20 +353,21 @@ async function createTask(rootDir, input) {
|
|
|
299
353
|
frontmatter: {
|
|
300
354
|
title: input.title.trim(),
|
|
301
355
|
priority: input.priority ?? "MUST",
|
|
302
|
-
status
|
|
356
|
+
status,
|
|
303
357
|
createdAt: now,
|
|
304
358
|
updatedAt: now
|
|
305
359
|
}
|
|
306
360
|
};
|
|
307
|
-
await
|
|
308
|
-
await
|
|
309
|
-
const
|
|
310
|
-
|
|
361
|
+
await fs4.mkdir(path5.dirname(absolutePath), { recursive: true });
|
|
362
|
+
await fs4.writeFile(absolutePath, serializeTask(record), "utf8");
|
|
363
|
+
const config = await readConfig(rootDir);
|
|
364
|
+
const filteredOrder = config.order.filter((item) => item !== relativePath);
|
|
365
|
+
await saveOrder(rootDir, [relativePath, ...filteredOrder]);
|
|
311
366
|
return parseTask(rootDir, relativePath);
|
|
312
367
|
}
|
|
313
368
|
async function updateTask(rootDir, currentPath, input) {
|
|
314
369
|
const normalizedCurrentPath = ensureMarkdownExtension(normalizeRelativePath(currentPath));
|
|
315
|
-
const absoluteCurrentPath =
|
|
370
|
+
const absoluteCurrentPath = path5.join(rootDir, normalizedCurrentPath);
|
|
316
371
|
let existing;
|
|
317
372
|
try {
|
|
318
373
|
existing = await parseTask(rootDir, normalizedCurrentPath);
|
|
@@ -326,20 +381,23 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
326
381
|
if (input.baseUpdatedAt && existing.frontmatter.updatedAt !== input.baseUpdatedAt) {
|
|
327
382
|
throw new ConflictError("The task changed on disk. Reload before saving.");
|
|
328
383
|
}
|
|
384
|
+
const status = ensureRequiredStatus(input.status);
|
|
329
385
|
const nextPath = input.path?.trim() ? await ensureDirectoryForFile(rootDir, input.path) : normalizedCurrentPath;
|
|
330
|
-
const absoluteNextPath =
|
|
386
|
+
const absoluteNextPath = path5.join(rootDir, nextPath);
|
|
331
387
|
if (nextPath !== normalizedCurrentPath) {
|
|
388
|
+
let targetExists = false;
|
|
332
389
|
try {
|
|
333
|
-
await
|
|
390
|
+
await fs4.access(absoluteNextPath);
|
|
391
|
+
targetExists = true;
|
|
334
392
|
} catch (error) {
|
|
335
393
|
const maybeError = error;
|
|
336
|
-
if (maybeError.code
|
|
337
|
-
} else if (maybeError.code) {
|
|
394
|
+
if (maybeError.code !== "ENOENT") {
|
|
338
395
|
throw error;
|
|
339
|
-
} else {
|
|
340
|
-
throw new ValidationError("A task already exists at the target path.");
|
|
341
396
|
}
|
|
342
397
|
}
|
|
398
|
+
if (targetExists) {
|
|
399
|
+
throw new ValidationError("A task already exists at the target path.");
|
|
400
|
+
}
|
|
343
401
|
}
|
|
344
402
|
const record = {
|
|
345
403
|
path: nextPath,
|
|
@@ -350,15 +408,15 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
350
408
|
frontmatter: {
|
|
351
409
|
title: input.title.trim(),
|
|
352
410
|
priority: input.priority,
|
|
353
|
-
status
|
|
411
|
+
status,
|
|
354
412
|
createdAt: existing.frontmatter.createdAt,
|
|
355
413
|
updatedAt: asUtcISOString(/* @__PURE__ */ new Date())
|
|
356
414
|
}
|
|
357
415
|
};
|
|
358
|
-
await
|
|
416
|
+
await fs4.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
|
|
359
417
|
if (nextPath !== normalizedCurrentPath) {
|
|
360
|
-
await
|
|
361
|
-
await
|
|
418
|
+
await fs4.mkdir(path5.dirname(absoluteNextPath), { recursive: true });
|
|
419
|
+
await fs4.rename(absoluteCurrentPath, absoluteNextPath);
|
|
362
420
|
}
|
|
363
421
|
if (nextPath !== normalizedCurrentPath) {
|
|
364
422
|
const config = await readConfig(rootDir);
|
|
@@ -377,9 +435,9 @@ async function updateTask(rootDir, currentPath, input) {
|
|
|
377
435
|
}
|
|
378
436
|
async function deleteTask(rootDir, relativePath) {
|
|
379
437
|
const normalizedPath = ensureMarkdownExtension(normalizeRelativePath(relativePath));
|
|
380
|
-
const absolutePath =
|
|
438
|
+
const absolutePath = path5.join(rootDir, normalizedPath);
|
|
381
439
|
try {
|
|
382
|
-
await
|
|
440
|
+
await fs4.unlink(absolutePath);
|
|
383
441
|
} catch (error) {
|
|
384
442
|
const maybeError = error;
|
|
385
443
|
if (maybeError.code === "ENOENT") {
|
|
@@ -395,7 +453,7 @@ async function deleteTask(rootDir, relativePath) {
|
|
|
395
453
|
}
|
|
396
454
|
async function patchTaskFields(rootDir, currentPath, input) {
|
|
397
455
|
const normalizedCurrentPath = ensureMarkdownExtension(normalizeRelativePath(currentPath));
|
|
398
|
-
const absoluteCurrentPath =
|
|
456
|
+
const absoluteCurrentPath = path5.join(rootDir, normalizedCurrentPath);
|
|
399
457
|
let existing;
|
|
400
458
|
try {
|
|
401
459
|
existing = await parseTask(rootDir, normalizedCurrentPath);
|
|
@@ -406,8 +464,8 @@ async function patchTaskFields(rootDir, currentPath, input) {
|
|
|
406
464
|
}
|
|
407
465
|
throw error;
|
|
408
466
|
}
|
|
409
|
-
const priority = input.priority &&
|
|
410
|
-
const status = input.status &&
|
|
467
|
+
const priority = input.priority && isValidPriority(input.priority) ? input.priority : existing.frontmatter.priority;
|
|
468
|
+
const status = input.status && isValidStatus(input.status) ? input.status : existing.frontmatter.status;
|
|
411
469
|
if (priority === existing.frontmatter.priority && status === existing.frontmatter.status) {
|
|
412
470
|
return existing;
|
|
413
471
|
}
|
|
@@ -424,19 +482,13 @@ async function patchTaskFields(rootDir, currentPath, input) {
|
|
|
424
482
|
updatedAt: asUtcISOString(/* @__PURE__ */ new Date())
|
|
425
483
|
}
|
|
426
484
|
};
|
|
427
|
-
await
|
|
485
|
+
await fs4.writeFile(absoluteCurrentPath, serializeTask(record), "utf8");
|
|
428
486
|
return parseTask(rootDir, normalizedCurrentPath);
|
|
429
487
|
}
|
|
430
|
-
function parseOrderPayload(input) {
|
|
431
|
-
if (!Array.isArray(input)) {
|
|
432
|
-
throw new ValidationError("Order payload must be an array.");
|
|
433
|
-
}
|
|
434
|
-
return input.map((item) => ensureMarkdownExtension(normalizeRelativePath(String(item))));
|
|
435
|
-
}
|
|
436
488
|
|
|
437
489
|
// src/commandExecutor.ts
|
|
438
490
|
import { spawn } from "child_process";
|
|
439
|
-
import
|
|
491
|
+
import path6 from "path";
|
|
440
492
|
var TIMEOUT_MS = 3e4;
|
|
441
493
|
var VARIABLE_PATTERN = /\$\{?(TASK_TITLE|TASK_FILEPATH|TASK_BODY)\}?/g;
|
|
442
494
|
function substituteVariables(command, vars) {
|
|
@@ -446,7 +498,7 @@ async function executeCommandPipeline(rootDir, steps, task) {
|
|
|
446
498
|
if (steps.length === 0) {
|
|
447
499
|
return { stdout: "", stderr: "", exitCode: 0, duration: 0 };
|
|
448
500
|
}
|
|
449
|
-
const absoluteFilePath =
|
|
501
|
+
const absoluteFilePath = path6.resolve(rootDir, task.path);
|
|
450
502
|
const vars = {
|
|
451
503
|
TASK_TITLE: task.frontmatter.title,
|
|
452
504
|
TASK_FILEPATH: absoluteFilePath,
|
|
@@ -524,7 +576,7 @@ async function executeCommandPipeline(rootDir, steps, task) {
|
|
|
524
576
|
|
|
525
577
|
// src/server.ts
|
|
526
578
|
var __filename = fileURLToPath(import.meta.url);
|
|
527
|
-
var __dirname =
|
|
579
|
+
var __dirname = path7.dirname(__filename);
|
|
528
580
|
function resolveClientDir(explicitClientDir) {
|
|
529
581
|
if (explicitClientDir === null) {
|
|
530
582
|
return null;
|
|
@@ -532,7 +584,7 @@ function resolveClientDir(explicitClientDir) {
|
|
|
532
584
|
if (explicitClientDir) {
|
|
533
585
|
return explicitClientDir;
|
|
534
586
|
}
|
|
535
|
-
return
|
|
587
|
+
return path7.resolve(__dirname, "client");
|
|
536
588
|
}
|
|
537
589
|
function sendJsonError(reply, error) {
|
|
538
590
|
if (error instanceof ValidationError) {
|
|
@@ -546,7 +598,7 @@ function sendJsonError(reply, error) {
|
|
|
546
598
|
reply.code(500).send({ error: error instanceof Error ? error.message : "Internal server error" });
|
|
547
599
|
}
|
|
548
600
|
async function createServer(options) {
|
|
549
|
-
const app = Fastify({ logger: false });
|
|
601
|
+
const app = Fastify({ logger: false, forceCloseConnections: true });
|
|
550
602
|
const listeners = /* @__PURE__ */ new Set();
|
|
551
603
|
const clientDir = resolveClientDir(options.clientDir);
|
|
552
604
|
app.addHook("onClose", async () => {
|
|
@@ -695,18 +747,18 @@ async function createServer(options) {
|
|
|
695
747
|
});
|
|
696
748
|
const watcher = chokidar.watch(options.rootDir, {
|
|
697
749
|
ignoreInitial: true,
|
|
698
|
-
ignored: (watchPath) => watchPath.includes(`${
|
|
750
|
+
ignored: (watchPath) => watchPath.includes(`${path7.sep}.git`) || watchPath.includes(`${path7.sep}node_modules`)
|
|
699
751
|
});
|
|
700
752
|
watcher.on("all", (eventName, changedPath) => {
|
|
701
753
|
const isMarkdown = changedPath.endsWith(".md") || changedPath.endsWith(".markdown");
|
|
702
|
-
const isConfigFile =
|
|
754
|
+
const isConfigFile = path7.basename(changedPath) === ".md-task-viewer.json";
|
|
703
755
|
if (!isMarkdown && !isConfigFile) {
|
|
704
756
|
return;
|
|
705
757
|
}
|
|
706
758
|
const payload = JSON.stringify({
|
|
707
759
|
type: "tasks-changed",
|
|
708
760
|
eventName,
|
|
709
|
-
path:
|
|
761
|
+
path: path7.relative(options.rootDir, changedPath)
|
|
710
762
|
});
|
|
711
763
|
for (const listener of listeners) {
|
|
712
764
|
listener.send(payload);
|