fabis-ralph-loop 0.1.0 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kristaps Fabians Geikins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # fabis-ralph-loop
2
+
3
+ CLI for setting up and running Claude Ralph autonomous coding loops in Docker containers.
4
+
5
+ Define a `fabis-ralph-loop.config.ts`, then use the CLI to generate Docker artifacts, manage the container lifecycle, and run iterative autonomous coding loops. Each iteration, a Claude agent picks the next user story from a PRD, implements it, runs quality checks, commits, and stops — then a fresh session picks up the next one.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add fabis-ralph-loop
11
+ ```
12
+
13
+ Requires Node.js >= 22.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Scaffold config file and generate all files
19
+ npx fabis-ralph-loop init
20
+
21
+ # 2. Edit fabis-ralph-loop.config.ts to match your project
22
+
23
+ # 3. Regenerate files after config changes
24
+ npx fabis-ralph-loop generate
25
+
26
+ # 4. Start the container (auto-attaches a shell)
27
+ npx fabis-ralph-loop start
28
+ ```
29
+
30
+ Before starting the container, prepare a PRD using the generated skills in your IDE:
31
+
32
+ ```
33
+ /prd # describe your feature → .ralph/prd-feature.md
34
+ /ralph # convert PRD to JSON → .ralph/prd.json
35
+ ```
36
+
37
+ Then inside the container, kick off the loop:
38
+
39
+ ```bash
40
+ # Run 20 iterations of the autonomous coding loop
41
+ run-fabis-ralph-loop 20
42
+
43
+ # Override the model
44
+ run-fabis-ralph-loop 20 --model opus
45
+
46
+ # Enable verbose progress output
47
+ run-fabis-ralph-loop 20 --verbose
48
+ ```
49
+
50
+ `run-fabis-ralph-loop` is a wrapper script installed in the container's PATH that pins the same CLI version as the host. Ctrl+C gracefully stops the current iteration; pressing it twice force-exits.
51
+
52
+ ## How It Works
53
+
54
+ 1. **Generate** — `fabis-ralph-loop generate` creates Docker artifacts (`.ralph-container/`), a prompt template (`ralph-prompt.md`), and AI skills
55
+ 2. **Prepare a PRD** — Use `/prd` in your IDE to write a Product Requirements Document, then `/ralph` to convert it into `.ralph/prd.json`
56
+ 3. **Start** — `fabis-ralph-loop start` builds the container image, starts it, runs the entrypoint (git safety, auth validation, direnv, setup hooks), then drops you into a shell
57
+ 4. **Run the loop** — `run-fabis-ralph-loop <iterations>` feeds the prompt to the Claude agent each iteration. The agent reads the PRD, picks the highest-priority incomplete story, implements it, runs backpressure commands (lint, typecheck, tests), commits, and stops. The next iteration picks up the next story.
58
+ 5. **Completion** — When all stories pass, the agent outputs a completion signal and the loop exits early
59
+
60
+ The container is sandboxed: git pushes are blocked, and each iteration runs in a fresh agent session so context doesn't leak between stories.
61
+
62
+ ## Configuration
63
+
64
+ Create a `fabis-ralph-loop.config.ts` in your project root:
65
+
66
+ ```ts
67
+ import { defineConfig } from 'fabis-ralph-loop'
68
+
69
+ export default defineConfig({
70
+ project: {
71
+ name: 'my-project',
72
+ description: 'What the project does',
73
+ context: 'Additional context for the AI agent',
74
+ backpressureCommands: [
75
+ { name: 'typecheck', command: 'pnpm tsc --noEmit' },
76
+ { name: 'lint', command: 'pnpm eslint .' },
77
+ ],
78
+ },
79
+ container: {
80
+ name: 'my-project-ralph',
81
+ playwright: true, // auto-configures Playwright MCP + headless Chromium
82
+ systemPackages: ['ripgrep'],
83
+ env: { NODE_ENV: 'development' },
84
+ hooks: {
85
+ rootSetup: ['apt-get install -y some-package'],
86
+ userSetup: ['npm install -g some-tool'],
87
+ entrypointSetup: ['pnpm install'],
88
+ },
89
+ },
90
+ defaults: {
91
+ model: 'sonnet',
92
+ sleepBetweenMs: 2000,
93
+ },
94
+ output: {
95
+ mode: 'direct', // or 'uac' for universal-ai-config integration
96
+ },
97
+ })
98
+ ```
99
+
100
+ ### Overrides Config
101
+
102
+ For environment-specific settings that shouldn't be committed (different models, debug flags, local API keys), create a `fabis-ralph-loop.overrides.config.ts`:
103
+
104
+ ```ts
105
+ import { defineOverridesConfig } from 'fabis-ralph-loop'
106
+
107
+ export default defineOverridesConfig({
108
+ defaults: {
109
+ model: 'opus',
110
+ verbose: true,
111
+ },
112
+ container: {
113
+ env: { DEBUG: 'true' },
114
+ },
115
+ })
116
+ ```
117
+
118
+ The overrides file is **gitignored automatically** by `fabis-ralph-loop init`. It gets deep-merged on top of the base config: objects merge recursively, arrays are replaced entirely, and scalars are overwritten.
119
+
120
+ ## CLI Commands
121
+
122
+ All commands run on the **host machine** via `npx fabis-ralph-loop <command>`:
123
+
124
+ | Command | Description |
125
+ | ---------- | --------------------------------------------------------- |
126
+ | `init` | Scaffold config and generate all files |
127
+ | `generate` | Regenerate files from config |
128
+ | `start` | Build and start the container (attaches shell by default) |
129
+ | `stop` | Stop and remove the container |
130
+ | `restart` | Stop + start the container |
131
+ | `logs` | Follow container logs |
132
+ | `run <n>` | Execute _n_ loop iterations (can also run from host) |
133
+ | `exec` | Run an arbitrary command inside the container |
134
+
135
+ ### Notable flags
136
+
137
+ - `generate --dry-run` — preview without writing files
138
+ - `generate --only <container|prompt|skills>` — generate a specific subset
139
+ - `start --no-attach` / `restart --no-attach` — don't attach a shell after starting
140
+ - `run --model <model>` — override the default model
141
+ - `run --verbose` — enable verbose progress output
142
+
143
+ ### Inside the container
144
+
145
+ The container has `run-fabis-ralph-loop` on the PATH — this is the primary way to kick off loop iterations. It wraps `fabis-ralph-loop run` pinned to the same version as your host install.
146
+
147
+ ## Generated Skills
148
+
149
+ `fabis-ralph-loop generate` seeds AI skills into your project that power the Ralph workflow. These are slash commands available to the Claude agent both inside the container and in your IDE:
150
+
151
+ - **`/prd`** — Generate a Product Requirements Document. Describe a feature, answer a few clarifying questions, and get a structured PRD saved to `.ralph/prd-<feature>.md`.
152
+ - **`/ralph`** — Convert a PRD into `.ralph/prd.json`, the structured format the loop consumes. Splits stories into iteration-sized chunks, orders by dependencies, and ensures each has verifiable acceptance criteria.
153
+ - **`/update-fabis-ralph-loop-config`** — Edit `fabis-ralph-loop.config.ts` without manually reading the schema. Useful for adding backpressure commands, container packages, env vars, or hooks.
154
+
155
+ ### Typical workflow
156
+
157
+ ```
158
+ # In your IDE (before starting the container):
159
+ /prd # describe your feature, answer questions → .ralph/prd-feature.md
160
+ /ralph # convert PRD to JSON → .ralph/prd.json
161
+
162
+ # Inside the container:
163
+ run-fabis-ralph-loop 20 # let the loop implement it
164
+ ```
165
+
166
+ ## What Gets Generated
167
+
168
+ Running `generate` creates:
169
+
170
+ - **`.ralph-container/Dockerfile`** — container image with system packages, user setup, and optional Playwright
171
+ - **`.ralph-container/docker-compose.yml`** — compose config with volumes, networking, and environment
172
+ - **`.ralph-container/entrypoint.ts`** — container entrypoint that bootstraps the environment
173
+ - **`ralph-prompt.md`** — the prompt fed to the AI agent each iteration
174
+ - **Skills** — AI tool skills for PRD creation and Ralph workflow (output location depends on `output.mode`)
175
+
176
+ ## Programmatic API
177
+
178
+ ```ts
179
+ import { loadRalphConfig, generateAll, defineConfig } from 'fabis-ralph-loop'
180
+ import type { RalphLoopConfig, ResolvedConfig } from 'fabis-ralph-loop'
181
+
182
+ const { config, projectRoot } = await loadRalphConfig()
183
+ await generateAll(config, projectRoot)
184
+ ```
185
+
186
+ ## License
187
+
188
+ MIT
package/dist/generate.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as loadRalphConfig } from "./loader.mjs";
2
- import { t as generateAll } from "./generators.mjs";
2
+ import { n as generateAll, t as ensureGitignoreBlock } from "./gitignore.mjs";
3
3
  import { defineCommand } from "citty";
4
4
 
5
5
  //#region src/commands/generate.ts
@@ -21,6 +21,7 @@ var generate_default = defineCommand({
21
21
  },
22
22
  async run({ args }) {
23
23
  const config = await loadRalphConfig();
24
+ if (!args["dry-run"]) await ensureGitignoreBlock();
24
25
  const only = args.only;
25
26
  await generateAll(config, process.cwd(), {
26
27
  dryRun: args["dry-run"],
@@ -1 +1 @@
1
- {"version":3,"file":"generate.mjs","names":[],"sources":["../src/commands/generate.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { generateAll } from '../generators/index.js'\n\nexport default defineCommand({\n meta: {\n name: 'generate',\n description: 'Regenerate all files from config (idempotent)',\n },\n args: {\n 'dry-run': {\n type: 'boolean',\n description: 'Preview what would be generated',\n default: false,\n },\n only: {\n type: 'string',\n description: 'Only generate specific type: container|prompt|skills',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const only = args.only as 'container' | 'prompt' | 'skills' | undefined\n await generateAll(config, process.cwd(), {\n dryRun: args['dry-run'],\n only,\n })\n },\n})\n"],"mappings":";;;;;AAIA,uBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM;EACJ,WAAW;GACT,MAAM;GACN,aAAa;GACb,SAAS;GACV;EACD,MAAM;GACJ,MAAM;GACN,aAAa;GACd;EACF;CACD,MAAM,IAAI,EAAE,QAAQ;EAClB,MAAM,SAAS,MAAM,iBAAiB;EACtC,MAAM,OAAO,KAAK;AAClB,QAAM,YAAY,QAAQ,QAAQ,KAAK,EAAE;GACvC,QAAQ,KAAK;GACb;GACD,CAAC;;CAEL,CAAC"}
1
+ {"version":3,"file":"generate.mjs","names":[],"sources":["../src/commands/generate.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { generateAll } from '../generators/index.js'\nimport { ensureGitignoreBlock } from '../utils/gitignore.js'\n\nexport default defineCommand({\n meta: {\n name: 'generate',\n description: 'Regenerate all files from config (idempotent)',\n },\n args: {\n 'dry-run': {\n type: 'boolean',\n description: 'Preview what would be generated',\n default: false,\n },\n only: {\n type: 'string',\n description: 'Only generate specific type: container|prompt|skills',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n if (!args['dry-run']) {\n await ensureGitignoreBlock()\n }\n const only = args.only as 'container' | 'prompt' | 'skills' | undefined\n await generateAll(config, process.cwd(), {\n dryRun: args['dry-run'],\n only,\n })\n },\n})\n"],"mappings":";;;;;AAKA,uBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM;EACJ,WAAW;GACT,MAAM;GACN,aAAa;GACb,SAAS;GACV;EACD,MAAM;GACJ,MAAM;GACN,aAAa;GACd;EACF;CACD,MAAM,IAAI,EAAE,QAAQ;EAClB,MAAM,SAAS,MAAM,iBAAiB;AACtC,MAAI,CAAC,KAAK,WACR,OAAM,sBAAsB;EAE9B,MAAM,OAAO,KAAK;AAClB,QAAM,YAAY,QAAQ,QAAQ,KAAK,EAAE;GACvC,QAAQ,KAAK;GACb;GACD,CAAC;;CAEL,CAAC"}
@@ -218,5 +218,23 @@ async function generateAll(config, projectRoot, options = {}) {
218
218
  }
219
219
 
220
220
  //#endregion
221
- export { generateAll as t };
222
- //# sourceMappingURL=generators.mjs.map
221
+ //#region src/utils/gitignore.ts
222
+ const MARKER_START = "# >>> fabis-ralph-loop >>>";
223
+ const GITIGNORE_BLOCK = `${MARKER_START}
224
+ /fabis-ralph-loop.overrides.*
225
+ # <<< fabis-ralph-loop <<<`;
226
+ /**
227
+ * Idempotently add fabis-ralph-loop gitignore entries to .gitignore.
228
+ * Uses marker comments to detect existing blocks and avoid duplication.
229
+ */
230
+ async function ensureGitignoreBlock(cwd = process.cwd()) {
231
+ const gitignorePath = join(cwd, ".gitignore");
232
+ let content = "";
233
+ if (existsSync(gitignorePath)) content = await readFile(gitignorePath, "utf8");
234
+ if (content.includes(MARKER_START)) return;
235
+ await writeFile(gitignorePath, content.trimEnd() ? `${content.trimEnd()}\n\n${GITIGNORE_BLOCK}\n` : `${GITIGNORE_BLOCK}\n`, "utf8");
236
+ }
237
+
238
+ //#endregion
239
+ export { generateAll as n, ensureGitignoreBlock as t };
240
+ //# sourceMappingURL=gitignore.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitignore.mjs","names":[],"sources":["../src/utils/template.ts","../src/utils/version.ts","../src/generators/dockerfile.ts","../src/generators/compose.ts","../src/generators/entrypoint.ts","../src/generators/prompt.ts","../src/generators/skills.ts","../src/generators/index.ts","../src/utils/gitignore.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport ejs from 'ejs'\n\n/**\n * Resolve a bundled asset directory (templates, static, uac-templates).\n * In dist (flat layout): assets are siblings of the compiled files.\n * In src (nested layout via tsx): assets are siblings of the parent dir.\n */\nexport function resolveAssetDir(assetName: string, metaUrl: string): string {\n const dir = dirname(fileURLToPath(metaUrl))\n const sibling = join(dir, assetName)\n if (existsSync(sibling)) return sibling\n return join(dir, '..', assetName)\n}\n\nconst TEMPLATES_DIR = resolveAssetDir('templates', import.meta.url)\n\nexport async function renderTemplate(\n templateName: string,\n data: Record<string, unknown>,\n): Promise<string> {\n const templatePath = join(TEMPLATES_DIR, templateName)\n const template = await readFile(templatePath, 'utf8')\n return ejs.render(template, data, { async: false }) as string\n}\n\nexport const GENERATED_HEADER = `# Generated by fabis-ralph-loop — DO NOT EDIT MANUALLY\n# Regenerate with: npx fabis-ralph-loop generate\n`\n","import { createRequire } from 'node:module'\n\nexport function getPackageVersion(): string {\n try {\n const require = createRequire(import.meta.url)\n const pkg = require('../../package.json') as { version: string }\n return pkg.version\n } catch {\n return 'latest'\n }\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport { getPackageVersion } from '../utils/version.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Detect whether the base image already includes Node.js.\n */\nfunction isNodeBaseImage(baseImage: string): boolean {\n return /^node[:/]/i.test(baseImage)\n}\n\nexport async function generateDockerfile(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('Dockerfile.ejs', {\n generatedHeader: GENERATED_HEADER,\n baseImage: config.container.baseImage,\n systemPackages: config.container.systemPackages,\n installNode: !isNodeBaseImage(config.container.baseImage),\n playwright: config.container.playwright,\n hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateCompose(config: ResolvedConfig): Promise<string> {\n const homeDir = `/home/${config.container.user}`\n\n // Ensure .claude config is always persisted with the correct home dir\n const persistVolumes: Record<string, string> = {\n 'ralph-claude-config': `${homeDir}/.claude`,\n ...Object.fromEntries(\n Object.entries(config.container.persistVolumes).map(([name, path]) => [\n name,\n path.replace('/home/sandbox', homeDir),\n ]),\n ),\n }\n\n return renderTemplate('docker-compose.yml.ejs', {\n generatedHeader: GENERATED_HEADER,\n containerName: config.container.name,\n shmSize: config.container.shmSize,\n networkMode: config.container.networkMode,\n capabilities: config.container.capabilities,\n shadowVolumes: config.container.shadowVolumes,\n persistVolumes,\n extraVolumes: config.container.volumes,\n env: config.container.env,\n homeDir,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateEntrypoint(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('entrypoint.ts.ejs', {\n generatedHeader: GENERATED_HEADER.replace(/^# /gm, '// '),\n agent: config.defaults.agent,\n shadowVolumes: config.container.shadowVolumes,\n entrypointSetup: config.container.hooks.entrypointSetup,\n user,\n homeDir: `/home/${user}`,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generatePrompt(config: ResolvedConfig): Promise<string> {\n return renderTemplate('ralph-prompt.md.ejs', {\n generatedHeader: GENERATED_HEADER,\n projectName: config.project.name,\n projectDescription: config.project.description,\n projectContext: config.project.context,\n backpressureCommands: config.project.backpressureCommands,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n completionSignal: config.defaults.completionSignal,\n })\n}\n","import { readdir, readFile, writeFile, mkdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { ResolvedConfig } from '../config/schema.js'\nimport { resolveAssetDir } from '../utils/template.js'\n\nconst UAC_TEMPLATES_DIR = resolveAssetDir('uac-templates', import.meta.url)\n\nfunction buildLevel1Variables(config: ResolvedConfig): Record<string, unknown> {\n return {\n backpressureCommands: config.project.backpressureCommands,\n projectName: config.project.name,\n projectContext: config.project.context,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n config,\n }\n}\n\nasync function discoverSkills(): Promise<string[]> {\n const skillsDir = join(UAC_TEMPLATES_DIR, 'skills')\n const entries = await readdir(skillsDir, { withFileTypes: true })\n return entries.filter((e) => e.isDirectory()).map((e) => e.name)\n}\n\nexport async function generateSkills(config: ResolvedConfig, projectRoot: string): Promise<void> {\n if (config.output.mode === 'direct') {\n await generateDirect(config, projectRoot)\n } else {\n await generateUac(config, projectRoot)\n }\n}\n\nasync function generateDirect(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n\n // Render Level 1 EJS and write to a temp dir structured as UAC templates\n const tempDir = join(tmpdir(), `ralph-skills-${Date.now()}`)\n\n try {\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n const rendered = ejs.render(template, variables) as string\n\n const outDir = join(tempDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n }\n\n // Use UAC's generate() API for the second pass (handles Level 2 EJS + frontmatter mapping)\n const files = await generate({\n root: projectRoot,\n targets: ['claude'],\n types: ['skills'],\n overrides: { templatesDir: tempDir },\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\n } finally {\n await rm(tempDir, { recursive: true, force: true })\n }\n}\n\nasync function generateUac(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n let count = 0\n\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n // Render Level 1 EJS — Level 2 <%% %> becomes <% %> in output\n const rendered = ejs.render(template, variables) as string\n\n const outDir = join(projectRoot, config.output.uacTemplatesDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n count++\n }\n\n consola.info(`Generated ${count} skill template(s) to ${config.output.uacTemplatesDir}/skills/`)\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\nimport { generateDockerfile } from './dockerfile.js'\nimport { generateCompose } from './compose.js'\nimport { generateEntrypoint } from './entrypoint.js'\nimport { generatePrompt } from './prompt.js'\nimport { generateSkills } from './skills.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface GenerateOptions {\n dryRun?: boolean\n only?: 'container' | 'prompt' | 'skills'\n}\n\ninterface GeneratedFile {\n path: string\n content: string\n}\n\nexport async function generateAll(\n config: ResolvedConfig,\n projectRoot: string,\n options: GenerateOptions = {},\n): Promise<GeneratedFile[]> {\n const files: GeneratedFile[] = []\n\n if (!options.only || options.only === 'container') {\n const containerDir = join(projectRoot, '.ralph-container')\n await mkdir(containerDir, { recursive: true })\n\n const dockerfile = await generateDockerfile(config)\n files.push({ path: join('.ralph-container', 'Dockerfile'), content: dockerfile })\n\n const entrypoint = await generateEntrypoint(config)\n files.push({ path: join('.ralph-container', 'entrypoint.ts'), content: entrypoint })\n\n const compose = await generateCompose(config)\n files.push({ path: join('.ralph-container', 'docker-compose.yml'), content: compose })\n }\n\n if (!options.only || options.only === 'prompt') {\n const prompt = await generatePrompt(config)\n files.push({ path: join('.ralph-container', 'ralph-prompt.md'), content: prompt })\n }\n\n if (options.dryRun) {\n for (const file of files) {\n consola.info(`[dry-run] Would write: ${file.path}`)\n }\n } else {\n for (const file of files) {\n const fullPath = join(projectRoot, file.path)\n await mkdir(join(fullPath, '..'), { recursive: true })\n await writeFile(fullPath, file.content, 'utf8')\n consola.success(`Written: ${file.path}`)\n }\n }\n\n if (!options.only || options.only === 'skills') {\n if (options.dryRun) {\n consola.info('[dry-run] Would generate skills')\n } else {\n await generateSkills(config, projectRoot)\n }\n }\n\n return files\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst MARKER_START = '# >>> fabis-ralph-loop >>>'\nconst MARKER_END = '# <<< fabis-ralph-loop <<<'\n\nconst GITIGNORE_BLOCK = `${MARKER_START}\n/fabis-ralph-loop.overrides.*\n${MARKER_END}`\n\n/**\n * Idempotently add fabis-ralph-loop gitignore entries to .gitignore.\n * Uses marker comments to detect existing blocks and avoid duplication.\n */\nexport async function ensureGitignoreBlock(cwd: string = process.cwd()): Promise<void> {\n const gitignorePath = join(cwd, '.gitignore')\n\n let content = ''\n if (existsSync(gitignorePath)) {\n content = await readFile(gitignorePath, 'utf8')\n }\n\n if (content.includes(MARKER_START)) return\n\n const newContent = content.trimEnd()\n ? `${content.trimEnd()}\\n\\n${GITIGNORE_BLOCK}\\n`\n : `${GITIGNORE_BLOCK}\\n`\n\n await writeFile(gitignorePath, newContent, 'utf8')\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAWA,SAAgB,gBAAgB,WAAmB,SAAyB;CAC1E,MAAM,MAAM,QAAQ,cAAc,QAAQ,CAAC;CAC3C,MAAM,UAAU,KAAK,KAAK,UAAU;AACpC,KAAI,WAAW,QAAQ,CAAE,QAAO;AAChC,QAAO,KAAK,KAAK,MAAM,UAAU;;AAGnC,MAAM,gBAAgB,gBAAgB,aAAa,OAAO,KAAK,IAAI;AAEnE,eAAsB,eACpB,cACA,MACiB;CAEjB,MAAM,WAAW,MAAM,SADF,KAAK,eAAe,aAAa,EACR,OAAO;AACrD,QAAO,IAAI,OAAO,UAAU,MAAM,EAAE,OAAO,OAAO,CAAC;;AAGrD,MAAa,mBAAmB;;;;;;AC3BhC,SAAgB,oBAA4B;AAC1C,KAAI;AAGF,SAFgB,cAAc,OAAO,KAAK,IAAI,CAC1B,qBAAqB,CAC9B;SACL;AACN,SAAO;;;;;;;;;ACDX,SAAS,gBAAgB,WAA4B;AACnD,QAAO,aAAa,KAAK,UAAU;;AAGrC,eAAsB,mBAAmB,QAAyC;CAChF,MAAM,OAAO,OAAO,UAAU;AAC9B,QAAO,eAAe,kBAAkB;EACtC,iBAAiB;EACjB,WAAW,OAAO,UAAU;EAC5B,gBAAgB,OAAO,UAAU;EACjC,aAAa,CAAC,gBAAgB,OAAO,UAAU,UAAU;EACzD,YAAY,OAAO,UAAU;EAC7B,OAAO,OAAO,UAAU;EACxB;EACA,YAAY,SAAS;EACrB,SAAS,SAAS;EAClB,gBAAgB,mBAAmB;EACpC,CAAC;;;;;ACrBJ,eAAsB,gBAAgB,QAAyC;CAC7E,MAAM,UAAU,SAAS,OAAO,UAAU;CAG1C,MAAM,iBAAyC;EAC7C,uBAAuB,GAAG,QAAQ;EAClC,GAAG,OAAO,YACR,OAAO,QAAQ,OAAO,UAAU,eAAe,CAAC,KAAK,CAAC,MAAM,UAAU,CACpE,MACA,KAAK,QAAQ,iBAAiB,QAAQ,CACvC,CAAC,CACH;EACF;AAED,QAAO,eAAe,0BAA0B;EAC9C,iBAAiB;EACjB,eAAe,OAAO,UAAU;EAChC,SAAS,OAAO,UAAU;EAC1B,aAAa,OAAO,UAAU;EAC9B,cAAc,OAAO,UAAU;EAC/B,eAAe,OAAO,UAAU;EAChC;EACA,cAAc,OAAO,UAAU;EAC/B,KAAK,OAAO,UAAU;EACtB;EACD,CAAC;;;;;ACzBJ,eAAsB,mBAAmB,QAAyC;CAChF,MAAM,OAAO,OAAO,UAAU;AAC9B,QAAO,eAAe,qBAAqB;EACzC,iBAAiB,iBAAiB,QAAQ,SAAS,MAAM;EACzD,OAAO,OAAO,SAAS;EACvB,eAAe,OAAO,UAAU;EAChC,iBAAiB,OAAO,UAAU,MAAM;EACxC;EACA,SAAS,SAAS;EACnB,CAAC;;;;;ACTJ,eAAsB,eAAe,QAAyC;AAC5E,QAAO,eAAe,uBAAuB;EAC3C,iBAAiB;EACjB,aAAa,OAAO,QAAQ;EAC5B,oBAAoB,OAAO,QAAQ;EACnC,gBAAgB,OAAO,QAAQ;EAC/B,sBAAsB,OAAO,QAAQ;EACrC,cAAc,OAAO,QAAQ;EAC7B,YAAY,OAAO,UAAU;EAC7B,kBAAkB,OAAO,SAAS;EACnC,CAAC;;;;;ACJJ,MAAM,oBAAoB,gBAAgB,iBAAiB,OAAO,KAAK,IAAI;AAE3E,SAAS,qBAAqB,QAAiD;AAC7E,QAAO;EACL,sBAAsB,OAAO,QAAQ;EACrC,aAAa,OAAO,QAAQ;EAC5B,gBAAgB,OAAO,QAAQ;EAC/B,cAAc,OAAO,QAAQ;EAC7B,YAAY,OAAO,UAAU;EAC7B;EACD;;AAGH,eAAe,iBAAoC;AAGjD,SADgB,MAAM,QADJ,KAAK,mBAAmB,SAAS,EACV,EAAE,eAAe,MAAM,CAAC,EAClD,QAAQ,MAAM,EAAE,aAAa,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK;;AAGlE,eAAsB,eAAe,QAAwB,aAAoC;AAC/F,KAAI,OAAO,OAAO,SAAS,SACzB,OAAM,eAAe,QAAQ,YAAY;KAEzC,OAAM,YAAY,QAAQ,YAAY;;AAI1C,eAAe,eAAe,QAAwB,aAAoC;CACxF,MAAM,YAAY,qBAAqB,OAAO;CAC9C,MAAM,SAAS,MAAM,gBAAgB;CAGrC,MAAM,UAAU,KAAK,QAAQ,EAAE,gBAAgB,KAAK,KAAK,GAAG;AAE5D,KAAI;AACF,OAAK,MAAM,SAAS,QAAQ;GAE1B,MAAM,WAAW,MAAM,SADF,KAAK,mBAAmB,UAAU,OAAO,WAAW,EAC3B,OAAO;GACrD,MAAM,WAAW,IAAI,OAAO,UAAU,UAAU;GAEhD,MAAM,SAAS,KAAK,SAAS,UAAU,MAAM;AAC7C,SAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;AACxC,SAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,UAAU,OAAO;;EAI7D,MAAM,QAAQ,MAAM,SAAS;GAC3B,MAAM;GACN,SAAS,CAAC,SAAS;GACnB,OAAO,CAAC,SAAS;GACjB,WAAW,EAAE,cAAc,SAAS;GACrC,CAAC;AAEF,QAAM,oBAAoB,OAAO,YAAY;AAC7C,UAAQ,KAAK,aAAa,MAAM,OAAO,gBAAgB;WAC/C;AACR,QAAM,GAAG,SAAS;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;;;AAIvD,eAAe,YAAY,QAAwB,aAAoC;CACrF,MAAM,YAAY,qBAAqB,OAAO;CAC9C,MAAM,SAAS,MAAM,gBAAgB;CACrC,IAAI,QAAQ;AAEZ,MAAK,MAAM,SAAS,QAAQ;EAE1B,MAAM,WAAW,MAAM,SADF,KAAK,mBAAmB,UAAU,OAAO,WAAW,EAC3B,OAAO;EAErD,MAAM,WAAW,IAAI,OAAO,UAAU,UAAU;EAEhD,MAAM,SAAS,KAAK,aAAa,OAAO,OAAO,iBAAiB,UAAU,MAAM;AAChF,QAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;AACxC,QAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,UAAU,OAAO;AAC3D;;AAGF,SAAQ,KAAK,aAAa,MAAM,wBAAwB,OAAO,OAAO,gBAAgB,UAAU;;;;;AClElG,eAAsB,YACpB,QACA,aACA,UAA2B,EAAE,EACH;CAC1B,MAAM,QAAyB,EAAE;AAEjC,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,aAAa;AAEjD,QAAM,MADe,KAAK,aAAa,mBAAmB,EAChC,EAAE,WAAW,MAAM,CAAC;EAE9C,MAAM,aAAa,MAAM,mBAAmB,OAAO;AACnD,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,aAAa;GAAE,SAAS;GAAY,CAAC;EAEjF,MAAM,aAAa,MAAM,mBAAmB,OAAO;AACnD,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,gBAAgB;GAAE,SAAS;GAAY,CAAC;EAEpF,MAAM,UAAU,MAAM,gBAAgB,OAAO;AAC7C,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,qBAAqB;GAAE,SAAS;GAAS,CAAC;;AAGxF,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,UAAU;EAC9C,MAAM,SAAS,MAAM,eAAe,OAAO;AAC3C,QAAM,KAAK;GAAE,MAAM,KAAK,oBAAoB,kBAAkB;GAAE,SAAS;GAAQ,CAAC;;AAGpF,KAAI,QAAQ,OACV,MAAK,MAAM,QAAQ,MACjB,SAAQ,KAAK,0BAA0B,KAAK,OAAO;KAGrD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,aAAa,KAAK,KAAK;AAC7C,QAAM,MAAM,KAAK,UAAU,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AACtD,QAAM,UAAU,UAAU,KAAK,SAAS,OAAO;AAC/C,UAAQ,QAAQ,YAAY,KAAK,OAAO;;AAI5C,KAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,SACpC,KAAI,QAAQ,OACV,SAAQ,KAAK,kCAAkC;KAE/C,OAAM,eAAe,QAAQ,YAAY;AAI7C,QAAO;;;;;AC/DT,MAAM,eAAe;AAGrB,MAAM,kBAAkB,GAAG,aAAa;;;;;;;AAQxC,eAAsB,qBAAqB,MAAc,QAAQ,KAAK,EAAiB;CACrF,MAAM,gBAAgB,KAAK,KAAK,aAAa;CAE7C,IAAI,UAAU;AACd,KAAI,WAAW,cAAc,CAC3B,WAAU,MAAM,SAAS,eAAe,OAAO;AAGjD,KAAI,QAAQ,SAAS,aAAa,CAAE;AAMpC,OAAM,UAAU,eAJG,QAAQ,SAAS,GAChC,GAAG,QAAQ,SAAS,CAAC,MAAM,gBAAgB,MAC3C,GAAG,gBAAgB,KAEoB,OAAO"}
package/dist/index.d.mts CHANGED
@@ -71,7 +71,27 @@ interface GeneratedFile {
71
71
  }
72
72
  declare function generateAll(config: ResolvedConfig, projectRoot: string, options?: GenerateOptions): Promise<GeneratedFile[]>;
73
73
  //#endregion
74
+ //#region src/config/merge.d.ts
75
+ /**
76
+ * Deep merge two config objects.
77
+ *
78
+ * Merge strategy:
79
+ * - Arrays: overrides REPLACE base arrays entirely
80
+ * - Plain objects: merge recursively
81
+ * - Scalars: overrides replace base values
82
+ * - undefined values in overrides are skipped
83
+ */
84
+ declare function mergeConfigs<T extends Record<string, unknown>>(base: T, overrides: Record<string, unknown>): T;
85
+ //#endregion
86
+ //#region src/utils/gitignore.d.ts
87
+ /**
88
+ * Idempotently add fabis-ralph-loop gitignore entries to .gitignore.
89
+ * Uses marker comments to detect existing blocks and avoid duplication.
90
+ */
91
+ declare function ensureGitignoreBlock(cwd?: string): Promise<void>;
92
+ //#endregion
74
93
  //#region src/index.d.ts
94
+ type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
75
95
  /**
76
96
  * Helper for defining a typed ralph-loop config.
77
97
  */
@@ -120,6 +140,59 @@ declare function defineConfig(config: RalphLoopConfig): {
120
140
  uacTemplatesDir?: string | undefined;
121
141
  } | undefined;
122
142
  };
143
+ /**
144
+ * Helper for defining a typed ralph-loop overrides config.
145
+ * All fields are optional — only specify what you want to override.
146
+ */
147
+ declare function defineOverridesConfig(config: DeepPartial<RalphLoopConfig>): {
148
+ project?: {
149
+ name?: string | undefined;
150
+ description?: string | undefined;
151
+ context?: string | undefined;
152
+ backpressureCommands?: ({
153
+ name?: string | undefined;
154
+ command?: string | undefined;
155
+ } | undefined)[] | undefined;
156
+ openAppSkill?: string | undefined;
157
+ } | undefined;
158
+ container?: {
159
+ name?: string | undefined;
160
+ baseImage?: string | undefined;
161
+ user?: string | undefined;
162
+ systemPackages?: (string | undefined)[] | undefined;
163
+ playwright?: boolean | undefined;
164
+ networkMode?: string | undefined;
165
+ env?: {
166
+ [x: string]: string | undefined;
167
+ } | undefined;
168
+ shmSize?: string | undefined;
169
+ capabilities?: (string | undefined)[] | undefined;
170
+ volumes?: (string | undefined)[] | undefined;
171
+ shadowVolumes?: (string | undefined)[] | undefined;
172
+ persistVolumes?: {
173
+ [x: string]: string | undefined;
174
+ } | undefined;
175
+ hooks?: {
176
+ rootSetup?: (string | undefined)[] | undefined;
177
+ userSetup?: (string | undefined)[] | undefined;
178
+ entrypointSetup?: (string | undefined)[] | undefined;
179
+ } | undefined;
180
+ } | undefined;
181
+ setup?: {
182
+ preStartCommand?: string | undefined;
183
+ } | undefined;
184
+ defaults?: {
185
+ agent?: "claude" | undefined;
186
+ model?: string | undefined;
187
+ verbose?: boolean | undefined;
188
+ sleepBetweenMs?: number | undefined;
189
+ completionSignal?: string | undefined;
190
+ } | undefined;
191
+ output?: {
192
+ mode?: "direct" | "uac" | undefined;
193
+ uacTemplatesDir?: string | undefined;
194
+ } | undefined;
195
+ };
123
196
  //#endregion
124
- export { type BackpressureCommand, type RalphLoopConfig, type ResolvedConfig, defineConfig, generateAll, loadRalphConfig, ralphLoopConfigSchema };
197
+ export { type BackpressureCommand, DeepPartial, type RalphLoopConfig, type ResolvedConfig, defineConfig, defineOverridesConfig, ensureGitignoreBlock, generateAll, loadRalphConfig, mergeConfigs, ralphLoopConfigSchema };
125
198
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/loader.ts","../src/generators/index.ts","../src/index.ts"],"mappings":";;;cAEM,yBAAA,EAAyB,CAAA,CAAA,SAAA;;;;cAsDlB,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQtB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,qBAAA;AAAA,KACjC,cAAA,GAAiB,CAAA,CAAE,MAAA,QAAc,qBAAA;AAAA,KACjC,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;;;iBC7D3B,eAAA,CAAgB,GAAA,YAAe,OAAA,CAAQ,cAAA;;;UCKnD,eAAA;EACR,MAAA;EACA,IAAA;AAAA;AAAA,UAGQ,aAAA;EACR,IAAA;EACA,OAAA;AAAA;AAAA,iBAGoB,WAAA,CACpB,MAAA,EAAQ,cAAA,EACR,WAAA,UACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,aAAA;;;;;;iBChBK,YAAA,CAAa,MAAA,EAAD,eAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/loader.ts","../src/generators/index.ts","../src/config/merge.ts","../src/utils/gitignore.ts","../src/index.ts"],"mappings":";;;cAEM,yBAAA,EAAyB,CAAA,CAAA,SAAA;;;;cAsDlB,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQtB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,qBAAA;AAAA,KACjC,cAAA,GAAiB,CAAA,CAAE,MAAA,QAAc,qBAAA;AAAA,KACjC,mBAAA,GAAsB,CAAA,CAAE,KAAA,QAAa,yBAAA;;;iBC5D3B,eAAA,CAAgB,GAAA,YAAe,OAAA,CAAQ,cAAA;;;UCInD,eAAA;EACR,MAAA;EACA,IAAA;AAAA;AAAA,UAGQ,aAAA;EACR,IAAA;EACA,OAAA;AAAA;AAAA,iBAGoB,WAAA,CACpB,MAAA,EAAQ,cAAA,EACR,WAAA,UACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,aAAA;;;;;;AFxBY;;;;;;iBGSP,YAAA,WAAuB,MAAA,kBAAA,CACrC,IAAA,EAAM,CAAA,EACN,SAAA,EAAW,MAAA,oBACV,CAAA;;;;;;AHZoB;iBIeD,oBAAA,CAAqB,GAAA,YAA8B,OAAA;;;KCR7D,WAAA,MAAiB,CAAA,gCAAiC,CAAA,IAAK,WAAA,CAAY,CAAA,CAAE,CAAA,OAAQ,CAAA;;;;iBAKzE,YAAA,CAAa,MAAA,EAAD,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQZ,qBAAA,CACd,MAAA,EAAQ,WAAA,CAD2B,eAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as ralphLoopConfigSchema, t as loadRalphConfig } from "./loader.mjs";
2
- import { t as generateAll } from "./generators.mjs";
1
+ import { n as mergeConfigs, r as ralphLoopConfigSchema, t as loadRalphConfig } from "./loader.mjs";
2
+ import { n as generateAll, t as ensureGitignoreBlock } from "./gitignore.mjs";
3
3
 
4
4
  //#region src/index.ts
5
5
  /**
@@ -8,7 +8,14 @@ import { t as generateAll } from "./generators.mjs";
8
8
  function defineConfig(config) {
9
9
  return config;
10
10
  }
11
+ /**
12
+ * Helper for defining a typed ralph-loop overrides config.
13
+ * All fields are optional — only specify what you want to override.
14
+ */
15
+ function defineOverridesConfig(config) {
16
+ return config;
17
+ }
11
18
 
12
19
  //#endregion
13
- export { defineConfig, generateAll, loadRalphConfig, ralphLoopConfigSchema };
20
+ export { defineConfig, defineOverridesConfig, ensureGitignoreBlock, generateAll, loadRalphConfig, mergeConfigs, ralphLoopConfigSchema };
14
21
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export { ralphLoopConfigSchema } from './config/schema.js'\nexport type { RalphLoopConfig, ResolvedConfig, BackpressureCommand } from './config/schema.js'\nexport { loadRalphConfig } from './config/loader.js'\nexport { generateAll } from './generators/index.js'\n\n/**\n * Helper for defining a typed ralph-loop config.\n */\nexport function defineConfig(config: import('./config/schema.js').RalphLoopConfig) {\n return config\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,aAAa,QAAsD;AACjF,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export { ralphLoopConfigSchema } from './config/schema.js'\nexport type { RalphLoopConfig, ResolvedConfig, BackpressureCommand } from './config/schema.js'\nexport { loadRalphConfig } from './config/loader.js'\nexport { generateAll } from './generators/index.js'\nexport { mergeConfigs } from './config/merge.js'\nexport { ensureGitignoreBlock } from './utils/gitignore.js'\n\nexport type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T\n\n/**\n * Helper for defining a typed ralph-loop config.\n */\nexport function defineConfig(config: import('./config/schema.js').RalphLoopConfig) {\n return config\n}\n\n/**\n * Helper for defining a typed ralph-loop overrides config.\n * All fields are optional — only specify what you want to override.\n */\nexport function defineOverridesConfig(\n config: DeepPartial<import('./config/schema.js').RalphLoopConfig>,\n) {\n return config\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,aAAa,QAAsD;AACjF,QAAO;;;;;;AAOT,SAAgB,sBACd,QACA;AACA,QAAO"}
package/dist/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as loadRalphConfig } from "./loader.mjs";
2
- import { t as generateAll } from "./generators.mjs";
2
+ import { n as generateAll, t as ensureGitignoreBlock } from "./gitignore.mjs";
3
3
  import { writeFile } from "node:fs/promises";
4
4
  import { consola } from "consola";
5
5
  import { existsSync } from "node:fs";
@@ -45,6 +45,7 @@ var init_default = defineCommand({
45
45
  await writeFile(configPath, SAMPLE_CONFIG, "utf8");
46
46
  consola.success(`Created ${configPath}`);
47
47
  }
48
+ await ensureGitignoreBlock();
48
49
  await generateAll(await loadRalphConfig(), process.cwd());
49
50
  consola.success("Init complete. Edit fabis-ralph-loop.config.ts and run `fabis-ralph-loop generate` to regenerate.");
50
51
  }
package/dist/init.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"init.mjs","names":[],"sources":["../src/commands/init.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { consola } from 'consola'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { generateAll } from '../generators/index.js'\n\nconst SAMPLE_CONFIG = `import { defineConfig } from 'fabis-ralph-loop'\n\nexport default defineConfig({\n container: {\n name: 'my-ralph-container',\n baseImage: 'node:22-bookworm',\n // playwright: true,\n // shadowVolumes: ['/workspace/node_modules'],\n hooks: {\n rootSetup: [\n // 'RUN npm install -g pnpm@10',\n ],\n userSetup: [\n // 'RUN corepack enable',\n ],\n },\n },\n project: {\n name: 'My Project',\n description: '',\n context: '- **Monorepo** managed with npm\\\\n- **TypeScript strict mode** everywhere',\n },\n output: {\n mode: 'direct',\n },\n})\n`\n\nexport default defineCommand({\n meta: {\n name: 'init',\n description: 'Scaffold ralph-loop config and generate all files',\n },\n async run() {\n const configPath = 'fabis-ralph-loop.config.ts'\n\n if (existsSync(configPath)) {\n consola.warn(`${configPath} already exists. Regenerating files from existing config.`)\n } else {\n await writeFile(configPath, SAMPLE_CONFIG, 'utf8')\n consola.success(`Created ${configPath}`)\n }\n\n // Load config and generate\n const config = await loadRalphConfig()\n await generateAll(config, process.cwd())\n\n consola.success(\n 'Init complete. Edit fabis-ralph-loop.config.ts and run `fabis-ralph-loop generate` to regenerate.',\n )\n },\n})\n"],"mappings":";;;;;;;;AAOA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BtB,mBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,MAAM;EACV,MAAM,aAAa;AAEnB,MAAI,WAAW,WAAW,CACxB,SAAQ,KAAK,GAAG,WAAW,2DAA2D;OACjF;AACL,SAAM,UAAU,YAAY,eAAe,OAAO;AAClD,WAAQ,QAAQ,WAAW,aAAa;;AAK1C,QAAM,YADS,MAAM,iBAAiB,EACZ,QAAQ,KAAK,CAAC;AAExC,UAAQ,QACN,oGACD;;CAEJ,CAAC"}
1
+ {"version":3,"file":"init.mjs","names":[],"sources":["../src/commands/init.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { consola } from 'consola'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { generateAll } from '../generators/index.js'\nimport { ensureGitignoreBlock } from '../utils/gitignore.js'\n\nconst SAMPLE_CONFIG = `import { defineConfig } from 'fabis-ralph-loop'\n\nexport default defineConfig({\n container: {\n name: 'my-ralph-container',\n baseImage: 'node:22-bookworm',\n // playwright: true,\n // shadowVolumes: ['/workspace/node_modules'],\n hooks: {\n rootSetup: [\n // 'RUN npm install -g pnpm@10',\n ],\n userSetup: [\n // 'RUN corepack enable',\n ],\n },\n },\n project: {\n name: 'My Project',\n description: '',\n context: '- **Monorepo** managed with npm\\\\n- **TypeScript strict mode** everywhere',\n },\n output: {\n mode: 'direct',\n },\n})\n`\n\nexport default defineCommand({\n meta: {\n name: 'init',\n description: 'Scaffold ralph-loop config and generate all files',\n },\n async run() {\n const configPath = 'fabis-ralph-loop.config.ts'\n\n if (existsSync(configPath)) {\n consola.warn(`${configPath} already exists. Regenerating files from existing config.`)\n } else {\n await writeFile(configPath, SAMPLE_CONFIG, 'utf8')\n consola.success(`Created ${configPath}`)\n }\n\n await ensureGitignoreBlock()\n\n // Load config and generate\n const config = await loadRalphConfig()\n await generateAll(config, process.cwd())\n\n consola.success(\n 'Init complete. Edit fabis-ralph-loop.config.ts and run `fabis-ralph-loop generate` to regenerate.',\n )\n },\n})\n"],"mappings":";;;;;;;;AAQA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BtB,mBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,MAAM;EACV,MAAM,aAAa;AAEnB,MAAI,WAAW,WAAW,CACxB,SAAQ,KAAK,GAAG,WAAW,2DAA2D;OACjF;AACL,SAAM,UAAU,YAAY,eAAe,OAAO;AAClD,WAAQ,QAAQ,WAAW,aAAa;;AAG1C,QAAM,sBAAsB;AAI5B,QAAM,YADS,MAAM,iBAAiB,EACZ,QAAQ,KAAK,CAAC;AAExC,UAAQ,QACN,oGACD;;CAEJ,CAAC"}
package/dist/loader.mjs CHANGED
@@ -73,22 +73,58 @@ function applyPlaywrightDefaults(config) {
73
73
  };
74
74
  }
75
75
 
76
+ //#endregion
77
+ //#region src/config/merge.ts
78
+ /**
79
+ * Deep merge two config objects.
80
+ *
81
+ * Merge strategy:
82
+ * - Arrays: overrides REPLACE base arrays entirely
83
+ * - Plain objects: merge recursively
84
+ * - Scalars: overrides replace base values
85
+ * - undefined values in overrides are skipped
86
+ */
87
+ function mergeConfigs(base, overrides) {
88
+ return deepMerge(base, overrides);
89
+ }
90
+ function isPlainObject(value) {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value);
92
+ }
93
+ function deepMerge(base, overrides) {
94
+ const result = { ...base };
95
+ for (const key of Object.keys(overrides)) {
96
+ const overrideValue = overrides[key];
97
+ const baseValue = base[key];
98
+ if (overrideValue === void 0) continue;
99
+ if (Array.isArray(overrideValue)) result[key] = overrideValue;
100
+ else if (isPlainObject(overrideValue) && isPlainObject(baseValue)) result[key] = deepMerge(baseValue, overrideValue);
101
+ else result[key] = overrideValue;
102
+ }
103
+ return result;
104
+ }
105
+
76
106
  //#endregion
77
107
  //#region src/config/loader.ts
78
108
  async function loadRalphConfig(cwd) {
79
- const { config } = await loadConfig({
109
+ const { config: baseConfig } = await loadConfig({
80
110
  name: "fabis-ralph-loop",
81
111
  cwd
82
112
  });
83
- if (!config || Object.keys(config).length === 0) throw new Error("No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.");
84
- const parsed = ralphLoopConfigSchema.safeParse(config);
113
+ if (!baseConfig || Object.keys(baseConfig).length === 0) throw new Error("No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.");
114
+ const { config: overridesConfig } = await loadConfig({
115
+ name: "fabis-ralph-loop.overrides",
116
+ cwd
117
+ });
118
+ const merged = overridesConfig && Object.keys(overridesConfig).length > 0 ? mergeConfigs(baseConfig, overridesConfig) : baseConfig;
119
+ const parsed = ralphLoopConfigSchema.safeParse(merged);
85
120
  if (!parsed.success) {
86
121
  const issues = parsed.error.issues.map((issue) => ` ${issue.path.join(".")}: ${issue.message}`).join("\n");
87
- throw new Error(`Invalid fabis-ralph-loop config:\n${issues}`);
122
+ const suffix = overridesConfig && Object.keys(overridesConfig).length > 0 ? " (after merging overrides)" : "";
123
+ throw new Error(`Invalid fabis-ralph-loop config${suffix}:\n${issues}`);
88
124
  }
89
125
  return applyPlaywrightDefaults(parsed.data);
90
126
  }
91
127
 
92
128
  //#endregion
93
- export { ralphLoopConfigSchema as n, loadRalphConfig as t };
129
+ export { mergeConfigs as n, ralphLoopConfigSchema as r, loadRalphConfig as t };
94
130
  //# sourceMappingURL=loader.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.mjs","names":[],"sources":["../src/config/schema.ts","../src/config/defaults.ts","../src/config/loader.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst backpressureCommandSchema = z.object({\n name: z.string().min(1),\n command: z.string().min(1),\n})\n\nconst containerHooksSchema = z.object({\n rootSetup: z.array(z.string()).default([]),\n userSetup: z.array(z.string()).default([]),\n entrypointSetup: z.array(z.string()).default([]),\n})\n\nconst containerSchema = z.object({\n name: z.string().min(1),\n baseImage: z.string().min(1).default('node:22-bookworm'),\n user: z.string().min(1).default('sandbox'),\n systemPackages: z.array(z.string()).default([]),\n playwright: z.boolean().default(false),\n networkMode: z.string().default('host'),\n env: z.record(z.string(), z.string()).default({}),\n shmSize: z.string().default('64m'),\n capabilities: z.array(z.string()).default([]),\n volumes: z.array(z.string()).default([]),\n shadowVolumes: z.array(z.string()).default([]),\n persistVolumes: z\n .record(z.string(), z.string())\n .default({ 'ralph-claude-config': '/home/sandbox/.claude' }),\n hooks: containerHooksSchema.prefault({}),\n})\n\nconst setupSchema = z.object({\n preStartCommand: z.string().default(''),\n})\n\nconst defaultsSchema = z.object({\n agent: z.literal('claude').default('claude'),\n model: z.string().default('sonnet'),\n verbose: z.boolean().default(false),\n sleepBetweenMs: z.number().int().min(0).default(2000),\n completionSignal: z.string().default('RALPH_WORK_FULLY_DONE'),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n description: z.string().default(''),\n context: z.string().default(''),\n backpressureCommands: z.array(backpressureCommandSchema).default([]),\n openAppSkill: z.string().default(''),\n})\n\nconst outputSchema = z.object({\n mode: z.enum(['direct', 'uac']).default('direct'),\n uacTemplatesDir: z.string().default('.universal-ai-config'),\n})\n\nexport const ralphLoopConfigSchema = z.object({\n container: containerSchema.prefault({ name: 'ralph-container' }),\n setup: setupSchema.prefault({}),\n defaults: defaultsSchema.prefault({}),\n project: projectSchema,\n output: outputSchema.prefault({}),\n})\n\nexport type RalphLoopConfig = z.input<typeof ralphLoopConfigSchema>\nexport type ResolvedConfig = z.output<typeof ralphLoopConfigSchema>\nexport type BackpressureCommand = z.infer<typeof backpressureCommandSchema>\n","import type { ResolvedConfig } from './schema.js'\n\n/**\n * Apply Playwright-specific defaults when playwright is enabled.\n * Merges SYS_ADMIN capability and 2gb shm_size if not already set.\n */\nexport function applyPlaywrightDefaults(config: ResolvedConfig): ResolvedConfig {\n if (!config.container.playwright) return config\n\n const shmSize = config.container.shmSize === '64m' ? '2gb' : config.container.shmSize\n\n const capabilities = config.container.capabilities.includes('SYS_ADMIN')\n ? config.container.capabilities\n : [...config.container.capabilities, 'SYS_ADMIN']\n\n return {\n ...config,\n container: {\n ...config.container,\n shmSize,\n capabilities,\n },\n }\n}\n","import { loadConfig } from 'c12'\nimport { ralphLoopConfigSchema } from './schema.js'\nimport { applyPlaywrightDefaults } from './defaults.js'\nimport type { RalphLoopConfig, ResolvedConfig } from './schema.js'\n\nexport async function loadRalphConfig(cwd?: string): Promise<ResolvedConfig> {\n const { config } = await loadConfig<RalphLoopConfig>({\n name: 'fabis-ralph-loop',\n cwd,\n })\n\n if (!config || Object.keys(config).length === 0) {\n throw new Error('No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.')\n }\n\n const parsed = ralphLoopConfigSchema.safeParse(config)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)\n .join('\\n')\n throw new Error(`Invalid fabis-ralph-loop config:\\n${issues}`)\n }\n\n return applyPlaywrightDefaults(parsed.data)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"loader.mjs","names":[],"sources":["../src/config/schema.ts","../src/config/defaults.ts","../src/config/merge.ts","../src/config/loader.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst backpressureCommandSchema = z.object({\n name: z.string().min(1),\n command: z.string().min(1),\n})\n\nconst containerHooksSchema = z.object({\n rootSetup: z.array(z.string()).default([]),\n userSetup: z.array(z.string()).default([]),\n entrypointSetup: z.array(z.string()).default([]),\n})\n\nconst containerSchema = z.object({\n name: z.string().min(1),\n baseImage: z.string().min(1).default('node:22-bookworm'),\n user: z.string().min(1).default('sandbox'),\n systemPackages: z.array(z.string()).default([]),\n playwright: z.boolean().default(false),\n networkMode: z.string().default('host'),\n env: z.record(z.string(), z.string()).default({}),\n shmSize: z.string().default('64m'),\n capabilities: z.array(z.string()).default([]),\n volumes: z.array(z.string()).default([]),\n shadowVolumes: z.array(z.string()).default([]),\n persistVolumes: z\n .record(z.string(), z.string())\n .default({ 'ralph-claude-config': '/home/sandbox/.claude' }),\n hooks: containerHooksSchema.prefault({}),\n})\n\nconst setupSchema = z.object({\n preStartCommand: z.string().default(''),\n})\n\nconst defaultsSchema = z.object({\n agent: z.literal('claude').default('claude'),\n model: z.string().default('sonnet'),\n verbose: z.boolean().default(false),\n sleepBetweenMs: z.number().int().min(0).default(2000),\n completionSignal: z.string().default('RALPH_WORK_FULLY_DONE'),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n description: z.string().default(''),\n context: z.string().default(''),\n backpressureCommands: z.array(backpressureCommandSchema).default([]),\n openAppSkill: z.string().default(''),\n})\n\nconst outputSchema = z.object({\n mode: z.enum(['direct', 'uac']).default('direct'),\n uacTemplatesDir: z.string().default('.universal-ai-config'),\n})\n\nexport const ralphLoopConfigSchema = z.object({\n container: containerSchema.prefault({ name: 'ralph-container' }),\n setup: setupSchema.prefault({}),\n defaults: defaultsSchema.prefault({}),\n project: projectSchema,\n output: outputSchema.prefault({}),\n})\n\nexport type RalphLoopConfig = z.input<typeof ralphLoopConfigSchema>\nexport type ResolvedConfig = z.output<typeof ralphLoopConfigSchema>\nexport type BackpressureCommand = z.infer<typeof backpressureCommandSchema>\n","import type { ResolvedConfig } from './schema.js'\n\n/**\n * Apply Playwright-specific defaults when playwright is enabled.\n * Merges SYS_ADMIN capability and 2gb shm_size if not already set.\n */\nexport function applyPlaywrightDefaults(config: ResolvedConfig): ResolvedConfig {\n if (!config.container.playwright) return config\n\n const shmSize = config.container.shmSize === '64m' ? '2gb' : config.container.shmSize\n\n const capabilities = config.container.capabilities.includes('SYS_ADMIN')\n ? config.container.capabilities\n : [...config.container.capabilities, 'SYS_ADMIN']\n\n return {\n ...config,\n container: {\n ...config.container,\n shmSize,\n capabilities,\n },\n }\n}\n","/**\n * Deep merge two config objects.\n *\n * Merge strategy:\n * - Arrays: overrides REPLACE base arrays entirely\n * - Plain objects: merge recursively\n * - Scalars: overrides replace base values\n * - undefined values in overrides are skipped\n */\nexport function mergeConfigs<T extends Record<string, unknown>>(\n base: T,\n overrides: Record<string, unknown>,\n): T {\n return deepMerge(base, overrides) as T\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction deepMerge(\n base: Record<string, unknown>,\n overrides: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = { ...base }\n\n for (const key of Object.keys(overrides)) {\n const overrideValue = overrides[key]\n const baseValue = base[key]\n\n if (overrideValue === undefined) continue\n\n if (Array.isArray(overrideValue)) {\n result[key] = overrideValue\n } else if (isPlainObject(overrideValue) && isPlainObject(baseValue)) {\n result[key] = deepMerge(baseValue, overrideValue)\n } else {\n result[key] = overrideValue\n }\n }\n\n return result\n}\n","import { loadConfig } from 'c12'\nimport { ralphLoopConfigSchema } from './schema.js'\nimport { applyPlaywrightDefaults } from './defaults.js'\nimport { mergeConfigs } from './merge.js'\nimport type { RalphLoopConfig, ResolvedConfig } from './schema.js'\n\nexport async function loadRalphConfig(cwd?: string): Promise<ResolvedConfig> {\n const { config: baseConfig } = await loadConfig<RalphLoopConfig>({\n name: 'fabis-ralph-loop',\n cwd,\n })\n\n if (!baseConfig || Object.keys(baseConfig).length === 0) {\n throw new Error('No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.')\n }\n\n const { config: overridesConfig } = await loadConfig<Partial<RalphLoopConfig>>({\n name: 'fabis-ralph-loop.overrides',\n cwd,\n })\n\n const merged =\n overridesConfig && Object.keys(overridesConfig).length > 0\n ? mergeConfigs(baseConfig, overridesConfig as RalphLoopConfig)\n : baseConfig\n\n const parsed = ralphLoopConfigSchema.safeParse(merged)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)\n .join('\\n')\n const suffix =\n overridesConfig && Object.keys(overridesConfig).length > 0 ? ' (after merging overrides)' : ''\n throw new Error(`Invalid fabis-ralph-loop config${suffix}:\\n${issues}`)\n }\n\n return applyPlaywrightDefaults(parsed.data)\n}\n"],"mappings":";;;;AAEA,MAAM,4BAA4B,EAAE,OAAO;CACzC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,CAAC;AAEF,MAAM,uBAAuB,EAAE,OAAO;CACpC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC1C,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC1C,iBAAiB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACjD,CAAC;AAEF,MAAM,kBAAkB,EAAE,OAAO;CAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,mBAAmB;CACxD,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,UAAU;CAC1C,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC/C,YAAY,EAAE,SAAS,CAAC,QAAQ,MAAM;CACtC,aAAa,EAAE,QAAQ,CAAC,QAAQ,OAAO;CACvC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACjD,SAAS,EAAE,QAAQ,CAAC,QAAQ,MAAM;CAClC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC7C,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACxC,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAC9C,gBAAgB,EACb,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAC9B,QAAQ,EAAE,uBAAuB,yBAAyB,CAAC;CAC9D,OAAO,qBAAqB,SAAS,EAAE,CAAC;CACzC,CAAC;AAEF,MAAM,cAAc,EAAE,OAAO,EAC3B,iBAAiB,EAAE,QAAQ,CAAC,QAAQ,GAAG,EACxC,CAAC;AAEF,MAAM,iBAAiB,EAAE,OAAO;CAC9B,OAAO,EAAE,QAAQ,SAAS,CAAC,QAAQ,SAAS;CAC5C,OAAO,EAAE,QAAQ,CAAC,QAAQ,SAAS;CACnC,SAAS,EAAE,SAAS,CAAC,QAAQ,MAAM;CACnC,gBAAgB,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,IAAK;CACrD,kBAAkB,EAAE,QAAQ,CAAC,QAAQ,wBAAwB;CAC9D,CAAC;AAEF,MAAM,gBAAgB,EAAE,OAAO;CAC7B,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,aAAa,EAAE,QAAQ,CAAC,QAAQ,GAAG;CACnC,SAAS,EAAE,QAAQ,CAAC,QAAQ,GAAG;CAC/B,sBAAsB,EAAE,MAAM,0BAA0B,CAAC,QAAQ,EAAE,CAAC;CACpE,cAAc,EAAE,QAAQ,CAAC,QAAQ,GAAG;CACrC,CAAC;AAEF,MAAM,eAAe,EAAE,OAAO;CAC5B,MAAM,EAAE,KAAK,CAAC,UAAU,MAAM,CAAC,CAAC,QAAQ,SAAS;CACjD,iBAAiB,EAAE,QAAQ,CAAC,QAAQ,uBAAuB;CAC5D,CAAC;AAEF,MAAa,wBAAwB,EAAE,OAAO;CAC5C,WAAW,gBAAgB,SAAS,EAAE,MAAM,mBAAmB,CAAC;CAChE,OAAO,YAAY,SAAS,EAAE,CAAC;CAC/B,UAAU,eAAe,SAAS,EAAE,CAAC;CACrC,SAAS;CACT,QAAQ,aAAa,SAAS,EAAE,CAAC;CAClC,CAAC;;;;;;;;ACxDF,SAAgB,wBAAwB,QAAwC;AAC9E,KAAI,CAAC,OAAO,UAAU,WAAY,QAAO;CAEzC,MAAM,UAAU,OAAO,UAAU,YAAY,QAAQ,QAAQ,OAAO,UAAU;CAE9E,MAAM,eAAe,OAAO,UAAU,aAAa,SAAS,YAAY,GACpE,OAAO,UAAU,eACjB,CAAC,GAAG,OAAO,UAAU,cAAc,YAAY;AAEnD,QAAO;EACL,GAAG;EACH,WAAW;GACT,GAAG,OAAO;GACV;GACA;GACD;EACF;;;;;;;;;;;;;;ACbH,SAAgB,aACd,MACA,WACG;AACH,QAAO,UAAU,MAAM,UAAU;;AAGnC,SAAS,cAAc,OAAkD;AACvE,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM;;AAG7E,SAAS,UACP,MACA,WACyB;CACzB,MAAM,SAAkC,EAAE,GAAG,MAAM;AAEnD,MAAK,MAAM,OAAO,OAAO,KAAK,UAAU,EAAE;EACxC,MAAM,gBAAgB,UAAU;EAChC,MAAM,YAAY,KAAK;AAEvB,MAAI,kBAAkB,OAAW;AAEjC,MAAI,MAAM,QAAQ,cAAc,CAC9B,QAAO,OAAO;WACL,cAAc,cAAc,IAAI,cAAc,UAAU,CACjE,QAAO,OAAO,UAAU,WAAW,cAAc;MAEjD,QAAO,OAAO;;AAIlB,QAAO;;;;;ACnCT,eAAsB,gBAAgB,KAAuC;CAC3E,MAAM,EAAE,QAAQ,eAAe,MAAM,WAA4B;EAC/D,MAAM;EACN;EACD,CAAC;AAEF,KAAI,CAAC,cAAc,OAAO,KAAK,WAAW,CAAC,WAAW,EACpD,OAAM,IAAI,MAAM,+EAA+E;CAGjG,MAAM,EAAE,QAAQ,oBAAoB,MAAM,WAAqC;EAC7E,MAAM;EACN;EACD,CAAC;CAEF,MAAM,SACJ,mBAAmB,OAAO,KAAK,gBAAgB,CAAC,SAAS,IACrD,aAAa,YAAY,gBAAmC,GAC5D;CAEN,MAAM,SAAS,sBAAsB,UAAU,OAAO;AACtD,KAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OACzB,KAAK,UAAU,KAAK,MAAM,KAAK,KAAK,IAAI,CAAC,IAAI,MAAM,UAAU,CAC7D,KAAK,KAAK;EACb,MAAM,SACJ,mBAAmB,OAAO,KAAK,gBAAgB,CAAC,SAAS,IAAI,+BAA+B;AAC9F,QAAM,IAAI,MAAM,kCAAkC,OAAO,KAAK,SAAS;;AAGzE,QAAO,wBAAwB,OAAO,KAAK"}
@@ -70,8 +70,9 @@ RUN echo '<%= user %> ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
70
70
  RUN printf 'eval "$(direnv hook bash)" || true\n' >> /etc/bash.bashrc \
71
71
  && printf '[ "$PWD" = "/workspace" ] && eval "$(direnv export bash 2>&1)" 2>/dev/null || true\n' >> /etc/bash.bashrc
72
72
 
73
- # Generate Ralph loop runner that pins the same CLI version as the host
74
- RUN printf '#!/bin/bash\nexec npx --yes fabis-ralph-loop@<%= packageVersion %> run "$@"\n' \
73
+ # Pre-install Ralph loop CLI so it's available immediately (no npx download on first run)
74
+ RUN npm install -g fabis-ralph-loop@<%= packageVersion %>
75
+ RUN printf '#!/bin/bash\nexec fabis-ralph-loop run "$@"\n' \
75
76
  > /usr/local/bin/run-fabis-ralph-loop && chmod +x /usr/local/bin/run-fabis-ralph-loop
76
77
 
77
78
  WORKDIR /workspace
@@ -225,6 +225,30 @@ Controls how Ralph generates skill files for the AI agent.
225
225
 
226
226
  ---
227
227
 
228
+ ## Overrides File
229
+
230
+ For environment-specific settings that shouldn't be committed, users can create `fabis-ralph-loop.overrides.config.ts`:
231
+
232
+ ```typescript
233
+ import { defineOverridesConfig } from 'fabis-ralph-loop'
234
+
235
+ export default defineOverridesConfig({
236
+ // Only specify what you want to override
237
+ defaults: { model: 'opus', verbose: true },
238
+ container: { env: { DEBUG: 'true' } },
239
+ })
240
+ ```
241
+
242
+ **When to suggest overrides vs base config:**
243
+
244
+ - Local-only settings (debug flags, developer preferences) → overrides file
245
+ - Environment-specific values (API keys, local URLs) → overrides file
246
+ - Project-wide settings shared by the team → base config
247
+
248
+ The overrides file is gitignored by default. It is deep-merged: objects merge recursively, arrays are replaced entirely, scalars are overwritten.
249
+
250
+ ---
251
+
228
252
  ## Common Tasks
229
253
 
230
254
  ### Adding a new backpressure command
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "fabis-ralph-loop",
3
- "version": "0.1.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI for setting up and running Claude Ralph autonomous coding loops in Docker containers",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/fabis94/fabis-ralph-loop.git"
8
+ },
5
9
  "type": "module",
6
10
  "types": "./dist/index.d.mts",
7
11
  "exports": {
@@ -29,7 +33,7 @@
29
33
  "format": "prettier --write .",
30
34
  "format:check": "prettier --check .",
31
35
  "unused:check": "knip",
32
- "check": "eslint . && prettier --check . && tsc --noEmit && knip",
36
+ "check": "eslint . && tsc --noEmit && knip",
33
37
  "prepare": "husky",
34
38
  "frl": "pnpm dev"
35
39
  },
@@ -70,5 +74,6 @@
70
74
  "engines": {
71
75
  "node": ">=22.0.0"
72
76
  },
73
- "license": "MIT"
77
+ "license": "MIT",
78
+ "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
74
79
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"generators.mjs","names":[],"sources":["../src/utils/template.ts","../src/utils/version.ts","../src/generators/dockerfile.ts","../src/generators/compose.ts","../src/generators/entrypoint.ts","../src/generators/prompt.ts","../src/generators/skills.ts","../src/generators/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport ejs from 'ejs'\n\n/**\n * Resolve a bundled asset directory (templates, static, uac-templates).\n * In dist (flat layout): assets are siblings of the compiled files.\n * In src (nested layout via tsx): assets are siblings of the parent dir.\n */\nexport function resolveAssetDir(assetName: string, metaUrl: string): string {\n const dir = dirname(fileURLToPath(metaUrl))\n const sibling = join(dir, assetName)\n if (existsSync(sibling)) return sibling\n return join(dir, '..', assetName)\n}\n\nconst TEMPLATES_DIR = resolveAssetDir('templates', import.meta.url)\n\nexport async function renderTemplate(\n templateName: string,\n data: Record<string, unknown>,\n): Promise<string> {\n const templatePath = join(TEMPLATES_DIR, templateName)\n const template = await readFile(templatePath, 'utf8')\n return ejs.render(template, data, { async: false }) as string\n}\n\nexport const GENERATED_HEADER = `# Generated by fabis-ralph-loop — DO NOT EDIT MANUALLY\n# Regenerate with: npx fabis-ralph-loop generate\n`\n","import { createRequire } from 'node:module'\n\nexport function getPackageVersion(): string {\n try {\n const require = createRequire(import.meta.url)\n const pkg = require('../../package.json') as { version: string }\n return pkg.version\n } catch {\n return 'latest'\n }\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport { getPackageVersion } from '../utils/version.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Detect whether the base image already includes Node.js.\n */\nfunction isNodeBaseImage(baseImage: string): boolean {\n return /^node[:/]/i.test(baseImage)\n}\n\nexport async function generateDockerfile(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('Dockerfile.ejs', {\n generatedHeader: GENERATED_HEADER,\n baseImage: config.container.baseImage,\n systemPackages: config.container.systemPackages,\n installNode: !isNodeBaseImage(config.container.baseImage),\n playwright: config.container.playwright,\n hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateCompose(config: ResolvedConfig): Promise<string> {\n const homeDir = `/home/${config.container.user}`\n\n // Ensure .claude config is always persisted with the correct home dir\n const persistVolumes: Record<string, string> = {\n 'ralph-claude-config': `${homeDir}/.claude`,\n ...Object.fromEntries(\n Object.entries(config.container.persistVolumes).map(([name, path]) => [\n name,\n path.replace('/home/sandbox', homeDir),\n ]),\n ),\n }\n\n return renderTemplate('docker-compose.yml.ejs', {\n generatedHeader: GENERATED_HEADER,\n containerName: config.container.name,\n shmSize: config.container.shmSize,\n networkMode: config.container.networkMode,\n capabilities: config.container.capabilities,\n shadowVolumes: config.container.shadowVolumes,\n persistVolumes,\n extraVolumes: config.container.volumes,\n env: config.container.env,\n homeDir,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generateEntrypoint(config: ResolvedConfig): Promise<string> {\n const user = config.container.user\n return renderTemplate('entrypoint.ts.ejs', {\n generatedHeader: GENERATED_HEADER.replace(/^# /gm, '// '),\n agent: config.defaults.agent,\n shadowVolumes: config.container.shadowVolumes,\n entrypointSetup: config.container.hooks.entrypointSetup,\n user,\n homeDir: `/home/${user}`,\n })\n}\n","import { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nexport async function generatePrompt(config: ResolvedConfig): Promise<string> {\n return renderTemplate('ralph-prompt.md.ejs', {\n generatedHeader: GENERATED_HEADER,\n projectName: config.project.name,\n projectDescription: config.project.description,\n projectContext: config.project.context,\n backpressureCommands: config.project.backpressureCommands,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n completionSignal: config.defaults.completionSignal,\n })\n}\n","import { readdir, readFile, writeFile, mkdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { ResolvedConfig } from '../config/schema.js'\nimport { resolveAssetDir } from '../utils/template.js'\n\nconst UAC_TEMPLATES_DIR = resolveAssetDir('uac-templates', import.meta.url)\n\nfunction buildLevel1Variables(config: ResolvedConfig): Record<string, unknown> {\n return {\n backpressureCommands: config.project.backpressureCommands,\n projectName: config.project.name,\n projectContext: config.project.context,\n openAppSkill: config.project.openAppSkill,\n playwright: config.container.playwright,\n config,\n }\n}\n\nasync function discoverSkills(): Promise<string[]> {\n const skillsDir = join(UAC_TEMPLATES_DIR, 'skills')\n const entries = await readdir(skillsDir, { withFileTypes: true })\n return entries.filter((e) => e.isDirectory()).map((e) => e.name)\n}\n\nexport async function generateSkills(config: ResolvedConfig, projectRoot: string): Promise<void> {\n if (config.output.mode === 'direct') {\n await generateDirect(config, projectRoot)\n } else {\n await generateUac(config, projectRoot)\n }\n}\n\nasync function generateDirect(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n\n // Render Level 1 EJS and write to a temp dir structured as UAC templates\n const tempDir = join(tmpdir(), `ralph-skills-${Date.now()}`)\n\n try {\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n const rendered = ejs.render(template, variables) as string\n\n const outDir = join(tempDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n }\n\n // Use UAC's generate() API for the second pass (handles Level 2 EJS + frontmatter mapping)\n const files = await generate({\n root: projectRoot,\n targets: ['claude'],\n types: ['skills'],\n overrides: { templatesDir: tempDir },\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\n } finally {\n await rm(tempDir, { recursive: true, force: true })\n }\n}\n\nasync function generateUac(config: ResolvedConfig, projectRoot: string): Promise<void> {\n const variables = buildLevel1Variables(config)\n const skills = await discoverSkills()\n let count = 0\n\n for (const skill of skills) {\n const templatePath = join(UAC_TEMPLATES_DIR, 'skills', skill, 'SKILL.md')\n const template = await readFile(templatePath, 'utf8')\n // Render Level 1 EJS — Level 2 <%% %> becomes <% %> in output\n const rendered = ejs.render(template, variables) as string\n\n const outDir = join(projectRoot, config.output.uacTemplatesDir, 'skills', skill)\n await mkdir(outDir, { recursive: true })\n await writeFile(join(outDir, 'SKILL.md'), rendered, 'utf8')\n count++\n }\n\n consola.info(`Generated ${count} skill template(s) to ${config.output.uacTemplatesDir}/skills/`)\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\nimport { generateDockerfile } from './dockerfile.js'\nimport { generateCompose } from './compose.js'\nimport { generateEntrypoint } from './entrypoint.js'\nimport { generatePrompt } from './prompt.js'\nimport { generateSkills } from './skills.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface GenerateOptions {\n dryRun?: boolean\n only?: 'container' | 'prompt' | 'skills'\n}\n\ninterface GeneratedFile {\n path: string\n content: string\n}\n\nexport async function generateAll(\n config: ResolvedConfig,\n projectRoot: string,\n options: GenerateOptions = {},\n): Promise<GeneratedFile[]> {\n const files: GeneratedFile[] = []\n\n if (!options.only || options.only === 'container') {\n const containerDir = join(projectRoot, '.ralph-container')\n await mkdir(containerDir, { recursive: true })\n\n const dockerfile = await generateDockerfile(config)\n files.push({ path: join('.ralph-container', 'Dockerfile'), content: dockerfile })\n\n const entrypoint = await generateEntrypoint(config)\n files.push({ path: join('.ralph-container', 'entrypoint.ts'), content: entrypoint })\n\n const compose = await generateCompose(config)\n files.push({ path: join('.ralph-container', 'docker-compose.yml'), content: compose })\n }\n\n if (!options.only || options.only === 'prompt') {\n const prompt = await generatePrompt(config)\n files.push({ path: join('.ralph-container', 'ralph-prompt.md'), content: prompt })\n }\n\n if (options.dryRun) {\n for (const file of files) {\n consola.info(`[dry-run] Would write: ${file.path}`)\n }\n } else {\n for (const file of files) {\n const fullPath = join(projectRoot, file.path)\n await mkdir(join(fullPath, '..'), { recursive: true })\n await writeFile(fullPath, file.content, 'utf8')\n consola.success(`Written: ${file.path}`)\n }\n }\n\n if (!options.only || options.only === 'skills') {\n if (options.dryRun) {\n consola.info('[dry-run] Would generate skills')\n } else {\n await generateSkills(config, projectRoot)\n }\n }\n\n return files\n}\n"],"mappings":"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"}