@g3nomaly/g3n-skills 0.1.0 → 0.1.2
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/.claude-plugin/marketplace.json +3 -2
- package/README.md +96 -0
- package/package.json +1 -1
- package/skills/coding/.claude-plugin/plugin.json +2 -2
- package/skills/coding/analyze/SKILL.md +32 -0
- package/skills/coding/analyze/agents/openai.yaml +5 -0
- package/skills/coding/analyze/references/api.md +27 -0
- package/skills/coding/analyze/references/commands.md +54 -0
- package/skills/coding/analyze/references/repository.md +13 -0
- package/skills/coding/document/SKILL.md +26 -9
- package/skills/coding/document/references/typescript.md +44 -3
- package/skills/task-manager/decisions/src/paths.ts +13 -1
- package/skills/task-manager/decisions/src/read.ts +20 -6
- package/skills/task-manager/decisions/src/write.ts +28 -5
- package/skills/task-manager/task-manager/hooks/cleanup.ts +6 -0
- package/skills/task-manager/task-manager/hooks/guard-tree.ts +31 -4
- package/skills/task-manager/task-manager/hooks/sync-tree.ts +1 -1
- package/skills/task-manager/task-manager/src/filetree/ensure.ts +15 -3
- package/skills/task-manager/task-manager/src/filetree/entries.ts +26 -5
- package/skills/task-manager/task-manager/src/filetree/frontmatter.ts +23 -7
- package/skills/task-manager/task-manager/src/filetree/generate.ts +31 -6
- package/skills/task-manager/task-manager/src/filetree/levels.ts +1 -0
- package/skills/task-manager/task-manager/src/filetree/locate.ts +45 -13
- package/skills/task-manager/task-manager/src/filetree/paths.ts +30 -6
- package/skills/task-manager/task-manager/src/filetree/read.ts +15 -7
- package/skills/task-manager/task-manager/src/filetree/remove.ts +7 -4
- package/skills/task-manager/task-manager/src/filetree/root.ts +4 -3
- package/skills/task-manager/task-manager/src/filetree/status.ts +29 -7
- package/skills/task-manager/task-manager/src/hooks/cli.ts +50 -12
- package/skills/task-manager/task-manager/src/schemas/config.ts +1 -0
- package/skills/task-manager/task-manager/src/schemas/decision.ts +8 -2
- package/skills/task-manager/task-manager/src/schemas/phase.ts +6 -0
- package/skills/task-manager/task-manager/src/schemas/plan.ts +6 -0
- package/skills/task-manager/task-manager/src/schemas/primitives.ts +3 -0
- package/skills/task-manager/task-manager/src/schemas/task.ts +7 -0
- package/skills/task-manager/task-manager/src/schemas/term.ts +8 -1
- package/skills/task-manager/task-manager/src/schemas/validation.ts +31 -7
- package/skills/task-manager/task-manager/src/vocabulary/file.ts +37 -4
- package/skills/task-manager/task-manager/src/vocabulary/match.ts +29 -7
- package/skills/workflow/run/hooks/advance-step.ts +14 -2
- package/skills/workflow/run/src/gates.ts +21 -8
- package/skills/workflow/run/src/phase.ts +17 -3
- package/skills/workflow/run/src/steps.ts +10 -4
|
@@ -7,15 +7,22 @@ import { listEntryNames } from "./entries";
|
|
|
7
7
|
import { generateIndex } from "./generate";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Creates every folder and index file the tree under `root` is missing, and
|
|
10
|
+
* @brief Creates every folder and index file the tree under `root` is missing, and
|
|
11
11
|
* leaves everything that already exists untouched. A hook can call this on
|
|
12
12
|
* every run.
|
|
13
|
+
*
|
|
14
|
+
* @param root - Folder of the tree, made absolute before use.
|
|
13
15
|
*/
|
|
14
16
|
export async function ensureTree(root: string): Promise<void> {
|
|
15
17
|
await ensureFrom(resolve(root), "root");
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* @brief Creates the folders and index files of one folder and of every folder below it.
|
|
22
|
+
*
|
|
23
|
+
* @param dir - Absolute folder; `childDirFor` keeps it absolute down the walk.
|
|
24
|
+
* @param level - Level of `dir` in the tree.
|
|
25
|
+
*/
|
|
19
26
|
async function ensureFrom(dir: string, level: TreeLevel): Promise<void> {
|
|
20
27
|
await mkdir(dir, { recursive: true });
|
|
21
28
|
if (level === "phase") await mkdir(tasksDirFor(dir), { recursive: true });
|
|
@@ -30,7 +37,12 @@ async function ensureFrom(dir: string, level: TreeLevel): Promise<void> {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* @brief Builds a missing index from the folder it belongs to; keeps an existing one.
|
|
42
|
+
*
|
|
43
|
+
* @param dir - Folder the index belongs to.
|
|
44
|
+
* @param level - Level of `dir` in the tree.
|
|
45
|
+
*/
|
|
34
46
|
async function ensureIndexFile(dir: string, level: TreeLevel): Promise<void> {
|
|
35
47
|
const exists = await Bun.file(indexPathFor(dir, level)).exists();
|
|
36
48
|
if (exists) return;
|
|
@@ -4,10 +4,14 @@ import { ENTRY_KIND, MARKDOWN, type TreeLevel } from "./levels";
|
|
|
4
4
|
import { entryPathFor, tasksDirFor } from "./paths";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* reported as a broken plan.
|
|
7
|
+
* @brief Lists the entries of a folder at `level`, alphabetical, which is the order the queue runs in.
|
|
8
|
+
*
|
|
9
|
+
* A folder only counts as an entry once it holds its own `<name>.md`, so a
|
|
10
|
+
* directory left behind by hand is skipped rather than reported as a broken plan.
|
|
11
|
+
*
|
|
12
|
+
* @param dir - Folder whose entries are listed.
|
|
13
|
+
* @param level - Level of `dir` in the tree.
|
|
14
|
+
* @returns Entry names, sorted.
|
|
11
15
|
*/
|
|
12
16
|
export async function listEntryNames(dir: string, level: TreeLevel): Promise<string[]> {
|
|
13
17
|
const names = ENTRY_KIND[level] === "folder"
|
|
@@ -17,6 +21,12 @@ export async function listEntryNames(dir: string, level: TreeLevel): Promise<str
|
|
|
17
21
|
return names.sort((a, b) => a.localeCompare(b));
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @brief Lists the subfolders of `dir` that hold their own `<name>.md`.
|
|
26
|
+
*
|
|
27
|
+
* @param dir - Folder whose subfolders are checked.
|
|
28
|
+
* @param level - Level of `dir`, which picks the file each subfolder must hold.
|
|
29
|
+
*/
|
|
20
30
|
async function listFolderEntries(dir: string, level: TreeLevel): Promise<string[]> {
|
|
21
31
|
const names: string[] = [];
|
|
22
32
|
|
|
@@ -30,7 +40,13 @@ async function listFolderEntries(dir: string, level: TreeLevel): Promise<string[
|
|
|
30
40
|
return names;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* @brief Lists the task ids of a phase, from the `.md` files in its `tasks/` folder.
|
|
45
|
+
*
|
|
46
|
+
* A phase with no `tasks/` folder yet has no tasks, which is not an error.
|
|
47
|
+
*
|
|
48
|
+
* @param phaseDir - Phase folder.
|
|
49
|
+
*/
|
|
34
50
|
async function listTaskEntries(phaseDir: string): Promise<string[]> {
|
|
35
51
|
let files;
|
|
36
52
|
|
|
@@ -46,6 +62,11 @@ async function listTaskEntries(phaseDir: string): Promise<string[]> {
|
|
|
46
62
|
.map((entry) => entry.name.slice(0, -MARKDOWN.length));
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @brief Checks whether an error is a missing file or folder (`ENOENT`).
|
|
67
|
+
*
|
|
68
|
+
* @param error - Caught value.
|
|
69
|
+
*/
|
|
49
70
|
function isMissing(error: unknown): boolean {
|
|
50
71
|
return (
|
|
51
72
|
typeof error === "object" &&
|
|
@@ -3,18 +3,24 @@ import { FileValidationError } from "../schemas/validation";
|
|
|
3
3
|
|
|
4
4
|
const FENCE = "---";
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* @brief Holds a `.md` file split at its closing fence.
|
|
8
|
+
*
|
|
9
|
+
* @property frontmatter - Mapping read from the block between the fences, not yet validated.
|
|
10
|
+
* @property body - Everything after the closing fence, leading blank lines trimmed.
|
|
11
|
+
*/
|
|
7
12
|
export interface Document {
|
|
8
13
|
frontmatter: unknown;
|
|
9
|
-
/** Everything after the closing fence, leading blank lines trimmed. */
|
|
10
14
|
body: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
|
-
* Reads a `.md` file as its frontmatter mapping plus the prose under it.
|
|
18
|
+
* @brief Reads a `.md` file as its frontmatter mapping plus the prose under it.
|
|
15
19
|
*
|
|
16
|
-
* @
|
|
17
|
-
*
|
|
20
|
+
* @param file - Path of the `.md` file.
|
|
21
|
+
* @returns The frontmatter mapping and the body.
|
|
22
|
+
* @throws {FileValidationError} When the file has no frontmatter block or the
|
|
23
|
+
* block is not a YAML mapping.
|
|
18
24
|
*/
|
|
19
25
|
export async function readDocument(file: string): Promise<Document> {
|
|
20
26
|
const text = await Bun.file(file).text();
|
|
@@ -37,12 +43,22 @@ export async function readDocument(file: string): Promise<Document> {
|
|
|
37
43
|
return { frontmatter, body: split.body };
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* @brief Reads only the frontmatter mapping of a `.md` file.
|
|
48
|
+
*
|
|
49
|
+
* @param file - Path of the `.md` file.
|
|
50
|
+
* @returns The frontmatter mapping, not yet validated.
|
|
51
|
+
*/
|
|
41
52
|
export async function readFrontmatter(file: string): Promise<unknown> {
|
|
42
53
|
return (await readDocument(file)).frontmatter;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* @brief Splits a file's text at its closing fence.
|
|
58
|
+
*
|
|
59
|
+
* @param text - Whole file text.
|
|
60
|
+
* @returns The text between the fences and the text after them, or `null` when the fences are missing.
|
|
61
|
+
*/
|
|
46
62
|
function splitAtFences(text: string): { block: string; body: string } | null {
|
|
47
63
|
const lines = text.split("\n");
|
|
48
64
|
|
|
@@ -11,12 +11,14 @@ import { listEntryNames } from "./entries";
|
|
|
11
11
|
import { readEntry, type EntryOf } from "./read";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Rebuilds one folder's index from the entries directly under it, replacing
|
|
14
|
+
* @brief Rebuilds one folder's index from the entries directly under it, replacing
|
|
15
15
|
* whatever was there. Plans and phases are read from `<name>/<name>.md`, tasks
|
|
16
16
|
* from `tasks/<id>.md`, so the result only depends on that one folder.
|
|
17
17
|
*
|
|
18
|
-
* @
|
|
19
|
-
*
|
|
18
|
+
* @param dir - Folder whose index is rebuilt.
|
|
19
|
+
* @param level - Level of `dir` in the tree.
|
|
20
|
+
* @throws {FileValidationError} When an entry fails its schema or its identifier
|
|
21
|
+
* does not match the folder or file it was read from.
|
|
20
22
|
*/
|
|
21
23
|
export async function generateIndex(dir: string, level: TreeLevel): Promise<void> {
|
|
22
24
|
switch (level) {
|
|
@@ -33,14 +35,22 @@ export async function generateIndex(dir: string, level: TreeLevel): Promise<void
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
* Rebuilds every index in the tree, from the root folder down.
|
|
38
|
+
* @brief Rebuilds every index in the tree, from the root folder down.
|
|
39
|
+
*
|
|
37
40
|
* Run `ensureTree` first — a plan or phase without its folder throws here.
|
|
41
|
+
*
|
|
42
|
+
* @param root - Tree root folder.
|
|
38
43
|
*/
|
|
39
44
|
export async function generateTree(root: string): Promise<void> {
|
|
40
45
|
await generateFrom(resolve(root), "root");
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* @brief Rebuilds the index of one folder and of every folder below it.
|
|
50
|
+
*
|
|
51
|
+
* @param dir - Absolute folder; `childDirFor` keeps it absolute down the walk.
|
|
52
|
+
* @param level - Level of `dir` in the tree.
|
|
53
|
+
*/
|
|
44
54
|
async function generateFrom(dir: string, level: TreeLevel): Promise<void> {
|
|
45
55
|
await generateIndex(dir, level);
|
|
46
56
|
|
|
@@ -52,7 +62,13 @@ async function generateFrom(dir: string, level: TreeLevel): Promise<void> {
|
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* @brief Reads and validates every entry of `dir`, in queue order.
|
|
67
|
+
*
|
|
68
|
+
* @param dir - Folder whose entries are read.
|
|
69
|
+
* @param level - Level of `dir` in the tree.
|
|
70
|
+
* @returns The validated fields, one per entry.
|
|
71
|
+
*/
|
|
56
72
|
async function collect<L extends TreeLevel>(dir: string, level: L): Promise<EntryOf<L>[]> {
|
|
57
73
|
const entries: EntryOf<L>[] = [];
|
|
58
74
|
|
|
@@ -63,6 +79,15 @@ async function collect<L extends TreeLevel>(dir: string, level: L): Promise<Entr
|
|
|
63
79
|
return entries;
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @brief Validates an index against its schema and writes it to the folder's index file.
|
|
84
|
+
*
|
|
85
|
+
* @param dir - Folder the index belongs to.
|
|
86
|
+
* @param level - Level of `dir` in the tree.
|
|
87
|
+
* @param schema - Zod schema the index must match.
|
|
88
|
+
* @param index - Index contents, before validation.
|
|
89
|
+
* @throws {FileValidationError} When the index fails `schema`.
|
|
90
|
+
*/
|
|
66
91
|
async function writeIndex<S extends z.ZodType>(
|
|
67
92
|
dir: string,
|
|
68
93
|
level: TreeLevel,
|
|
@@ -4,28 +4,42 @@ import { CHILD_LEVEL, ENTRY_TYPE, type TreeLevel } from "./levels";
|
|
|
4
4
|
import { childDirFor, entryPathFor } from "./paths";
|
|
5
5
|
import { listEntryNames } from "./entries";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* @brief Holds where one entry sits, from the folder that lists it.
|
|
9
|
+
*
|
|
10
|
+
* @property level - Level of `dir`, the folder whose index lists this entry.
|
|
11
|
+
* @property type - What this entry is: a `root` folder lists plans, a `phase` folder tasks.
|
|
12
|
+
* @property name - Folder name for a plan or phase, task id for a task.
|
|
13
|
+
* @property dir - Folder whose index lists this entry.
|
|
14
|
+
* @property file - Path of the entry's `.md` file.
|
|
15
|
+
* @property trail - Names from the tree root down to and including this entry.
|
|
16
|
+
*/
|
|
8
17
|
export interface EntryLocation {
|
|
9
|
-
/** Level of `dir` — the folder whose index lists this entry. */
|
|
10
18
|
level: TreeLevel;
|
|
11
|
-
/** What this entry is: a `root` folder lists plans, a `phase` folder tasks. */
|
|
12
19
|
type: "plan" | "phase" | "task";
|
|
13
20
|
name: string;
|
|
14
21
|
dir: string;
|
|
15
22
|
file: string;
|
|
16
|
-
/** Names from the tree root down to and including this entry. */
|
|
17
23
|
trail: string[];
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* @brief Holds where a folder's entries are listed, and at which level.
|
|
28
|
+
*
|
|
29
|
+
* @property dir - Folder holding the entries.
|
|
30
|
+
* @property level - Level of `dir` in the tree.
|
|
31
|
+
*/
|
|
21
32
|
export interface ParentFolder {
|
|
22
33
|
dir: string;
|
|
23
34
|
level: TreeLevel;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
|
-
*
|
|
38
|
+
* @brief Lists every entry in the tree, in the order the queue runs: each folder's entries
|
|
28
39
|
* alphabetically, and each entry's own entries directly after it.
|
|
40
|
+
*
|
|
41
|
+
* @param root - Tree root folder.
|
|
42
|
+
* @returns Every entry's location.
|
|
29
43
|
*/
|
|
30
44
|
export async function listAllEntries(root: string): Promise<EntryLocation[]> {
|
|
31
45
|
const found: EntryLocation[] = [];
|
|
@@ -33,6 +47,14 @@ export async function listAllEntries(root: string): Promise<EntryLocation[]> {
|
|
|
33
47
|
return found;
|
|
34
48
|
}
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @brief Adds the entries of one folder to `found`, each followed by its own entries.
|
|
52
|
+
*
|
|
53
|
+
* @param dir - Folder whose entries are added.
|
|
54
|
+
* @param level - Level of `dir` in the tree.
|
|
55
|
+
* @param trail - Names from the tree root down to `dir`.
|
|
56
|
+
* @param found - List the entries are appended to.
|
|
57
|
+
*/
|
|
36
58
|
async function collectFrom(
|
|
37
59
|
dir: string,
|
|
38
60
|
level: TreeLevel,
|
|
@@ -58,10 +80,13 @@ async function collectFrom(
|
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
/**
|
|
61
|
-
* Finds the one entry named `name` anywhere in the tree.
|
|
83
|
+
* @brief Finds the one entry named `name` anywhere in the tree.
|
|
62
84
|
*
|
|
63
|
-
* @
|
|
64
|
-
*
|
|
85
|
+
* @param root - Tree root folder.
|
|
86
|
+
* @param name - Name to look for.
|
|
87
|
+
* @returns The entry's location.
|
|
88
|
+
* @throws {Error} When nothing is named `name`, or when two entries are — a name
|
|
89
|
+
* the caller cannot act on is reported rather than resolved by guessing.
|
|
65
90
|
*/
|
|
66
91
|
export async function locateEntry(root: string, name: string): Promise<EntryLocation> {
|
|
67
92
|
const matches = (await listAllEntries(root)).filter((entry) => entry.name === name);
|
|
@@ -80,10 +105,14 @@ export async function locateEntry(root: string, name: string): Promise<EntryLoca
|
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
/**
|
|
83
|
-
* `locateEntry` for a caller that only accepts one kind of entry, such as a
|
|
108
|
+
* @brief `locateEntry` for a caller that only accepts one kind of entry, such as a
|
|
84
109
|
* hook whose `--in` must name a plan.
|
|
85
110
|
*
|
|
86
|
-
* @
|
|
111
|
+
* @param root - Tree root folder.
|
|
112
|
+
* @param name - Name of the entry.
|
|
113
|
+
* @param type - Kind the entry must be: `plan`, `phase` or `task`.
|
|
114
|
+
* @returns The entry's location.
|
|
115
|
+
* @throws {Error} When the entry named `name` is of another type.
|
|
87
116
|
*/
|
|
88
117
|
export async function locateEntryOfType(
|
|
89
118
|
root: string,
|
|
@@ -100,9 +129,12 @@ export async function locateEntryOfType(
|
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
/**
|
|
103
|
-
*
|
|
132
|
+
* @brief Finds the folder whose entries `parent` holds, or the tree root when no parent is named.
|
|
104
133
|
*
|
|
105
|
-
* @param
|
|
134
|
+
* @param root - Tree root folder.
|
|
135
|
+
* @param parent - Name of a plan or phase; a task holds nothing.
|
|
136
|
+
* @returns The folder and the level of its entries.
|
|
137
|
+
* @throws {Error} When `parent` names no entry, or names a task.
|
|
106
138
|
*/
|
|
107
139
|
export async function resolveParent(root: string, parent?: string): Promise<ParentFolder> {
|
|
108
140
|
if (parent === undefined) return { dir: resolve(root), level: "root" };
|
|
@@ -2,22 +2,42 @@
|
|
|
2
2
|
import { join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { ENTRY_KIND, INDEX_FILES, LEVELS, MARKDOWN, TASKS_DIR, type TreeLevel } from "./levels";
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* @brief Builds the path of the index file a folder at `level` owns.
|
|
7
|
+
*
|
|
8
|
+
* @param dir - Folder the index belongs to.
|
|
9
|
+
* @param level - Level of `dir` in the tree.
|
|
10
|
+
*/
|
|
6
11
|
export function indexPathFor(dir: string, level: TreeLevel): string {
|
|
7
12
|
return join(dir, INDEX_FILES[level]);
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* @brief Finds the folder a `folder` entry owns, which holds its own file and the level below it.
|
|
17
|
+
*
|
|
18
|
+
* @param dir - Folder that lists the entry.
|
|
19
|
+
* @param entryName - Name of the entry.
|
|
20
|
+
*/
|
|
11
21
|
export function childDirFor(dir: string, entryName: string): string {
|
|
12
22
|
return join(dir, entryName);
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* @brief Finds the `tasks/` folder that holds a phase's task files.
|
|
27
|
+
*
|
|
28
|
+
* @param phaseDir - Phase folder.
|
|
29
|
+
*/
|
|
16
30
|
export function tasksDirFor(phaseDir: string): string {
|
|
17
31
|
return join(phaseDir, TASKS_DIR);
|
|
18
32
|
}
|
|
19
33
|
|
|
20
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* @brief Builds the path of the `.md` file that describes one entry of a folder at `level`.
|
|
36
|
+
*
|
|
37
|
+
* @param dir - Folder that lists the entry.
|
|
38
|
+
* @param level - Level of `dir` in the tree.
|
|
39
|
+
* @param entryName - Name of the entry.
|
|
40
|
+
*/
|
|
21
41
|
export function entryPathFor(dir: string, level: TreeLevel, entryName: string): string {
|
|
22
42
|
return ENTRY_KIND[level] === "folder"
|
|
23
43
|
? join(dir, entryName, `${entryName}${MARKDOWN}`)
|
|
@@ -25,8 +45,12 @@ export function entryPathFor(dir: string, level: TreeLevel, entryName: string):
|
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
48
|
+
* @brief Finds the level of `dir` from how deep it sits under `root`.
|
|
49
|
+
*
|
|
50
|
+
* @param root - Tree root folder.
|
|
51
|
+
* @param dir - Folder whose level is found.
|
|
52
|
+
* @returns The level of `dir`.
|
|
53
|
+
* @throws {Error} When `dir` sits outside `root` or below a phase folder.
|
|
30
54
|
*/
|
|
31
55
|
export function levelFor(root: string, dir: string): TreeLevel {
|
|
32
56
|
const rel = relative(resolve(root), resolve(dir));
|
|
@@ -7,10 +7,16 @@ import { ID_FIELD, type TreeLevel } from "./levels";
|
|
|
7
7
|
import { entryPathFor } from "./paths";
|
|
8
8
|
import { readDocument } from "./frontmatter";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/** Names the entry type a folder at level `L` holds: `Plan` under the root, `Phase` under a plan, `Task` otherwise. */
|
|
11
11
|
export type EntryOf<L extends TreeLevel> = L extends "root" ? Plan : L extends "plan" ? Phase : Task;
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* @brief Holds one entry as it sits on disk: where it was read from, its fields, its prose.
|
|
15
|
+
*
|
|
16
|
+
* @property file - Path of the entry's `.md` file.
|
|
17
|
+
* @property meta - Fields from the frontmatter, checked against the schema of its level.
|
|
18
|
+
* @property body - Prose after the closing fence of the frontmatter.
|
|
19
|
+
*/
|
|
14
20
|
export interface EntryDocument<L extends TreeLevel> {
|
|
15
21
|
file: string;
|
|
16
22
|
meta: EntryOf<L>;
|
|
@@ -24,13 +30,15 @@ const SCHEMAS = {
|
|
|
24
30
|
} as const;
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
|
-
* Reads one entry of a folder at `level` and checks it against its schema.
|
|
33
|
+
* @brief Reads one entry of a folder at `level` and checks it against its schema.
|
|
28
34
|
* Every read of a plan, phase or task file goes through here.
|
|
29
35
|
*
|
|
30
|
-
* @param dir
|
|
31
|
-
* @param
|
|
32
|
-
* @
|
|
33
|
-
*
|
|
36
|
+
* @param dir - Folder whose index lists the entry.
|
|
37
|
+
* @param level - Level of `dir` in the tree, which picks the schema.
|
|
38
|
+
* @param name - Folder name for a plan or phase, task id for a task.
|
|
39
|
+
* @returns The entry's path, its validated fields and its prose.
|
|
40
|
+
* @throws {FileValidationError} When a field fails its schema, or when the
|
|
41
|
+
* identifier inside the file disagrees with the folder or file it was read from.
|
|
34
42
|
*/
|
|
35
43
|
export async function readEntry<L extends TreeLevel>(
|
|
36
44
|
dir: string,
|
|
@@ -5,10 +5,13 @@ import { childDirFor } from "./paths";
|
|
|
5
5
|
import type { EntryLocation } from "./locate";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Deletes an entry — a task's `.md` file, or a plan or phase folder with
|
|
9
|
-
* everything under it.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* @brief Deletes an entry — a task's `.md` file, or a plan or phase folder with
|
|
9
|
+
* everything under it.
|
|
10
|
+
*
|
|
11
|
+
* Removing something already gone is not an error, so a plan and a phase inside
|
|
12
|
+
* it can both be handed in. Regenerate the tree afterwards.
|
|
13
|
+
*
|
|
14
|
+
* @param entry - Location of the entry to delete.
|
|
12
15
|
*/
|
|
13
16
|
export async function removeEntry(entry: EntryLocation): Promise<void> {
|
|
14
17
|
const target = ENTRY_KIND[entry.level] === "folder"
|
|
@@ -4,12 +4,13 @@ import { CONFIG_FILE, FiletreeConfigSchema } from "../schemas/config";
|
|
|
4
4
|
import { FileValidationError, parseFile } from "../schemas/validation";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* @brief Finds the absolute path of the tree, from `skills.settings.filetreeRoot` in
|
|
8
8
|
* `.g3n.config.json`. Falls back to `docs/task-manager` when the project has
|
|
9
9
|
* no config file or the file does not set it.
|
|
10
10
|
*
|
|
11
|
-
* @param projectRoot - Folder holding `.g3n.config.json
|
|
12
|
-
* @
|
|
11
|
+
* @param projectRoot - Folder holding `.g3n.config.json`.
|
|
12
|
+
* @returns Absolute path of the tree.
|
|
13
|
+
* @throws {FileValidationError} When the config file is not JSON or sets a root that is not a string.
|
|
13
14
|
*/
|
|
14
15
|
export async function resolveFiletreeRoot(projectRoot: string): Promise<string> {
|
|
15
16
|
const path = join(projectRoot, CONFIG_FILE);
|
|
@@ -6,26 +6,40 @@ import { FileValidationError } from "../schemas/validation";
|
|
|
6
6
|
const FENCE = "---";
|
|
7
7
|
const STATUS_LINE = /^(status:\s*)(\S+)(\s*)$/;
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* @brief Holds the status an entry moved from and to, for a caller that reports the change.
|
|
11
|
+
*
|
|
12
|
+
* @property from - Status before the change.
|
|
13
|
+
* @property to - Status after the change.
|
|
14
|
+
*/
|
|
10
15
|
export interface StatusChange<T extends string = TaskStatus> {
|
|
11
16
|
from: T;
|
|
12
17
|
to: T;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* @brief Sets the status of a plan, phase or task file.
|
|
22
|
+
*
|
|
23
|
+
* @param file - Path of the entry's `.md` file.
|
|
24
|
+
* @param to - Status to write.
|
|
25
|
+
* @returns The status the file had and the one it has now.
|
|
26
|
+
*/
|
|
16
27
|
export async function setEntryStatus(file: string, to: TaskStatus): Promise<StatusChange> {
|
|
17
28
|
return setStatusLine(file, to, StatusSchema);
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
|
-
* Rewrites the `status:` line in a file's frontmatter and leaves every other
|
|
32
|
+
* @brief Rewrites the `status:` line in a file's frontmatter and leaves every other
|
|
22
33
|
* byte alone, so comments, key order and quoting survive the change. Regenerate
|
|
23
34
|
* the owning index afterwards — this touches the `.md` file only.
|
|
24
35
|
*
|
|
25
|
-
* @param
|
|
26
|
-
* @
|
|
27
|
-
*
|
|
28
|
-
* @
|
|
36
|
+
* @param file - Path of the `.md` file.
|
|
37
|
+
* @param to - Status to write.
|
|
38
|
+
* @param schema - Zod schema of the statuses the file may hold.
|
|
39
|
+
* @returns The status the file had and the one it has now.
|
|
40
|
+
* @throws {FileValidationError} When the frontmatter has no `status:` line, or
|
|
41
|
+
* holds a status outside `schema`.
|
|
42
|
+
* @throws {Error} When the file already has status `to`.
|
|
29
43
|
*/
|
|
30
44
|
export async function setStatusLine<T extends string>(
|
|
31
45
|
file: string,
|
|
@@ -58,6 +72,14 @@ export async function setStatusLine<T extends string>(
|
|
|
58
72
|
]);
|
|
59
73
|
}
|
|
60
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @brief Finds the line of the closing fence of a file's frontmatter.
|
|
77
|
+
*
|
|
78
|
+
* @param lines - Lines of the file.
|
|
79
|
+
* @param file - Path of the file, reported in the error.
|
|
80
|
+
* @returns Index of the closing fence line.
|
|
81
|
+
* @throws {FileValidationError} When the file has no opening fence or no closing one.
|
|
82
|
+
*/
|
|
61
83
|
function closingFenceAt(lines: readonly string[], file: string): number {
|
|
62
84
|
if (lines[0]?.trimEnd() !== FENCE) {
|
|
63
85
|
throw new FileValidationError(file, [
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { resolveFiletreeRoot } from "../filetree/root";
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/** Entry point of one hook, as `g3n-skills hook <name>` calls it; receives the flags after the name, untouched. */
|
|
6
6
|
export type Hook = (argv: readonly string[]) => Promise<void>;
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @brief Reads the value that follows a flag in the arguments.
|
|
10
|
+
*
|
|
11
|
+
* @param argv - Arguments of the hook, e.g. `["--term", "phase"]`.
|
|
12
|
+
* @param flag - Flag to look up, with its dashes, e.g. `--term`.
|
|
13
|
+
* @returns Value after the flag, or `undefined` when the flag is absent.
|
|
14
|
+
* @throws Error when the flag is last or followed by another flag.
|
|
15
|
+
*/
|
|
9
16
|
export function readFlag(argv: readonly string[], flag: string): string | undefined {
|
|
10
17
|
const at = argv.indexOf(flag);
|
|
11
18
|
if (at === -1) return undefined;
|
|
@@ -18,7 +25,15 @@ export function readFlag(argv: readonly string[], flag: string): string | undefi
|
|
|
18
25
|
return value;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* @brief Reads the value of a required flag.
|
|
30
|
+
*
|
|
31
|
+
* @param argv - Arguments of the hook.
|
|
32
|
+
* @param flag - Flag to look up, with its dashes, e.g. `--term`.
|
|
33
|
+
* @param example - Value shown in the error when the flag is missing, e.g. `phase`.
|
|
34
|
+
* @returns Value after the flag.
|
|
35
|
+
* @throws Error naming the flag and the example when it is missing.
|
|
36
|
+
*/
|
|
22
37
|
export function requireFlag(argv: readonly string[], flag: string, example: string): string {
|
|
23
38
|
const value = readFlag(argv, flag);
|
|
24
39
|
if (value === undefined) {
|
|
@@ -28,14 +43,24 @@ export function requireFlag(argv: readonly string[], flag: string, example: stri
|
|
|
28
43
|
return value;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @brief Tells whether a flag appears in the arguments.
|
|
48
|
+
*
|
|
49
|
+
* @param argv - Arguments of the hook.
|
|
50
|
+
* @param flag - Flag to look for, with its dashes.
|
|
51
|
+
*/
|
|
31
52
|
export function hasFlag(argv: readonly string[], flag: string): boolean {
|
|
32
53
|
return argv.includes(flag);
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
|
-
* Narrows a flag value to one of a fixed set
|
|
37
|
-
*
|
|
38
|
-
*
|
|
57
|
+
* @brief Narrows a flag value to one of a fixed set.
|
|
58
|
+
*
|
|
59
|
+
* @param value - Raw flag value.
|
|
60
|
+
* @param allowed - Accepted values.
|
|
61
|
+
* @param flag - Flag name used in the error message.
|
|
62
|
+
* @returns The value, typed as one of `allowed`.
|
|
63
|
+
* @throws Error naming every allowed value when `value` is not one of them.
|
|
39
64
|
*/
|
|
40
65
|
export function oneOf<T extends string>(value: string, allowed: readonly T[], flag: string): T {
|
|
41
66
|
if ((allowed as readonly string[]).includes(value)) return value as T;
|
|
@@ -44,9 +69,14 @@ export function oneOf<T extends string>(value: string, allowed: readonly T[], fl
|
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
72
|
+
* @brief Resolves the tree root the hook works on.
|
|
73
|
+
*
|
|
74
|
+
* Uses `--root` verbatim, otherwise the folder named by
|
|
75
|
+
* `skills.settings.filetreeRoot` under `--project` (default: the working
|
|
49
76
|
* directory).
|
|
77
|
+
*
|
|
78
|
+
* @param argv - Arguments of the hook.
|
|
79
|
+
* @returns Absolute tree root.
|
|
50
80
|
*/
|
|
51
81
|
export async function resolveRoot(argv: readonly string[]): Promise<string> {
|
|
52
82
|
const override = readFlag(argv, "--root");
|
|
@@ -55,19 +85,27 @@ export async function resolveRoot(argv: readonly string[]): Promise<string> {
|
|
|
55
85
|
return resolveFiletreeRoot(resolve(readFlag(argv, "--project") ?? process.cwd()));
|
|
56
86
|
}
|
|
57
87
|
|
|
58
|
-
/**
|
|
88
|
+
/** Reads everything piped to the hook on stdin, or returns `""` when run from a terminal. */
|
|
59
89
|
export async function readStdin(): Promise<string> {
|
|
60
90
|
return process.stdin.isTTY ? "" : Bun.stdin.text();
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* @brief Prints a value as JSON on stdout, indented when `--pretty` is given.
|
|
95
|
+
*
|
|
96
|
+
* @param value - Value to print; must be JSON-serializable.
|
|
97
|
+
* @param argv - Arguments of the hook, checked for `--pretty`.
|
|
98
|
+
*/
|
|
64
99
|
export function printJson(value: unknown, argv: readonly string[]): void {
|
|
65
100
|
console.log(JSON.stringify(value, null, hasFlag(argv, "--pretty") ? 2 : 0));
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
/**
|
|
69
|
-
* Runs a
|
|
70
|
-
*
|
|
104
|
+
* @brief Runs a hook's `main` and reports anything it throws.
|
|
105
|
+
*
|
|
106
|
+
* Writes the error message to stderr and exits with code 1.
|
|
107
|
+
*
|
|
108
|
+
* @param main - Async entry point of the hook.
|
|
71
109
|
*/
|
|
72
110
|
export function runHook(main: () => Promise<void>): void {
|
|
73
111
|
main().catch((error: unknown) => {
|