@jwilger/pi-development-system 0.72.0 → 0.73.0
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/package.json +1 -1
- package/src/tracker/repo-files.ts +36 -3
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { ok } from "../core/result.ts";
|
|
4
4
|
import { parseItem, renderBacklog, renderItem, unwritable } from "./item-format.ts";
|
|
5
5
|
import {
|
|
@@ -36,8 +36,35 @@ const byCodeUnit = (a: string, b: string): number => Number(a > b) - Number(a <
|
|
|
36
36
|
const describe = (cause: unknown): string =>
|
|
37
37
|
cause instanceof Error ? cause.message : String(cause);
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The first symlink on the way from the repo root to `target`, if any. A committed symlink
|
|
41
|
+
* under `work/` could otherwise make a tracker write land outside the repository.
|
|
42
|
+
*/
|
|
43
|
+
const symlinkOnPath = async (root: string, target: string): Promise<string | undefined> => {
|
|
44
|
+
let current = root;
|
|
45
|
+
for (const part of relative(root, target).split(sep)) {
|
|
46
|
+
current = join(current, part);
|
|
47
|
+
try {
|
|
48
|
+
if ((await lstat(current)).isSymbolicLink()) return current;
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
if (isMissing(cause)) return undefined;
|
|
51
|
+
throw cause;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const refuseSymlink = async (root: string, target: string): Promise<string | undefined> => {
|
|
58
|
+
const link = await symlinkOnPath(root, target);
|
|
59
|
+
return link === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: `${relative(root, link)} is a symbolic link; the repo-files tracker will not follow it`;
|
|
62
|
+
};
|
|
63
|
+
|
|
39
64
|
const readItem = async (root: string, id: string): Promise<TrackerResult<WorkItem>> => {
|
|
40
65
|
if (!ID.test(id)) return trackerError(`work item "${id}": not a valid id`);
|
|
66
|
+
const link = await refuseSymlink(root, fileOf(root, id));
|
|
67
|
+
if (link !== undefined) return trackerError(link);
|
|
41
68
|
try {
|
|
42
69
|
return parseItem(id, await readFile(fileOf(root, id), "utf8"));
|
|
43
70
|
} catch (cause) {
|
|
@@ -47,6 +74,7 @@ const readItem = async (root: string, id: string): Promise<TrackerResult<WorkIte
|
|
|
47
74
|
};
|
|
48
75
|
|
|
49
76
|
const itemIds = async (root: string): Promise<string[]> => {
|
|
77
|
+
if ((await refuseSymlink(root, itemsDir(root))) !== undefined) return [];
|
|
50
78
|
try {
|
|
51
79
|
const files = await readdir(itemsDir(root));
|
|
52
80
|
return files.flatMap((f) =>
|
|
@@ -71,6 +99,11 @@ const readAll = async (root: string): Promise<TrackerResult<WorkItem[]>> => {
|
|
|
71
99
|
const save = async (root: string, item: WorkItem): Promise<TrackerResult<WorkItem>> => {
|
|
72
100
|
const problem = unwritable(item);
|
|
73
101
|
if (problem !== undefined) return trackerError(problem);
|
|
102
|
+
const backlog = join(root, "work", "backlog.md");
|
|
103
|
+
for (const target of [itemsDir(root), fileOf(root, item.id), backlog]) {
|
|
104
|
+
const link = await refuseSymlink(root, target);
|
|
105
|
+
if (link !== undefined) return trackerError(link);
|
|
106
|
+
}
|
|
74
107
|
const others = await readAll(root);
|
|
75
108
|
if (!others.ok) return others;
|
|
76
109
|
await mkdir(itemsDir(root), { recursive: true });
|
|
@@ -78,7 +111,7 @@ const save = async (root: string, item: WorkItem): Promise<TrackerResult<WorkIte
|
|
|
78
111
|
const items = [...others.value.filter((i) => i.id !== item.id), item].toSorted((a, b) =>
|
|
79
112
|
byCodeUnit(a.id, b.id),
|
|
80
113
|
);
|
|
81
|
-
await writeFile(
|
|
114
|
+
await writeFile(backlog, renderBacklog(items));
|
|
82
115
|
return ok(item);
|
|
83
116
|
};
|
|
84
117
|
|