fabis-ralph-loop 1.4.2 → 1.5.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/README.md CHANGED
@@ -78,7 +78,8 @@ export default defineConfig({
78
78
  },
79
79
  container: {
80
80
  name: 'my-project-ralph',
81
- playwright: true, // auto-configures Playwright MCP + headless Chromium
81
+ playwright: true, // auto-configures Playwright CLI + headless Chromium (or 'mcp' for MCP mode)
82
+ sslCerts: '.certs', // trust custom SSL certs in container (for local HTTPS dev servers)
82
83
  systemPackages: ['ripgrep'],
83
84
  env: { NODE_ENV: 'development' },
84
85
  hooks: {
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
4
  import { consola } from "consola";
5
5
  import { existsSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -57,6 +57,7 @@ async function generateDockerfile(config) {
57
57
  systemPackages: config.container.systemPackages,
58
58
  installNode: !isNodeBaseImage(config.container.baseImage),
59
59
  playwright: config.container.playwright,
60
+ sslCerts: !!config.container.sslCerts,
60
61
  hooks: config.container.hooks,
61
62
  user,
62
63
  createUser: user === "sandbox",
@@ -67,12 +68,20 @@ async function generateDockerfile(config) {
67
68
 
68
69
  //#endregion
69
70
  //#region src/generators/compose.ts
71
+ /**
72
+ * Resolve a host path for use in docker-compose volumes.
73
+ * Relative paths get `../` prepended since compose runs from `.ralph-container/`.
74
+ */
75
+ function resolveHostPath(hostPath) {
76
+ return isAbsolute(hostPath) ? hostPath : `../${hostPath}`;
77
+ }
70
78
  async function generateCompose(config) {
71
79
  const homeDir = `/home/${config.container.user}`;
72
80
  const persistVolumes = {
73
81
  "ralph-claude-config": `${homeDir}/.claude`,
74
82
  ...Object.fromEntries(Object.entries(config.container.persistVolumes).map(([name, path]) => [name, path.replace("/home/sandbox", homeDir)]))
75
83
  };
84
+ const sslCertsVolume = config.container.sslCerts ? `${resolveHostPath(config.container.sslCerts)}:/tmp/ssl-certs:ro` : void 0;
76
85
  return renderTemplate("docker-compose.yml.ejs", {
77
86
  generatedHeader: GENERATED_HEADER,
78
87
  containerName: config.container.name,
@@ -82,6 +91,7 @@ async function generateCompose(config) {
82
91
  shadowVolumes: config.container.shadowVolumes,
83
92
  persistVolumes,
84
93
  extraVolumes: config.container.volumes,
94
+ sslCertsVolume,
85
95
  env: config.container.env,
86
96
  homeDir
87
97
  });
@@ -96,6 +106,8 @@ async function generateEntrypoint(config) {
96
106
  agent: config.defaults.agent,
97
107
  shadowVolumes: config.container.shadowVolumes,
98
108
  entrypointSetup: config.container.hooks.entrypointSetup,
109
+ sslCerts: !!config.container.sslCerts,
110
+ playwright: config.container.playwright,
99
111
  user,
100
112
  homeDir: `/home/${user}`
101
113
  });
@@ -1 +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, escape: (s: string) => s }) 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 } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { InMemoryTemplate } 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 into in-memory templates for UAC's second pass\n const templates: InMemoryTemplate[] = await Promise.all(\n skills.map(async (skill) => {\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, { escape: (s: string) => s }) as string\n return { name: skill, type: 'skills' as const, content: rendered }\n }),\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 templates,\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\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, { escape: (s: string) => s }) 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":"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"}
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, escape: (s: string) => s }) 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 sslCerts: !!config.container.sslCerts,\n hooks: config.container.hooks,\n user,\n createUser: user === 'sandbox',\n homeDir: `/home/${user}`,\n packageVersion: getPackageVersion(),\n })\n}\n","import { isAbsolute } from 'node:path'\nimport { renderTemplate, GENERATED_HEADER } from '../utils/template.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\n/**\n * Resolve a host path for use in docker-compose volumes.\n * Relative paths get `../` prepended since compose runs from `.ralph-container/`.\n */\nfunction resolveHostPath(hostPath: string): string {\n return isAbsolute(hostPath) ? hostPath : `../${hostPath}`\n}\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 const sslCertsVolume = config.container.sslCerts\n ? `${resolveHostPath(config.container.sslCerts)}:/tmp/ssl-certs:ro`\n : undefined\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 sslCertsVolume,\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 sslCerts: !!config.container.sslCerts,\n playwright: config.container.playwright,\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 } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport ejs from 'ejs'\nimport { consola } from 'consola'\nimport { generate, writeGeneratedFiles } from 'universal-ai-config'\nimport type { InMemoryTemplate } 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 into in-memory templates for UAC's second pass\n const templates: InMemoryTemplate[] = await Promise.all(\n skills.map(async (skill) => {\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, { escape: (s: string) => s }) as string\n return { name: skill, type: 'skills' as const, content: rendered }\n }),\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 templates,\n })\n\n await writeGeneratedFiles(files, projectRoot)\n consola.info(`Generated ${files.length} skill file(s)`)\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, { escape: (s: string) => s }) 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":"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"}
package/dist/index.d.mts CHANGED
@@ -11,7 +11,11 @@ declare const ralphLoopConfigSchema: z.ZodObject<{
11
11
  baseImage: z.ZodDefault<z.ZodString>;
12
12
  user: z.ZodDefault<z.ZodString>;
13
13
  systemPackages: z.ZodDefault<z.ZodArray<z.ZodString>>;
14
- playwright: z.ZodDefault<z.ZodBoolean>;
14
+ playwright: z.ZodPipe<z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodEnum<{
15
+ cli: "cli";
16
+ mcp: "mcp";
17
+ }>]>>, z.ZodTransform<false | "cli" | "mcp", boolean | "cli" | "mcp">>;
18
+ sslCerts: z.ZodOptional<z.ZodString>;
15
19
  networkMode: z.ZodDefault<z.ZodString>;
16
20
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
17
21
  shmSize: z.ZodDefault<z.ZodString>;
@@ -111,7 +115,8 @@ declare function defineConfig(config: RalphLoopConfig): {
111
115
  baseImage?: string | undefined;
112
116
  user?: string | undefined;
113
117
  systemPackages?: string[] | undefined;
114
- playwright?: boolean | undefined;
118
+ playwright?: boolean | "cli" | "mcp" | undefined;
119
+ sslCerts?: string | undefined;
115
120
  networkMode?: string | undefined;
116
121
  env?: Record<string, string> | undefined;
117
122
  shmSize?: string | undefined;
@@ -160,7 +165,8 @@ declare function defineOverridesConfig(config: DeepPartial<RalphLoopConfig>): {
160
165
  baseImage?: string | undefined;
161
166
  user?: string | undefined;
162
167
  systemPackages?: (string | undefined)[] | undefined;
163
- playwright?: boolean | undefined;
168
+ playwright?: boolean | "cli" | "mcp" | undefined;
169
+ sslCerts?: string | undefined;
164
170
  networkMode?: string | undefined;
165
171
  env?: {
166
172
  [x: string]: string | undefined;
@@ -1 +1 @@
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"}
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;;;;cA8DlB,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;;;iBCpE3B,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/loader.mjs CHANGED
@@ -16,7 +16,12 @@ const containerSchema = z.object({
16
16
  baseImage: z.string().min(1).default("node:22-bookworm"),
17
17
  user: z.string().min(1).default("sandbox"),
18
18
  systemPackages: z.array(z.string()).default([]),
19
- playwright: z.boolean().default(false),
19
+ playwright: z.union([z.boolean(), z.enum(["cli", "mcp"])]).default(false).transform((val) => {
20
+ if (val === true) return "cli";
21
+ if (val === false) return false;
22
+ return val;
23
+ }),
24
+ sslCerts: z.string().optional(),
20
25
  networkMode: z.string().default("host"),
21
26
  env: z.record(z.string(), z.string()).default({}),
22
27
  shmSize: z.string().default("64m"),
@@ -56,7 +61,7 @@ const ralphLoopConfigSchema = z.object({
56
61
  //#endregion
57
62
  //#region src/config/defaults.ts
58
63
  /**
59
- * Apply Playwright-specific defaults when playwright is enabled.
64
+ * Apply Playwright-specific defaults when playwright is enabled ('cli' or 'mcp').
60
65
  * Merges SYS_ADMIN capability and 2gb shm_size if not already set.
61
66
  */
62
67
  function applyPlaywrightDefaults(config) {
@@ -1 +1 @@
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":"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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\n .union([z.boolean(), z.enum(['cli', 'mcp'])])\n .default(false)\n .transform((val) => {\n if (val === true) return 'cli' as const\n if (val === false) return false as const\n return val\n }),\n sslCerts: z.string().optional(),\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 ('cli' or 'mcp').\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,EACT,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAC5C,QAAQ,MAAM,CACd,WAAW,QAAQ;AAClB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,QAAQ,MAAO,QAAO;AAC1B,SAAO;GACP;CACJ,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,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;;;;;;;;AChEF,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"}
@@ -37,6 +37,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
37
37
  libcairo2 \
38
38
  libasound2 \
39
39
  libxshmfence1 \
40
+ <% if (sslCerts) { -%>
41
+ libnss3-tools \
42
+ <% } -%>
40
43
  && rm -rf /var/lib/apt/lists/*
41
44
  <% } -%>
42
45
  <% for (const instruction of hooks.rootSetup) { %>
@@ -24,6 +24,9 @@ services:
24
24
  - <%= name %>:<%= path %>
25
25
  <% } -%>
26
26
  - ${HOME}/.claude/plans:<%= homeDir %>/host-plans:ro
27
+ <% if (sslCertsVolume) { -%>
28
+ - <%= sslCertsVolume %>
29
+ <% } -%>
27
30
  <% for (const v of extraVolumes) { -%>
28
31
  - <%= v %>
29
32
  <% } -%>
@@ -35,6 +35,43 @@ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
35
35
  process.exit(1)
36
36
  }
37
37
  console.log('Auth token present.')
38
+ <% if (sslCerts) { %>
39
+ // === SSL Certificate Trust ===
40
+ const sslCertsDir = '/tmp/ssl-certs'
41
+ if (existsSync(sslCertsDir)) {
42
+ try {
43
+ execSync('sudo mkdir -p /usr/local/share/ca-certificates/custom', { stdio: 'pipe' })
44
+ execSync(`
45
+ for f in ${sslCertsDir}/*.crt ${sslCertsDir}/*.pem ${sslCertsDir}/*.cer; do
46
+ [ -f "$f" ] && sudo cp "$f" "/usr/local/share/ca-certificates/custom/$(basename "\${f%.*}").crt"
47
+ done
48
+ `, { stdio: 'pipe', shell: '/bin/bash' })
49
+ execSync('sudo update-ca-certificates', { stdio: 'pipe' })
50
+ process.env.NODE_EXTRA_CA_CERTS = '/etc/ssl/certs/ca-certificates.crt'
51
+ console.log('SSL certificates trusted (system + Node.js).')
52
+ <% if (playwright) { %>
53
+ // Also trust in Chromium's NSS database
54
+ try {
55
+ const nssDb = `${process.env.HOME || '<%= homeDir %>'}/.pki/nssdb`
56
+ execSync(`mkdir -p ${nssDb}`, { stdio: 'pipe' })
57
+ execSync(`certutil -d sql:${nssDb} -N --empty-password 2>/dev/null || true`, { stdio: 'pipe', shell: '/bin/bash' })
58
+ execSync(`
59
+ for f in ${sslCertsDir}/*.crt ${sslCertsDir}/*.pem ${sslCertsDir}/*.cer; do
60
+ [ -f "$f" ] && certutil -d sql:${nssDb} -A -t "CT,C,C" -n "$(basename "$f")" -i "$f" 2>/dev/null || true
61
+ done
62
+ `, { stdio: 'pipe', shell: '/bin/bash' })
63
+ console.log('SSL certificates trusted (Chromium NSS).')
64
+ } catch {
65
+ console.warn('Warning: Could not set up NSS cert database for Chromium.')
66
+ }
67
+ <% } -%>
68
+ } catch (error) {
69
+ console.warn('Warning: Could not trust SSL certificates:', error)
70
+ }
71
+ } else {
72
+ console.warn('Warning: sslCerts configured but /tmp/ssl-certs not mounted.')
73
+ }
74
+ <% } -%>
38
75
 
39
76
  // === Chown shadow volumes ===
40
77
  const shadowVolumes: string[] = <%- JSON.stringify(shadowVolumes) %>
@@ -76,7 +76,64 @@ Only add patterns that are **general and reusable**, not story-specific details.
76
76
  - Use `import type { ... }` for type-only imports (separate from value imports)
77
77
  <% if (openAppSkill || playwright) { %>
78
78
  ## Browser Testing
79
- <% if (playwright) { %>
79
+ <% if (playwright === 'cli') { %>
80
+ This container has `@playwright/cli` installed with headless Chromium. **Do NOT use Playwright MCP or Chrome MCP tools — use the CLI instead.**
81
+
82
+ Run commands via:
83
+
84
+ ```bash
85
+ playwright-cli <command>
86
+ ```
87
+
88
+ ### Essential Commands
89
+
90
+ | Command | Description |
91
+ |---------|-------------|
92
+ | `open [url]` | Launch browser, optionally navigate to URL |
93
+ | `goto <url>` | Navigate to a URL |
94
+ | `snapshot` | Get page structure (preferred over screenshots) |
95
+ | `screenshot [ref]` | Capture visual screenshot |
96
+ | `click <ref>` | Click an element |
97
+ | `fill <ref> <text>` | Fill a text field |
98
+ | `type <text>` | Type into focused element |
99
+ | `hover <ref>` | Hover over element |
100
+ | `select <ref> <val>` | Choose dropdown option |
101
+ | `press <key>` | Press a keyboard key |
102
+ | `eval <func> [ref]` | Execute JavaScript on the page |
103
+ | `console [level]` | Display console messages |
104
+ | `network` | List HTTP requests since page load |
105
+ | `tab-list` | List open tabs |
106
+ | `tab-new [url]` | Open new tab |
107
+ | `tab-select <index>` | Switch tab |
108
+ | `close` | Close the page |
109
+
110
+ ### Session Management
111
+
112
+ Use named sessions to keep browser state between commands:
113
+
114
+ ```bash
115
+ playwright-cli -s=mytest open http://localhost:3000
116
+ playwright-cli -s=mytest snapshot
117
+ playwright-cli -s=mytest click <ref>
118
+ playwright-cli -s=mytest close
119
+ ```
120
+
121
+ ### Interaction Pattern
122
+
123
+ 1. `snapshot` — understand page structure, get element refs
124
+ 2. Interact — `click`, `fill`, `type` using refs from snapshot
125
+ 3. `snapshot` or `screenshot` — verify result
126
+ 4. `console error` — check for errors
127
+
128
+ For any story that changes UI:
129
+
130
+ 1. Ensure the dev servers are running before testing
131
+ 2. Use `playwright-cli` to navigate and verify the changes
132
+ 3. Capture a screenshot if helpful for the progress log
133
+ <% if (openAppSkill) { %>
134
+ Read `<%= openAppSkill %>` for the full app launch and authentication procedure.
135
+ <% } -%>
136
+ <% } else if (playwright === 'mcp') { %>
80
137
  This container has **Playwright MCP** configured with headless Chromium. For any story that changes UI:
81
138
 
82
139
  1. Ensure the dev servers are running before testing
@@ -92,13 +92,25 @@ Each story should be small enough to implement in one focused session.
92
92
  - [ ] Specific verifiable criterion
93
93
  - [ ] Another criterion
94
94
  - [ ] Typecheck passes
95
+ <% if (playwright === 'cli') { -%>
96
+ - [ ] **[UI stories only]** Verify in browser using Playwright CLI
97
+ <% } else if (playwright === 'mcp') { -%>
95
98
  - [ ] **[UI stories only]** Verify in browser using Playwright MCP tools
99
+ <% } else if (playwright) { -%>
100
+ - [ ] **[UI stories only]** Verify in browser
101
+ <% } -%>
96
102
  ```
97
103
 
98
104
  **Important:**
99
105
 
100
106
  - Acceptance criteria must be verifiable, not vague. "Works correctly" is bad. "Button shows confirmation dialog before deleting" is good.
107
+ <% if (playwright === 'cli') { -%>
108
+ - **For any story with UI changes:** Always include "Verify in browser using Playwright CLI" as acceptance criteria.
109
+ <% } else if (playwright === 'mcp') { -%>
101
110
  - **For any story with UI changes:** Always include "Verify in browser using Playwright MCP tools" as acceptance criteria.
111
+ <% } else if (playwright) { -%>
112
+ - **For any story with UI changes:** Always include "Verify in browser" as acceptance criteria.
113
+ <% } -%>
102
114
 
103
115
  ### 4. Functional Requirements
104
116
 
@@ -113,13 +113,24 @@ For stories with testable logic, also include:
113
113
  "Tests pass"
114
114
  ```
115
115
 
116
+ <% if (playwright) { %>
117
+
116
118
  ### For stories that change UI, also include:
117
119
 
118
120
  ```
121
+ <% if (playwright === 'cli') { -%>
122
+ "Verify in browser using Playwright CLI"
123
+ <% } else { -%>
119
124
  "Verify in browser using Playwright MCP tools"
125
+ <% } -%>
120
126
  ```
121
127
 
128
+ <% if (playwright === 'cli') { -%>
129
+ Frontend stories are NOT complete until visually verified. Ralph will use `playwright-cli` to navigate to the page, take snapshots/screenshots, and confirm changes work.
130
+ <% } else { -%>
122
131
  Frontend stories are NOT complete until visually verified. Ralph will use Playwright MCP tools to navigate to the page, interact with the UI, and confirm changes work.
132
+ <% } -%>
133
+ <% } %>
123
134
 
124
135
  ---
125
136
 
@@ -200,8 +211,9 @@ Add ability to mark tasks with different statuses.
200
211
  "acceptanceCriteria": [
201
212
  "Each task card shows colored status badge",
202
213
  "Badge colors: gray=pending, blue=in_progress, green=done",
203
- "Typecheck passes",
204
- "Verify in browser using Playwright MCP tools"
214
+ "Typecheck passes"<% if (playwright === 'cli') { %>,
215
+ "Verify in browser using Playwright CLI"<% } else if (playwright === 'mcp') { %>,
216
+ "Verify in browser using Playwright MCP tools"<% } %>
205
217
  ],
206
218
  "priority": 2,
207
219
  "passes": false,
@@ -234,6 +246,10 @@ Before writing `.ralph/prd.json`, verify:
234
246
  - [ ] Each story is completable in one iteration (small enough)
235
247
  - [ ] Stories are ordered by dependency (schema to backend to UI)
236
248
  - [ ] Every story has "Typecheck passes" as criterion
249
+ <% if (playwright === 'cli') { -%>
250
+ - [ ] UI stories have "Verify in browser using Playwright CLI" as criterion
251
+ <% } else if (playwright === 'mcp') { -%>
237
252
  - [ ] UI stories have "Verify in browser using Playwright MCP tools" as criterion
253
+ <% } -%>
238
254
  - [ ] Acceptance criteria are verifiable (not vague)
239
255
  - [ ] No story depends on a later story
@@ -88,21 +88,22 @@ backpressureCommands: [
88
88
 
89
89
  Docker container configuration. Controls the environment Ralph runs in.
90
90
 
91
- | Key | Type | Default | Purpose |
92
- | ---------------- | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
93
- | `name` | `string` | `'ralph-container'` | Docker container name. Change if running multiple Ralph instances |
94
- | `baseImage` | `string` | `'node:22-bookworm'` | Base Docker image. Use a different Node version or distro if needed |
95
- | `user` | `string` | `'sandbox'` | Container user. Default creates `sandbox` (UID 1000). Set to existing user (e.g. `'node'`) to reuse it |
96
- | `systemPackages` | `string[]` | `[]` | APT packages to install (e.g., `['postgresql-client', 'redis-tools']`) |
97
- | `playwright` | `boolean` | `false` | Enable Playwright browser testing. Auto-adds `SYS_ADMIN` capability and sets `shmSize` to `'2gb'` |
98
- | `networkMode` | `string` | `'host'` | Docker network mode. Use `'bridge'` if host networking causes conflicts |
99
- | `env` | `Record<string, string>` | `{}` | Environment variables injected into the container |
100
- | `shmSize` | `string` | `'64m'` | Shared memory size. Auto-upgraded to `'2gb'` when `playwright: true` |
101
- | `capabilities` | `string[]` | `[]` | Docker capabilities (e.g., `['SYS_ADMIN']`). Auto-added when `playwright: true` |
102
- | `volumes` | `string[]` | `[]` | Additional Docker volume mounts (standard Docker `-v` syntax) |
103
- | `shadowVolumes` | `string[]` | `[]` | Paths to exclude from the project mount using anonymous volumes (see below) |
104
- | `persistVolumes` | `Record<string, string>` | `{ 'ralph-claude-config': '/home/sandbox/.claude' }` | Named volumes that persist across container restarts. Key = volume name, value = container path |
105
- | `hooks` | `ContainerHooks` | `{}` | Dockerfile build hooks (see below) |
91
+ | Key | Type | Default | Purpose |
92
+ | ---------------- | --------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `name` | `string` | `'ralph-container'` | Docker container name. Change if running multiple Ralph instances |
94
+ | `baseImage` | `string` | `'node:22-bookworm'` | Base Docker image. Use a different Node version or distro if needed |
95
+ | `user` | `string` | `'sandbox'` | Container user. Default creates `sandbox` (UID 1000). Set to existing user (e.g. `'node'`) to reuse it |
96
+ | `systemPackages` | `string[]` | `[]` | APT packages to install (e.g., `['postgresql-client', 'redis-tools']`) |
97
+ | `playwright` | `boolean \| 'cli' \| 'mcp'` | `false` | Enable Playwright browser testing. `true` or `'cli'` uses @playwright/cli (preferred). `'mcp'` uses Playwright MCP server. Auto-adds `SYS_ADMIN` capability and sets `shmSize` to `'2gb'` |
98
+ | `sslCerts` | `string` | `undefined` | Host path to SSL certificate directory. Certs are trusted system-wide in the container. When combined with `playwright`, also adds certs to Chromium's NSS store |
99
+ | `networkMode` | `string` | `'host'` | Docker network mode. Use `'bridge'` if host networking causes conflicts |
100
+ | `env` | `Record<string, string>` | `{}` | Environment variables injected into the container |
101
+ | `shmSize` | `string` | `'64m'` | Shared memory size. Auto-upgraded to `'2gb'` when `playwright` is enabled |
102
+ | `capabilities` | `string[]` | `[]` | Docker capabilities (e.g., `['SYS_ADMIN']`). Auto-added when `playwright` is enabled |
103
+ | `volumes` | `string[]` | `[]` | Additional Docker volume mounts (standard Docker `-v` syntax) |
104
+ | `shadowVolumes` | `string[]` | `[]` | Paths to exclude from the project mount using anonymous volumes (see below) |
105
+ | `persistVolumes` | `Record<string, string>` | `{ 'ralph-claude-config': '/home/sandbox/.claude' }` | Named volumes that persist across container restarts. Key = volume name, value = container path |
106
+ | `hooks` | `ContainerHooks` | `{}` | Dockerfile build hooks (see below) |
106
107
 
107
108
  #### Shadow Volumes
108
109
 
@@ -161,7 +162,8 @@ container: {
161
162
  **When to update `container`:**
162
163
 
163
164
  - The project needs system-level dependencies → `systemPackages`
164
- - The project uses Playwright or browser-based tests → `playwright: true`
165
+ - The project uses Playwright or browser-based tests → `playwright: true` (CLI mode, preferred) or `playwright: 'mcp'`
166
+ - The project uses self-signed SSL certificates (e.g., local HTTPS dev server) → `sslCerts` (path to cert directory)
165
167
  - Need API keys or secrets in the container → `env` (prefer secrets management over hardcoding)
166
168
  - Running multiple Ralph instances side-by-side → change `name`
167
169
  - Need to persist additional directories across runs → `persistVolumes`
@@ -266,7 +268,7 @@ backpressureCommands: [
266
268
 
267
269
  ```typescript
268
270
  container: {
269
- playwright: true,
271
+ playwright: true, // CLI mode (default, preferred) — or 'mcp' for Playwright MCP
270
272
  }
271
273
  ```
272
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabis-ralph-loop",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for setting up and running Claude Ralph autonomous coding loops in Docker containers",
5
5
  "repository": {
6
6
  "type": "git",