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
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
|
+
};
|