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.
Files changed (39) hide show
  1. package/.project/adrs/001-use-bun-typescript.md +52 -0
  2. package/.project/guardrails.md +65 -0
  3. package/.project/kanban/backlog.md +7 -0
  4. package/.project/kanban/done.md +240 -0
  5. package/.project/kanban/in-progress.md +11 -0
  6. package/.project/kickstart.md +63 -0
  7. package/.project/protocol.md +134 -0
  8. package/.project/specs/requirements.md +152 -0
  9. package/.project/testing/strategy.md +123 -0
  10. package/.project/theology.md +125 -0
  11. package/CLAUDE.md +119 -0
  12. package/README.md +143 -0
  13. package/package.json +31 -0
  14. package/src/args.ts +104 -0
  15. package/src/commands/add.ts +78 -0
  16. package/src/commands/init.ts +128 -0
  17. package/src/commands/praise.ts +19 -0
  18. package/src/commands/regenerate.ts +122 -0
  19. package/src/commands/status.ts +163 -0
  20. package/src/help.ts +52 -0
  21. package/src/index.ts +102 -0
  22. package/src/kanban.ts +83 -0
  23. package/src/manifest.ts +67 -0
  24. package/src/presets/base.ts +195 -0
  25. package/src/presets/elixir.ts +118 -0
  26. package/src/presets/github.ts +194 -0
  27. package/src/presets/index.ts +154 -0
  28. package/src/presets/typescript.ts +589 -0
  29. package/src/presets/zig.ts +494 -0
  30. package/tests/integration/add.test.ts +104 -0
  31. package/tests/integration/init.test.ts +197 -0
  32. package/tests/integration/praise.test.ts +36 -0
  33. package/tests/integration/regenerate.test.ts +154 -0
  34. package/tests/integration/status.test.ts +165 -0
  35. package/tests/unit/args.test.ts +144 -0
  36. package/tests/unit/kanban.test.ts +162 -0
  37. package/tests/unit/manifest.test.ts +155 -0
  38. package/tests/unit/presets.test.ts +295 -0
  39. package/tsconfig.json +19 -0
package/src/args.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The Argument Parser — Interprets the Tech-Priest's Commands
3
+ *
4
+ * Parses CLI arguments into structured commands and flags.
5
+ */
6
+
7
+ export type Command = "init" | "add" | "status" | "regenerate" | "praise";
8
+
9
+ export interface ParsedFlags {
10
+ help: boolean;
11
+ version: boolean;
12
+ force: boolean;
13
+ yes: boolean;
14
+ with: string[];
15
+ }
16
+
17
+ export interface ParsedArgs {
18
+ command?: Command;
19
+ args: string[];
20
+ flags: ParsedFlags;
21
+ error?: string;
22
+ }
23
+
24
+ const VALID_COMMANDS: Command[] = [
25
+ "init",
26
+ "add",
27
+ "status",
28
+ "regenerate",
29
+ "praise",
30
+ ];
31
+
32
+ /**
33
+ * Parse command line arguments into structured form.
34
+ * @param argv - Raw arguments (typically process.argv.slice(2))
35
+ */
36
+ export const parseArgs = (argv: string[]): ParsedArgs => {
37
+ const flags: ParsedFlags = {
38
+ help: false,
39
+ version: false,
40
+ force: false,
41
+ yes: false,
42
+ with: [],
43
+ };
44
+
45
+ const args: string[] = [];
46
+ let command: Command | undefined;
47
+ let error: string | undefined;
48
+
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+
52
+ // Handle flags
53
+ if (arg.startsWith("--")) {
54
+ const flag = arg.slice(2);
55
+
56
+ if (flag === "help") {
57
+ flags.help = true;
58
+ } else if (flag === "version") {
59
+ flags.version = true;
60
+ } else if (flag === "force") {
61
+ flags.force = true;
62
+ } else if (flag === "yes") {
63
+ flags.yes = true;
64
+ } else if (flag === "with") {
65
+ // Next argument is the preset list
66
+ const nextArg = argv[++i];
67
+ if (nextArg) {
68
+ flags.with = nextArg.split(",").map((s) => s.trim());
69
+ }
70
+ } else {
71
+ error = `Unknown flag: --${flag}`;
72
+ break;
73
+ }
74
+ } else if (arg.startsWith("-")) {
75
+ const flag = arg.slice(1);
76
+
77
+ if (flag === "h") {
78
+ flags.help = true;
79
+ } else if (flag === "v") {
80
+ flags.version = true;
81
+ } else if (flag === "f") {
82
+ flags.force = true;
83
+ } else if (flag === "y") {
84
+ flags.yes = true;
85
+ } else {
86
+ error = `Unknown flag: -${flag}`;
87
+ break;
88
+ }
89
+ } else if (!command) {
90
+ // First non-flag argument is the command
91
+ if (VALID_COMMANDS.includes(arg as Command)) {
92
+ command = arg as Command;
93
+ } else {
94
+ error = `Unknown command: ${arg}`;
95
+ break;
96
+ }
97
+ } else {
98
+ // Additional arguments after command
99
+ args.push(arg);
100
+ }
101
+ }
102
+
103
+ return { command, args, flags, error };
104
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The Add Command — Incorporate a Codex into an Existing Project
3
+ *
4
+ * Adds a preset to an already consecrated project.
5
+ */
6
+
7
+ import { stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import { resolvePreset, writePreset, isPresetInstalled } from "../presets";
10
+
11
+ export interface AddResult {
12
+ success: boolean;
13
+ message: string;
14
+ }
15
+
16
+ /**
17
+ * Run the add command to incorporate a preset.
18
+ */
19
+ export const runAdd = async (
20
+ targetDir: string,
21
+ presetName: string
22
+ ): Promise<AddResult> => {
23
+ const projectDir = join(targetDir, ".project");
24
+
25
+ // Check if .project/ exists
26
+ try {
27
+ const stats = await stat(projectDir);
28
+ if (!stats.isDirectory()) {
29
+ return {
30
+ success: false,
31
+ message:
32
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
33
+ };
34
+ }
35
+ } catch {
36
+ return {
37
+ success: false,
38
+ message:
39
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
40
+ };
41
+ }
42
+
43
+ // Resolve the preset
44
+ const result = await resolvePreset(presetName);
45
+ if (!result.success) {
46
+ return {
47
+ success: false,
48
+ message: result.error,
49
+ };
50
+ }
51
+
52
+ // Check if already installed
53
+ if (await isPresetInstalled(targetDir, result.preset.name)) {
54
+ return {
55
+ success: false,
56
+ message: `Preset '${presetName}' is already incorporated into this project.`,
57
+ };
58
+ }
59
+
60
+ // Write the preset
61
+ await writePreset(targetDir, result.preset);
62
+
63
+ return {
64
+ success: true,
65
+ message: `Codex '${presetName}' has been incorporated.`,
66
+ };
67
+ };
68
+
69
+ /**
70
+ * Format the add result for CLI output.
71
+ */
72
+ export const formatAdd = (result: AddResult): string => {
73
+ if (result.success) {
74
+ return `✓ ${result.message}\n\nThe Omnissiah provides.`;
75
+ } else {
76
+ return `✗ ${result.message}`;
77
+ }
78
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The Init Command — Consecrate a New Project
3
+ *
4
+ * Creates the .project/ folder structure with Sacred Texts.
5
+ */
6
+
7
+ import { mkdir, writeFile, rm, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import { resolvePreset, writePreset, BASE_PRESET } from "../presets";
10
+
11
+ export interface InitOptions {
12
+ force?: boolean;
13
+ presets?: string[];
14
+ }
15
+
16
+ export interface InitResult {
17
+ success: boolean;
18
+ message: string;
19
+ presetErrors?: string[];
20
+ }
21
+
22
+ /**
23
+ * Run the init command to consecrate a project.
24
+ */
25
+ export const runInit = async (
26
+ targetDir: string,
27
+ options: InitOptions = {}
28
+ ): Promise<InitResult> => {
29
+ const projectDir = join(targetDir, ".project");
30
+
31
+ // Check if .project/ already exists
32
+ try {
33
+ const stats = await stat(projectDir);
34
+ if (stats.isDirectory()) {
35
+ if (options.force) {
36
+ // Purify existing .project/
37
+ await rm(projectDir, { recursive: true });
38
+ } else {
39
+ return {
40
+ success: false,
41
+ message:
42
+ "The .project/ folder already exists. Use --force to purify and recreate.",
43
+ };
44
+ }
45
+ }
46
+ } catch {
47
+ // Directory doesn't exist, which is what we want
48
+ }
49
+
50
+ // Create the directory structure
51
+ const directories = [
52
+ projectDir,
53
+ join(projectDir, "specs"),
54
+ join(projectDir, "design"),
55
+ join(projectDir, "adrs"),
56
+ join(projectDir, "kanban"),
57
+ join(projectDir, "testing"),
58
+ join(projectDir, "references"),
59
+ join(projectDir, "presets"),
60
+ ];
61
+
62
+ for (const dir of directories) {
63
+ await mkdir(dir, { recursive: true });
64
+ }
65
+
66
+ // Write the Sacred Texts from the Base Codex
67
+ for (const file of BASE_PRESET.files) {
68
+ const filePath = join(projectDir, file.path);
69
+ const fileDir = join(filePath, "..");
70
+ await mkdir(fileDir, { recursive: true });
71
+ await writeFile(filePath, file.content);
72
+ }
73
+
74
+ // Install the Base Codex as a preset (for manifest tracking)
75
+ await writePreset(targetDir, BASE_PRESET);
76
+
77
+ // Handle presets if specified
78
+ const presetErrors: string[] = [];
79
+ if (options.presets && options.presets.length > 0) {
80
+ for (const presetName of options.presets) {
81
+ const result = await resolvePreset(presetName);
82
+ if (result.success) {
83
+ await writePreset(targetDir, result.preset);
84
+ } else {
85
+ presetErrors.push(result.error);
86
+ }
87
+ }
88
+ }
89
+
90
+ if (presetErrors.length > 0) {
91
+ return {
92
+ success: true,
93
+ message:
94
+ "Project consecrated with warnings. Some presets failed to install.",
95
+ presetErrors,
96
+ };
97
+ }
98
+
99
+ return {
100
+ success: true,
101
+ message: "Project consecrated. The Sacred Texts have been inscribed.",
102
+ };
103
+ };
104
+
105
+ /**
106
+ * Format the init result for CLI output.
107
+ */
108
+ export const formatInit = (result: InitResult): string => {
109
+ const lines: string[] = [];
110
+
111
+ if (result.success) {
112
+ lines.push("✓ " + result.message);
113
+ lines.push("");
114
+ lines.push("The Omnissiah provides.");
115
+ } else {
116
+ lines.push("✗ " + result.message);
117
+ }
118
+
119
+ if (result.presetErrors && result.presetErrors.length > 0) {
120
+ lines.push("");
121
+ lines.push("Preset warnings:");
122
+ for (const error of result.presetErrors) {
123
+ lines.push(" - " + error);
124
+ }
125
+ }
126
+
127
+ return lines.join("\n");
128
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The Praise Command — Recite the Sacred Litany
3
+ *
4
+ * Outputs the Litany of Deterministic Codegen.
5
+ */
6
+
7
+ const LITANY = `The flesh is weak, but the protocol is strong.
8
+ The code is temporary, but the spec endures.
9
+ The tests do not lie, and the agent does not tire.
10
+ From specification, code. From code, verification. From verification, truth.
11
+ The Omnissiah provides.
12
+ Praise the Machine Spirit.`;
13
+
14
+ /**
15
+ * Run the praise command.
16
+ */
17
+ export const runPraise = (): string => {
18
+ return LITANY;
19
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The Regenerate Command — Purify the Codebase
3
+ *
4
+ * Deletes generated paths in preparation for regeneration.
5
+ */
6
+
7
+ import { rm, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import { collectGeneratedPaths } from "../manifest";
10
+
11
+ export interface RegenerateOptions {
12
+ yes?: boolean;
13
+ }
14
+
15
+ export interface RegenerateResult {
16
+ success: boolean;
17
+ message: string;
18
+ deletedPaths?: string[];
19
+ requiresConfirmation?: boolean;
20
+ pathsToDelete?: string[];
21
+ }
22
+
23
+ /**
24
+ * Run the regenerate command to purify the codebase.
25
+ */
26
+ export const runRegenerate = async (
27
+ targetDir: string,
28
+ options: RegenerateOptions = {}
29
+ ): Promise<RegenerateResult> => {
30
+ const projectDir = join(targetDir, ".project");
31
+
32
+ // Check if .project/ exists
33
+ try {
34
+ const stats = await stat(projectDir);
35
+ if (!stats.isDirectory()) {
36
+ return {
37
+ success: false,
38
+ message:
39
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
40
+ };
41
+ }
42
+ } catch {
43
+ return {
44
+ success: false,
45
+ message:
46
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
47
+ };
48
+ }
49
+
50
+ // Collect paths to delete
51
+ const pathsToDelete = await collectGeneratedPaths(targetDir);
52
+
53
+ // If not confirmed, request confirmation
54
+ if (!options.yes) {
55
+ return {
56
+ success: true,
57
+ requiresConfirmation: true,
58
+ pathsToDelete,
59
+ message: "Confirmation required",
60
+ };
61
+ }
62
+
63
+ // Delete the paths
64
+ const deletedPaths: string[] = [];
65
+ for (const path of pathsToDelete) {
66
+ const fullPath = join(targetDir, path);
67
+ try {
68
+ await rm(fullPath, { recursive: true });
69
+ deletedPaths.push(path);
70
+ } catch {
71
+ // Path doesn't exist, which is fine
72
+ }
73
+ }
74
+
75
+ return {
76
+ success: true,
77
+ message: "The codebase has been purified.",
78
+ deletedPaths,
79
+ };
80
+ };
81
+
82
+ /**
83
+ * Format the regenerate result for CLI output.
84
+ */
85
+ export const formatRegenerate = (result: RegenerateResult): string => {
86
+ if (!result.success) {
87
+ return `✗ ${result.message}`;
88
+ }
89
+
90
+ if (result.requiresConfirmation) {
91
+ const lines: string[] = [];
92
+ lines.push("⚠ The Rite of Purification");
93
+ lines.push("");
94
+ lines.push("The following paths will be purified:");
95
+ for (const path of result.pathsToDelete || []) {
96
+ lines.push(` - ${path}`);
97
+ }
98
+ lines.push("");
99
+ lines.push("Run with --yes to confirm.");
100
+ return lines.join("\n");
101
+ }
102
+
103
+ const lines: string[] = [];
104
+ lines.push("✓ " + result.message);
105
+ lines.push("");
106
+
107
+ if (result.deletedPaths && result.deletedPaths.length > 0) {
108
+ lines.push("Purified paths:");
109
+ for (const path of result.deletedPaths) {
110
+ lines.push(` - ${path}`);
111
+ }
112
+ } else {
113
+ lines.push("No paths were purified (already clean).");
114
+ }
115
+
116
+ lines.push("");
117
+ lines.push("Invoke the Machine Spirit to regenerate.");
118
+ lines.push("");
119
+ lines.push("The Omnissiah provides.");
120
+
121
+ return lines.join("\n");
122
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The Status Command — Reveal the State of the Litany of Tasks
3
+ *
4
+ * Parses kanban markdown and displays project status.
5
+ */
6
+
7
+ import { readFile, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import { parseKanbanFile, formatRelativeTime, type Task } from "../kanban";
10
+
11
+ export interface StatusResult {
12
+ success: boolean;
13
+ message?: string;
14
+ data?: {
15
+ projectName: string;
16
+ backlogCount: number;
17
+ inProgressCount: number;
18
+ doneCount: number;
19
+ currentTask: Task | null;
20
+ lastActivity: Date | null;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Helper to check if date a is newer than date b.
26
+ */
27
+ const isNewer = (a: Date, b: Date | null): boolean => {
28
+ if (b === null) return true;
29
+ return a.getTime() > b.getTime();
30
+ };
31
+
32
+ /**
33
+ * Run the status command to display project state.
34
+ */
35
+ export const runStatus = async (targetDir: string): Promise<StatusResult> => {
36
+ const projectDir = join(targetDir, ".project");
37
+ const kanbanDir = join(projectDir, "kanban");
38
+
39
+ // Check if .project/ exists
40
+ try {
41
+ const stats = await stat(projectDir);
42
+ if (!stats.isDirectory()) {
43
+ return {
44
+ success: false,
45
+ message:
46
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
47
+ };
48
+ }
49
+ } catch {
50
+ return {
51
+ success: false,
52
+ message:
53
+ "No .project/ folder found. Run 'rtfct init' first to consecrate the project.",
54
+ };
55
+ }
56
+
57
+ // Get project name from directory
58
+ const projectName = targetDir.split("/").pop() || "unknown";
59
+
60
+ // Parse kanban files
61
+ let backlogCount = 0;
62
+ let inProgressCount = 0;
63
+ let doneCount = 0;
64
+ let currentTask: Task | null = null;
65
+ let lastActivity: Date | null = null;
66
+
67
+ try {
68
+ const backlogPath = join(kanbanDir, "backlog.md");
69
+ const backlogContent = await readFile(backlogPath, "utf-8");
70
+ const backlogStats = await stat(backlogPath);
71
+ const backlogResult = parseKanbanFile(backlogContent, "backlog");
72
+ backlogCount = backlogResult.count;
73
+ if (isNewer(backlogStats.mtime, lastActivity)) {
74
+ lastActivity = backlogStats.mtime;
75
+ }
76
+ } catch {
77
+ // File doesn't exist or isn't readable
78
+ }
79
+
80
+ try {
81
+ const inProgressPath = join(kanbanDir, "in-progress.md");
82
+ const inProgressContent = await readFile(inProgressPath, "utf-8");
83
+ const inProgressStats = await stat(inProgressPath);
84
+ const inProgressResult = parseKanbanFile(inProgressContent, "in-progress");
85
+ inProgressCount = inProgressResult.count;
86
+ currentTask = inProgressResult.currentTask;
87
+ if (isNewer(inProgressStats.mtime, lastActivity)) {
88
+ lastActivity = inProgressStats.mtime;
89
+ }
90
+ } catch {
91
+ // File doesn't exist or isn't readable
92
+ }
93
+
94
+ try {
95
+ const donePath = join(kanbanDir, "done.md");
96
+ const doneContent = await readFile(donePath, "utf-8");
97
+ const doneStats = await stat(donePath);
98
+ const doneResult = parseKanbanFile(doneContent, "done");
99
+ doneCount = doneResult.count;
100
+ if (isNewer(doneStats.mtime, lastActivity)) {
101
+ lastActivity = doneStats.mtime;
102
+ }
103
+ } catch {
104
+ // File doesn't exist or isn't readable
105
+ }
106
+
107
+ return {
108
+ success: true,
109
+ data: {
110
+ projectName,
111
+ backlogCount,
112
+ inProgressCount,
113
+ doneCount,
114
+ currentTask,
115
+ lastActivity,
116
+ },
117
+ };
118
+ };
119
+
120
+ /**
121
+ * Format the status result for CLI output.
122
+ */
123
+ export const formatStatus = (result: StatusResult): string => {
124
+ if (!result.success) {
125
+ return `✗ ${result.message}`;
126
+ }
127
+
128
+ const data = result.data!;
129
+ const lines: string[] = [];
130
+
131
+ lines.push(`rtfct: ${data.projectName}`);
132
+ lines.push("");
133
+ lines.push("══════════════════════════════════");
134
+ lines.push(" The Litany of Tasks");
135
+ lines.push("══════════════════════════════════");
136
+ lines.push(
137
+ ` Backlog: ${data.backlogCount} unordained task${data.backlogCount === 1 ? "" : "s"}`
138
+ );
139
+ lines.push(
140
+ ` In Progress: ${data.inProgressCount} ordained task${data.inProgressCount === 1 ? "" : "s"}`
141
+ );
142
+
143
+ if (data.currentTask) {
144
+ lines.push(` → [${data.currentTask.id}] ${data.currentTask.title}`);
145
+ }
146
+
147
+ lines.push(
148
+ ` Completed: ${data.doneCount} work${data.doneCount === 1 ? "" : "s"} done`
149
+ );
150
+ lines.push("══════════════════════════════════");
151
+ lines.push("");
152
+
153
+ if (data.lastActivity) {
154
+ lines.push(`Last activity: ${formatRelativeTime(data.lastActivity)}`);
155
+ } else {
156
+ lines.push("Last activity: unknown");
157
+ }
158
+
159
+ lines.push("");
160
+ lines.push("The Omnissiah provides.");
161
+
162
+ return lines.join("\n");
163
+ };
package/src/help.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The Help System — Guidance for the Tech-Priest
3
+ *
4
+ * Displays usage information, version, and error messages.
5
+ */
6
+
7
+ const VERSION = "0.1.0";
8
+
9
+ const USAGE = `
10
+ rtfct — The Ritual Factory
11
+
12
+ USAGE:
13
+ rtfct <command> [options]
14
+
15
+ COMMANDS:
16
+ init Consecrate a new project with Sacred Texts
17
+ add <preset> Incorporate a Codex into an existing project
18
+ status Reveal the state of the Litany of Tasks
19
+ regenerate Purify the codebase for regeneration
20
+ praise Recite the Litany of Deterministic Codegen
21
+
22
+ OPTIONS:
23
+ --help, -h Display this sacred guidance
24
+ --version, -v Display the version of rtfct
25
+ --force, -f Purify existing .project/ and recreate (init only)
26
+ --yes, -y Skip confirmation prompts
27
+ --with <list> Comma-separated Codices to include (init only)
28
+
29
+ EXAMPLES:
30
+ rtfct init # Basic consecration
31
+ rtfct init --with zig # Consecrate with Zig Codex
32
+ rtfct init --with zig,elixir # Multiple Codices
33
+ rtfct add typescript # Add TypeScript Codex
34
+ rtfct status # View task progress
35
+ rtfct regenerate --yes # Purify without confirmation
36
+
37
+ The Omnissiah provides.
38
+ `.trim();
39
+
40
+ export const printHelp = (): void => {
41
+ console.log(USAGE);
42
+ };
43
+
44
+ export const printVersion = (): void => {
45
+ console.log(`rtfct v${VERSION}`);
46
+ };
47
+
48
+ export const printError = (message: string): void => {
49
+ console.error(`Error: ${message}`);
50
+ console.error("");
51
+ console.error("Run 'rtfct --help' for usage.");
52
+ };