rtfct 0.1.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/.project/adrs/001-use-bun-typescript.md +52 -0
- package/.project/guardrails.md +65 -0
- package/.project/kanban/backlog.md +7 -0
- package/.project/kanban/done.md +240 -0
- package/.project/kanban/in-progress.md +11 -0
- package/.project/kickstart.md +63 -0
- package/.project/protocol.md +134 -0
- package/.project/specs/requirements.md +152 -0
- package/.project/testing/strategy.md +123 -0
- package/.project/theology.md +125 -0
- package/CLAUDE.md +119 -0
- package/README.md +143 -0
- package/package.json +31 -0
- package/src/args.ts +104 -0
- package/src/commands/add.ts +78 -0
- package/src/commands/init.ts +128 -0
- package/src/commands/praise.ts +19 -0
- package/src/commands/regenerate.ts +122 -0
- package/src/commands/status.ts +163 -0
- package/src/help.ts +52 -0
- package/src/index.ts +102 -0
- package/src/kanban.ts +83 -0
- package/src/manifest.ts +67 -0
- package/src/presets/base.ts +195 -0
- package/src/presets/elixir.ts +118 -0
- package/src/presets/github.ts +194 -0
- package/src/presets/index.ts +154 -0
- package/src/presets/typescript.ts +589 -0
- package/src/presets/zig.ts +494 -0
- package/tests/integration/add.test.ts +104 -0
- package/tests/integration/init.test.ts +197 -0
- package/tests/integration/praise.test.ts +36 -0
- package/tests/integration/regenerate.test.ts +154 -0
- package/tests/integration/status.test.ts +165 -0
- package/tests/unit/args.test.ts +144 -0
- package/tests/unit/kanban.test.ts +162 -0
- package/tests/unit/manifest.test.ts +155 -0
- package/tests/unit/presets.test.ts +295 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for the Kanban Parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
countTasks,
|
|
8
|
+
extractCurrentTask,
|
|
9
|
+
parseKanbanFile,
|
|
10
|
+
formatRelativeTime,
|
|
11
|
+
} from "../../src/kanban";
|
|
12
|
+
|
|
13
|
+
describe("countTasks", () => {
|
|
14
|
+
test("counts zero tasks in empty content", () => {
|
|
15
|
+
expect(countTasks("# Backlog\n\nNo tasks here.")).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("counts single task", () => {
|
|
19
|
+
const content = `# Backlog
|
|
20
|
+
|
|
21
|
+
## [TASK-001] First Task
|
|
22
|
+
|
|
23
|
+
Description here.
|
|
24
|
+
`;
|
|
25
|
+
expect(countTasks(content)).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("counts multiple tasks", () => {
|
|
29
|
+
const content = `# Backlog
|
|
30
|
+
|
|
31
|
+
## [TASK-001] First Task
|
|
32
|
+
|
|
33
|
+
Description.
|
|
34
|
+
|
|
35
|
+
## [TASK-002] Second Task
|
|
36
|
+
|
|
37
|
+
Another description.
|
|
38
|
+
|
|
39
|
+
## [TASK-003] Third Task
|
|
40
|
+
|
|
41
|
+
Yet another.
|
|
42
|
+
`;
|
|
43
|
+
expect(countTasks(content)).toBe(3);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("ignores non-task headers", () => {
|
|
47
|
+
const content = `# Backlog
|
|
48
|
+
|
|
49
|
+
## [TASK-001] Real Task
|
|
50
|
+
|
|
51
|
+
## Not a Task
|
|
52
|
+
|
|
53
|
+
## [OTHER-002] Also Not a Task
|
|
54
|
+
`;
|
|
55
|
+
expect(countTasks(content)).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("extractCurrentTask", () => {
|
|
60
|
+
test("returns null for empty content", () => {
|
|
61
|
+
expect(extractCurrentTask("# In Progress\n\nNo tasks.")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("extracts task from content", () => {
|
|
65
|
+
const content = `# In Progress
|
|
66
|
+
|
|
67
|
+
## [TASK-004] Implement user auth
|
|
68
|
+
|
|
69
|
+
Description here.
|
|
70
|
+
`;
|
|
71
|
+
const task = extractCurrentTask(content);
|
|
72
|
+
expect(task).not.toBeNull();
|
|
73
|
+
expect(task!.id).toBe("TASK-004");
|
|
74
|
+
expect(task!.title).toBe("Implement user auth");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("extracts first task when multiple exist", () => {
|
|
78
|
+
const content = `# In Progress
|
|
79
|
+
|
|
80
|
+
## [TASK-001] First
|
|
81
|
+
|
|
82
|
+
## [TASK-002] Second
|
|
83
|
+
`;
|
|
84
|
+
const task = extractCurrentTask(content);
|
|
85
|
+
expect(task!.id).toBe("TASK-001");
|
|
86
|
+
expect(task!.title).toBe("First");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("parseKanbanFile", () => {
|
|
91
|
+
test("parses backlog file", () => {
|
|
92
|
+
const content = `# Backlog
|
|
93
|
+
|
|
94
|
+
## [TASK-001] First
|
|
95
|
+
|
|
96
|
+
## [TASK-002] Second
|
|
97
|
+
`;
|
|
98
|
+
const result = parseKanbanFile(content, "backlog");
|
|
99
|
+
expect(result.count).toBe(2);
|
|
100
|
+
expect(result.currentTask).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("parses in-progress file with current task", () => {
|
|
104
|
+
const content = `# In Progress
|
|
105
|
+
|
|
106
|
+
## [TASK-003] Current Work
|
|
107
|
+
`;
|
|
108
|
+
const result = parseKanbanFile(content, "in-progress");
|
|
109
|
+
expect(result.count).toBe(1);
|
|
110
|
+
expect(result.currentTask).not.toBeNull();
|
|
111
|
+
expect(result.currentTask!.id).toBe("TASK-003");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("parses done file", () => {
|
|
115
|
+
const content = `# Done
|
|
116
|
+
|
|
117
|
+
## [TASK-001] First Completed
|
|
118
|
+
|
|
119
|
+
## [TASK-002] Second Completed
|
|
120
|
+
`;
|
|
121
|
+
const result = parseKanbanFile(content, "done");
|
|
122
|
+
expect(result.count).toBe(2);
|
|
123
|
+
expect(result.currentTask).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("formatRelativeTime", () => {
|
|
128
|
+
test("formats just now", () => {
|
|
129
|
+
const now = new Date();
|
|
130
|
+
expect(formatRelativeTime(now)).toBe("just now");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("formats minutes ago", () => {
|
|
134
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
135
|
+
expect(formatRelativeTime(fiveMinutesAgo)).toBe("5 minutes ago");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("formats single minute", () => {
|
|
139
|
+
const oneMinuteAgo = new Date(Date.now() - 1 * 60 * 1000);
|
|
140
|
+
expect(formatRelativeTime(oneMinuteAgo)).toBe("1 minute ago");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("formats hours ago", () => {
|
|
144
|
+
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
|
145
|
+
expect(formatRelativeTime(threeHoursAgo)).toBe("3 hours ago");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("formats single hour", () => {
|
|
149
|
+
const oneHourAgo = new Date(Date.now() - 1 * 60 * 60 * 1000);
|
|
150
|
+
expect(formatRelativeTime(oneHourAgo)).toBe("1 hour ago");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("formats days ago", () => {
|
|
154
|
+
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
155
|
+
expect(formatRelativeTime(twoDaysAgo)).toBe("2 days ago");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("formats single day", () => {
|
|
159
|
+
const oneDayAgo = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);
|
|
160
|
+
expect(formatRelativeTime(oneDayAgo)).toBe("1 day ago");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for the Manifest Reader
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { mkdir, writeFile, rm } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { collectGeneratedPaths, readManifest } from "../../src/manifest";
|
|
9
|
+
|
|
10
|
+
describe("readManifest", () => {
|
|
11
|
+
const testDir = "/tmp/rtfct-manifest-test";
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await mkdir(testDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await rm(testDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("reads valid manifest", async () => {
|
|
22
|
+
const manifest = {
|
|
23
|
+
name: "test",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
description: "Test preset",
|
|
26
|
+
generated_paths: ["src/", "lib/"],
|
|
27
|
+
};
|
|
28
|
+
await writeFile(join(testDir, "manifest.json"), JSON.stringify(manifest));
|
|
29
|
+
|
|
30
|
+
const result = await readManifest(testDir);
|
|
31
|
+
expect(result).not.toBeNull();
|
|
32
|
+
expect(result!.name).toBe("test");
|
|
33
|
+
expect(result!.generated_paths).toEqual(["src/", "lib/"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns null for missing manifest", async () => {
|
|
37
|
+
const result = await readManifest(testDir);
|
|
38
|
+
expect(result).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns null for invalid JSON", async () => {
|
|
42
|
+
await writeFile(join(testDir, "manifest.json"), "not valid json");
|
|
43
|
+
|
|
44
|
+
const result = await readManifest(testDir);
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("collectGeneratedPaths", () => {
|
|
50
|
+
const testDir = "/tmp/rtfct-collect-test";
|
|
51
|
+
const presetsDir = join(testDir, ".project", "presets");
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
await mkdir(presetsDir, { recursive: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
await rm(testDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns default paths when no presets exist", async () => {
|
|
62
|
+
// Empty presets directory
|
|
63
|
+
const paths = await collectGeneratedPaths(testDir);
|
|
64
|
+
expect(paths).toEqual(["src/", "tests/"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("collects paths from single preset", async () => {
|
|
68
|
+
const presetDir = join(presetsDir, "zig");
|
|
69
|
+
await mkdir(presetDir);
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(presetDir, "manifest.json"),
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
name: "zig",
|
|
74
|
+
version: "1.0.0",
|
|
75
|
+
description: "Zig preset",
|
|
76
|
+
generated_paths: ["src/", "build.zig"],
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const paths = await collectGeneratedPaths(testDir);
|
|
81
|
+
expect(paths).toContain("src/");
|
|
82
|
+
expect(paths).toContain("build.zig");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("collects paths from multiple presets", async () => {
|
|
86
|
+
// Create zig preset
|
|
87
|
+
const zigDir = join(presetsDir, "zig");
|
|
88
|
+
await mkdir(zigDir);
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(zigDir, "manifest.json"),
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
name: "zig",
|
|
93
|
+
version: "1.0.0",
|
|
94
|
+
description: "Zig",
|
|
95
|
+
generated_paths: ["src/", "build.zig"],
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Create typescript preset
|
|
100
|
+
const tsDir = join(presetsDir, "typescript");
|
|
101
|
+
await mkdir(tsDir);
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(tsDir, "manifest.json"),
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
name: "typescript",
|
|
106
|
+
version: "1.0.0",
|
|
107
|
+
description: "TypeScript",
|
|
108
|
+
generated_paths: ["src/", "tests/"],
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const paths = await collectGeneratedPaths(testDir);
|
|
113
|
+
expect(paths).toContain("src/");
|
|
114
|
+
expect(paths).toContain("build.zig");
|
|
115
|
+
expect(paths).toContain("tests/");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("deduplicates paths from multiple presets", async () => {
|
|
119
|
+
// Both presets have src/
|
|
120
|
+
const zigDir = join(presetsDir, "zig");
|
|
121
|
+
await mkdir(zigDir);
|
|
122
|
+
await writeFile(
|
|
123
|
+
join(zigDir, "manifest.json"),
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
name: "zig",
|
|
126
|
+
version: "1.0.0",
|
|
127
|
+
description: "Zig",
|
|
128
|
+
generated_paths: ["src/"],
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const tsDir = join(presetsDir, "typescript");
|
|
133
|
+
await mkdir(tsDir);
|
|
134
|
+
await writeFile(
|
|
135
|
+
join(tsDir, "manifest.json"),
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
name: "typescript",
|
|
138
|
+
version: "1.0.0",
|
|
139
|
+
description: "TypeScript",
|
|
140
|
+
generated_paths: ["src/"],
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const paths = await collectGeneratedPaths(testDir);
|
|
145
|
+
// Should only have src/ once
|
|
146
|
+
const srcCount = paths.filter((p) => p === "src/").length;
|
|
147
|
+
expect(srcCount).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("returns default when presets directory does not exist", async () => {
|
|
151
|
+
await rm(presetsDir, { recursive: true, force: true });
|
|
152
|
+
const paths = await collectGeneratedPaths(testDir);
|
|
153
|
+
expect(paths).toEqual(["src/", "tests/"]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for the Preset System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { mkdir, rm, stat } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
resolvePreset,
|
|
10
|
+
resolvePresetSync,
|
|
11
|
+
writePreset,
|
|
12
|
+
isPresetInstalled,
|
|
13
|
+
BASE_PRESET,
|
|
14
|
+
} from "../../src/presets";
|
|
15
|
+
import { ZIG_PRESET } from "../../src/presets/zig";
|
|
16
|
+
import { TYPESCRIPT_PRESET } from "../../src/presets/typescript";
|
|
17
|
+
import { ELIXIR_PRESET } from "../../src/presets/elixir";
|
|
18
|
+
import { parseGitHubRef } from "../../src/presets/github";
|
|
19
|
+
|
|
20
|
+
describe("resolvePresetSync", () => {
|
|
21
|
+
test("resolves base preset", () => {
|
|
22
|
+
const result = resolvePresetSync("base");
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
if (result.success) {
|
|
25
|
+
expect(result.preset.name).toBe("base");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("resolves zig preset", () => {
|
|
30
|
+
const result = resolvePresetSync("zig");
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
if (result.success) {
|
|
33
|
+
expect(result.preset.name).toBe("zig");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolves typescript preset", () => {
|
|
38
|
+
const result = resolvePresetSync("typescript");
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
if (result.success) {
|
|
41
|
+
expect(result.preset.name).toBe("typescript");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("resolves elixir preset", () => {
|
|
46
|
+
const result = resolvePresetSync("elixir");
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
if (result.success) {
|
|
49
|
+
expect(result.preset.name).toBe("elixir");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("is case insensitive", () => {
|
|
54
|
+
const result = resolvePresetSync("ZIG");
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("returns error for unknown preset", () => {
|
|
59
|
+
const result = resolvePresetSync("unknown");
|
|
60
|
+
expect(result.success).toBe(false);
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
expect(result.error).toContain("Unknown built-in preset");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolvePreset (async)", () => {
|
|
68
|
+
test("resolves built-in presets", async () => {
|
|
69
|
+
const result = await resolvePreset("base");
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
if (result.success) {
|
|
72
|
+
expect(result.preset.name).toBe("base");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("is case insensitive", async () => {
|
|
77
|
+
const result = await resolvePreset("ZIG");
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns error for unknown preset", async () => {
|
|
82
|
+
const result = await resolvePreset("unknown");
|
|
83
|
+
expect(result.success).toBe(false);
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
expect(result.error).toContain("Unknown preset");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns error for local presets (not yet supported)", async () => {
|
|
90
|
+
const result = await resolvePreset("./local-preset");
|
|
91
|
+
expect(result.success).toBe(false);
|
|
92
|
+
if (!result.success) {
|
|
93
|
+
expect(result.error).toContain("Local presets not yet supported");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns error for absolute local presets (not yet supported)", async () => {
|
|
98
|
+
const result = await resolvePreset("/absolute/path");
|
|
99
|
+
expect(result.success).toBe(false);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
expect(result.error).toContain("Local presets not yet supported");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("attempts GitHub resolution for owner/repo format", async () => {
|
|
106
|
+
// This will fail because the repo doesn't exist, but it should attempt GitHub resolution
|
|
107
|
+
const result = await resolvePreset("nonexistent-owner/nonexistent-repo");
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
// Should be a GitHub-related error, not "Unknown preset"
|
|
111
|
+
expect(result.error).toContain("nonexistent-owner/nonexistent-repo");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("parseGitHubRef", () => {
|
|
117
|
+
test("parses owner/repo format", () => {
|
|
118
|
+
const ref = parseGitHubRef("mattneel/rtfct-preset");
|
|
119
|
+
expect(ref).not.toBeNull();
|
|
120
|
+
expect(ref!.owner).toBe("mattneel");
|
|
121
|
+
expect(ref!.repo).toBe("rtfct-preset");
|
|
122
|
+
expect(ref!.ref).toBe("main");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("parses owner/repo@branch format", () => {
|
|
126
|
+
const ref = parseGitHubRef("mattneel/rtfct-preset@develop");
|
|
127
|
+
expect(ref).not.toBeNull();
|
|
128
|
+
expect(ref!.owner).toBe("mattneel");
|
|
129
|
+
expect(ref!.repo).toBe("rtfct-preset");
|
|
130
|
+
expect(ref!.ref).toBe("develop");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("parses owner/repo@tag format", () => {
|
|
134
|
+
const ref = parseGitHubRef("mattneel/rtfct-preset@v1.0.0");
|
|
135
|
+
expect(ref).not.toBeNull();
|
|
136
|
+
expect(ref!.owner).toBe("mattneel");
|
|
137
|
+
expect(ref!.repo).toBe("rtfct-preset");
|
|
138
|
+
expect(ref!.ref).toBe("v1.0.0");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns null for invalid format (no slash)", () => {
|
|
142
|
+
const ref = parseGitHubRef("invalid");
|
|
143
|
+
expect(ref).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("returns null for invalid format (empty owner)", () => {
|
|
147
|
+
const ref = parseGitHubRef("/repo");
|
|
148
|
+
expect(ref).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("returns null for invalid format (empty repo)", () => {
|
|
152
|
+
const ref = parseGitHubRef("owner/");
|
|
153
|
+
expect(ref).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns null for invalid format (empty branch)", () => {
|
|
157
|
+
const ref = parseGitHubRef("owner/repo@");
|
|
158
|
+
expect(ref).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("returns null for too many slashes", () => {
|
|
162
|
+
const ref = parseGitHubRef("owner/repo/extra");
|
|
163
|
+
expect(ref).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("writePreset", () => {
|
|
168
|
+
const testDir = "/tmp/rtfct-preset-write-test";
|
|
169
|
+
|
|
170
|
+
beforeEach(async () => {
|
|
171
|
+
await mkdir(join(testDir, ".project", "presets"), { recursive: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterEach(async () => {
|
|
175
|
+
await rm(testDir, { recursive: true, force: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("writes preset files to project", async () => {
|
|
179
|
+
await writePreset(testDir, ZIG_PRESET);
|
|
180
|
+
|
|
181
|
+
const presetDir = join(testDir, ".project", "presets", "zig");
|
|
182
|
+
const stats = await stat(presetDir);
|
|
183
|
+
expect(stats.isDirectory()).toBe(true);
|
|
184
|
+
|
|
185
|
+
// Check manifest exists
|
|
186
|
+
const manifestStats = await stat(join(presetDir, "manifest.json"));
|
|
187
|
+
expect(manifestStats.isFile()).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("writes all preset files", async () => {
|
|
191
|
+
await writePreset(testDir, ZIG_PRESET);
|
|
192
|
+
|
|
193
|
+
const presetDir = join(testDir, ".project", "presets", "zig");
|
|
194
|
+
|
|
195
|
+
// Check files exist
|
|
196
|
+
for (const file of ZIG_PRESET.files) {
|
|
197
|
+
const filePath = join(presetDir, file.path);
|
|
198
|
+
const stats = await stat(filePath);
|
|
199
|
+
expect(stats.isFile()).toBe(true);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("isPresetInstalled", () => {
|
|
205
|
+
const testDir = "/tmp/rtfct-preset-installed-test";
|
|
206
|
+
|
|
207
|
+
beforeEach(async () => {
|
|
208
|
+
await mkdir(join(testDir, ".project", "presets"), { recursive: true });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(async () => {
|
|
212
|
+
await rm(testDir, { recursive: true, force: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("returns false when preset not installed", async () => {
|
|
216
|
+
const result = await isPresetInstalled(testDir, "zig");
|
|
217
|
+
expect(result).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("returns true when preset is installed", async () => {
|
|
221
|
+
await writePreset(testDir, ZIG_PRESET);
|
|
222
|
+
const result = await isPresetInstalled(testDir, "zig");
|
|
223
|
+
expect(result).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("preset content", () => {
|
|
228
|
+
test("base preset has all Sacred Text files", () => {
|
|
229
|
+
expect(BASE_PRESET.files.length).toBeGreaterThan(0);
|
|
230
|
+
const paths = BASE_PRESET.files.map((f) => f.path);
|
|
231
|
+
expect(paths).toContain("protocol.md");
|
|
232
|
+
expect(paths).toContain("theology.md");
|
|
233
|
+
expect(paths).toContain("kickstart.md");
|
|
234
|
+
expect(paths).toContain("guardrails.md");
|
|
235
|
+
expect(paths).toContain("kanban/backlog.md");
|
|
236
|
+
expect(paths).toContain("kanban/in-progress.md");
|
|
237
|
+
expect(paths).toContain("kanban/done.md");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("base preset has correct manifest", () => {
|
|
241
|
+
expect(BASE_PRESET.manifest.name).toBe("base");
|
|
242
|
+
expect(BASE_PRESET.manifest.version).toBe("0.1.0");
|
|
243
|
+
expect(BASE_PRESET.manifest.generated_paths).toContain("src/");
|
|
244
|
+
expect(BASE_PRESET.manifest.generated_paths).toContain("tests/");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("zig preset has required files", () => {
|
|
248
|
+
expect(ZIG_PRESET.files.length).toBeGreaterThan(0);
|
|
249
|
+
const paths = ZIG_PRESET.files.map((f) => f.path);
|
|
250
|
+
expect(paths).toContain("testing/strategy.md");
|
|
251
|
+
expect(paths).toContain("guardrails.md");
|
|
252
|
+
expect(paths).toContain("design/patterns.md");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("zig preset has build.zig in generated_paths", () => {
|
|
256
|
+
expect(ZIG_PRESET.manifest.generated_paths).toContain("build.zig");
|
|
257
|
+
expect(ZIG_PRESET.manifest.generated_paths).toContain("build.zig.zon");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("zig preset depends on base", () => {
|
|
261
|
+
expect(ZIG_PRESET.manifest.depends).toContain("base");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("typescript preset has required files", () => {
|
|
265
|
+
expect(TYPESCRIPT_PRESET.files.length).toBeGreaterThan(0);
|
|
266
|
+
const paths = TYPESCRIPT_PRESET.files.map((f) => f.path);
|
|
267
|
+
expect(paths).toContain("testing/strategy.md");
|
|
268
|
+
expect(paths).toContain("guardrails.md");
|
|
269
|
+
expect(paths).toContain("design/patterns.md");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("typescript preset has dist in generated_paths", () => {
|
|
273
|
+
expect(TYPESCRIPT_PRESET.manifest.generated_paths).toContain("dist/");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("typescript preset depends on base", () => {
|
|
277
|
+
expect(TYPESCRIPT_PRESET.manifest.depends).toContain("base");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("elixir preset has required files", () => {
|
|
281
|
+
expect(ELIXIR_PRESET.files.length).toBeGreaterThan(0);
|
|
282
|
+
const paths = ELIXIR_PRESET.files.map((f) => f.path);
|
|
283
|
+
expect(paths).toContain("testing/strategy.md");
|
|
284
|
+
expect(paths).toContain("guardrails.md");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("all presets have generated_paths", () => {
|
|
288
|
+
expect(BASE_PRESET.manifest.generated_paths.length).toBeGreaterThan(0);
|
|
289
|
+
expect(ZIG_PRESET.manifest.generated_paths.length).toBeGreaterThan(0);
|
|
290
|
+
expect(TYPESCRIPT_PRESET.manifest.generated_paths.length).toBeGreaterThan(
|
|
291
|
+
0
|
|
292
|
+
);
|
|
293
|
+
expect(ELIXIR_PRESET.manifest.generated_paths.length).toBeGreaterThan(0);
|
|
294
|
+
});
|
|
295
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"lib": ["ESNext"],
|
|
15
|
+
"types": ["bun-types"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|