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
@@ -0,0 +1,194 @@
1
+ /**
2
+ * GitHub Preset Resolution — Fetching Codices from the Cloud
3
+ *
4
+ * Enables downloading presets from GitHub repositories.
5
+ */
6
+
7
+ import type { Preset, PresetFile } from "./index";
8
+
9
+ export interface GitHubPresetRef {
10
+ owner: string;
11
+ repo: string;
12
+ ref: string; // branch, tag, or commit (default: main)
13
+ }
14
+
15
+ /**
16
+ * Parse a GitHub preset reference string.
17
+ * Formats:
18
+ * - "owner/repo" -> uses "main" branch
19
+ * - "owner/repo@branch" -> uses specified branch/tag/commit
20
+ */
21
+ export const parseGitHubRef = (name: string): GitHubPresetRef | null => {
22
+ // Check for @ symbol for branch specification
23
+ const atIndex = name.indexOf("@");
24
+
25
+ let ownerRepo: string;
26
+ let ref = "main";
27
+
28
+ if (atIndex !== -1) {
29
+ ownerRepo = name.slice(0, atIndex);
30
+ ref = name.slice(atIndex + 1);
31
+ if (!ref) {
32
+ return null; // Invalid: "owner/repo@" with no branch
33
+ }
34
+ } else {
35
+ ownerRepo = name;
36
+ }
37
+
38
+ const parts = ownerRepo.split("/");
39
+ if (parts.length !== 2) {
40
+ return null;
41
+ }
42
+
43
+ const [owner, repo] = parts;
44
+ if (!owner || !repo) {
45
+ return null;
46
+ }
47
+
48
+ return { owner, repo, ref };
49
+ };
50
+
51
+ /**
52
+ * Fetch a file from GitHub raw content.
53
+ */
54
+ const fetchGitHubFile = async (
55
+ ref: GitHubPresetRef,
56
+ path: string
57
+ ): Promise<string | null> => {
58
+ const url = `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${ref.ref}/${path}`;
59
+
60
+ try {
61
+ const response = await fetch(url);
62
+ if (!response.ok) {
63
+ return null;
64
+ }
65
+ return await response.text();
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Fetch the directory listing from GitHub API.
73
+ */
74
+ const fetchGitHubDirectory = async (
75
+ ref: GitHubPresetRef,
76
+ path: string = ""
77
+ ): Promise<string[]> => {
78
+ const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/contents/${path}?ref=${ref.ref}`;
79
+
80
+ try {
81
+ const response = await fetch(url, {
82
+ headers: {
83
+ Accept: "application/vnd.github.v3+json",
84
+ "User-Agent": "rtfct-cli",
85
+ },
86
+ });
87
+
88
+ if (!response.ok) {
89
+ return [];
90
+ }
91
+
92
+ const data = (await response.json()) as Array<{
93
+ type: string;
94
+ path: string;
95
+ name: string;
96
+ }>;
97
+
98
+ const files: string[] = [];
99
+
100
+ for (const item of data) {
101
+ if (item.type === "file") {
102
+ files.push(item.path);
103
+ } else if (item.type === "dir") {
104
+ // Recursively fetch directory contents
105
+ const subFiles = await fetchGitHubDirectory(ref, item.path);
106
+ files.push(...subFiles);
107
+ }
108
+ }
109
+
110
+ return files;
111
+ } catch {
112
+ return [];
113
+ }
114
+ };
115
+
116
+ /**
117
+ * Resolve a GitHub preset reference to a Preset object.
118
+ */
119
+ export const resolveGitHubPreset = async (
120
+ name: string
121
+ ): Promise<{ success: true; preset: Preset } | { success: false; error: string }> => {
122
+ const ref = parseGitHubRef(name);
123
+
124
+ if (!ref) {
125
+ return {
126
+ success: false,
127
+ error: `Invalid GitHub preset format: ${name}. Use "owner/repo" or "owner/repo@branch".`,
128
+ };
129
+ }
130
+
131
+ // First, fetch the manifest.json
132
+ const manifestContent = await fetchGitHubFile(ref, "manifest.json");
133
+
134
+ if (!manifestContent) {
135
+ return {
136
+ success: false,
137
+ error: `Could not fetch manifest.json from ${name}. Ensure the repository exists and contains a valid preset.`,
138
+ };
139
+ }
140
+
141
+ let manifest: Preset["manifest"];
142
+ try {
143
+ manifest = JSON.parse(manifestContent);
144
+ } catch {
145
+ return {
146
+ success: false,
147
+ error: `Invalid manifest.json in ${name}. The file must be valid JSON.`,
148
+ };
149
+ }
150
+
151
+ // Validate manifest has required fields
152
+ if (!manifest.name || !manifest.version || !manifest.generated_paths) {
153
+ return {
154
+ success: false,
155
+ error: `Invalid manifest.json in ${name}. Missing required fields (name, version, generated_paths).`,
156
+ };
157
+ }
158
+
159
+ // Fetch all files in the repository (excluding manifest.json)
160
+ const allFiles = await fetchGitHubDirectory(ref);
161
+
162
+ if (allFiles.length === 0) {
163
+ return {
164
+ success: false,
165
+ error: `Could not fetch file list from ${name}. Check if the repository is accessible.`,
166
+ };
167
+ }
168
+
169
+ // Filter out manifest.json and fetch content for each file
170
+ const presetFiles: PresetFile[] = [];
171
+
172
+ for (const filePath of allFiles) {
173
+ // Skip manifest.json and hidden files
174
+ if (filePath === "manifest.json" || filePath.startsWith(".")) {
175
+ continue;
176
+ }
177
+
178
+ const content = await fetchGitHubFile(ref, filePath);
179
+ if (content !== null) {
180
+ presetFiles.push({
181
+ path: filePath,
182
+ content,
183
+ });
184
+ }
185
+ }
186
+
187
+ const preset: Preset = {
188
+ name: manifest.name,
189
+ manifest,
190
+ files: presetFiles,
191
+ };
192
+
193
+ return { success: true, preset };
194
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The Preset System — Codex Resolution and Installation
3
+ *
4
+ * Resolves preset names to Codex content and writes them to projects.
5
+ */
6
+
7
+ import { mkdir, writeFile } from "fs/promises";
8
+ import { join } from "path";
9
+
10
+ import { BASE_PRESET } from "./base";
11
+ import { ZIG_PRESET } from "./zig";
12
+ import { TYPESCRIPT_PRESET } from "./typescript";
13
+ import { ELIXIR_PRESET } from "./elixir";
14
+ import { parseGitHubRef, resolveGitHubPreset } from "./github";
15
+
16
+ export interface PresetFile {
17
+ path: string;
18
+ content: string;
19
+ }
20
+
21
+ export interface Preset {
22
+ name: string;
23
+ manifest: {
24
+ name: string;
25
+ version: string;
26
+ description: string;
27
+ depends?: string[];
28
+ generated_paths: string[];
29
+ };
30
+ files: PresetFile[];
31
+ }
32
+
33
+ export type ResolveResult =
34
+ | { success: true; preset: Preset }
35
+ | { success: false; error: string };
36
+
37
+ export type AsyncResolveResult = Promise<ResolveResult>;
38
+
39
+ type BuiltInPresetName = "base" | "zig" | "typescript" | "elixir";
40
+
41
+ const BUILT_IN_PRESETS: Record<BuiltInPresetName, Preset> = {
42
+ base: BASE_PRESET,
43
+ zig: ZIG_PRESET,
44
+ typescript: TYPESCRIPT_PRESET,
45
+ elixir: ELIXIR_PRESET,
46
+ };
47
+
48
+ // Re-export base preset for direct use by init command
49
+ export { BASE_PRESET } from "./base";
50
+
51
+ /**
52
+ * Resolve a preset name to a Preset object (synchronous, built-in only).
53
+ */
54
+ export const resolvePresetSync = (name: string): ResolveResult => {
55
+ const lowerName = name.toLowerCase();
56
+
57
+ if (lowerName in BUILT_IN_PRESETS) {
58
+ return {
59
+ success: true,
60
+ preset: BUILT_IN_PRESETS[lowerName as BuiltInPresetName],
61
+ };
62
+ }
63
+
64
+ return {
65
+ success: false,
66
+ error: `Unknown built-in preset: ${name}. Available: base, zig, typescript, elixir`,
67
+ };
68
+ };
69
+
70
+ /**
71
+ * Resolve a preset name to a Preset object.
72
+ * Supports built-in presets, GitHub presets (owner/repo), and local presets.
73
+ */
74
+ export const resolvePreset = async (name: string): AsyncResolveResult => {
75
+ const lowerName = name.toLowerCase();
76
+
77
+ // Check built-in presets first
78
+ if (lowerName in BUILT_IN_PRESETS) {
79
+ return {
80
+ success: true,
81
+ preset: BUILT_IN_PRESETS[lowerName as BuiltInPresetName],
82
+ };
83
+ }
84
+
85
+ // Check local paths (not yet supported)
86
+ if (name.startsWith("./") || name.startsWith("/")) {
87
+ return {
88
+ success: false,
89
+ error: `Local presets not yet supported: ${name}`,
90
+ };
91
+ }
92
+
93
+ // Check for GitHub preset format (owner/repo or owner/repo@branch)
94
+ if (name.includes("/")) {
95
+ const ref = parseGitHubRef(name);
96
+ if (ref) {
97
+ return await resolveGitHubPreset(name);
98
+ }
99
+ return {
100
+ success: false,
101
+ error: `Invalid GitHub preset format: ${name}. Use "owner/repo" or "owner/repo@branch".`,
102
+ };
103
+ }
104
+
105
+ return {
106
+ success: false,
107
+ error: `Unknown preset: ${name}. Available: base, zig, typescript, elixir, or use owner/repo for GitHub presets.`,
108
+ };
109
+ };
110
+
111
+ /**
112
+ * Write a preset to a project's .project/presets/ directory.
113
+ */
114
+ export const writePreset = async (
115
+ projectDir: string,
116
+ preset: Preset
117
+ ): Promise<void> => {
118
+ const presetDir = join(projectDir, ".project", "presets", preset.name);
119
+
120
+ // Create the preset directory
121
+ await mkdir(presetDir, { recursive: true });
122
+
123
+ // Write the manifest
124
+ await writeFile(
125
+ join(presetDir, "manifest.json"),
126
+ JSON.stringify(preset.manifest, null, 2)
127
+ );
128
+
129
+ // Write all preset files
130
+ for (const file of preset.files) {
131
+ const filePath = join(presetDir, file.path);
132
+ const fileDir = join(filePath, "..");
133
+ await mkdir(fileDir, { recursive: true });
134
+ await writeFile(filePath, file.content);
135
+ }
136
+ };
137
+
138
+ /**
139
+ * Check if a preset is already installed in a project.
140
+ */
141
+ export const isPresetInstalled = async (
142
+ projectDir: string,
143
+ presetName: string
144
+ ): Promise<boolean> => {
145
+ const presetDir = join(projectDir, ".project", "presets", presetName);
146
+
147
+ try {
148
+ const { stat } = await import("fs/promises");
149
+ const stats = await stat(presetDir);
150
+ return stats.isDirectory();
151
+ } catch {
152
+ return false;
153
+ }
154
+ };