ralphflow 0.5.2 → 0.5.3
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/dist/{chunk-DOC64TD6.js → chunk-CA4XP6KI.js} +1 -1
- package/dist/ralphflow.js +132 -18
- package/dist/{server-EX5MWYW4.js → server-64NQCIKJ.js} +88 -21
- package/package.json +1 -1
- package/src/dashboard/ui/app.js +4 -1
- package/src/dashboard/ui/archives.js +27 -2
- package/src/dashboard/ui/index.html +1 -1
- package/src/dashboard/ui/loop-detail.js +1 -1
- package/src/dashboard/ui/sidebar.js +1 -1
- package/src/dashboard/ui/state.js +3 -0
- package/src/dashboard/ui/styles.css +56 -0
- package/src/dashboard/ui/utils.js +30 -0
- package/src/templates/code-review/loops/00-collect-loop/changesets.md +3 -0
- package/src/templates/code-review/loops/00-collect-loop/prompt.md +179 -0
- package/src/templates/code-review/loops/00-collect-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/01-spec-review-loop/prompt.md +238 -0
- package/src/templates/code-review/loops/01-spec-review-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/02-quality-review-loop/issues.md +3 -0
- package/src/templates/code-review/loops/02-quality-review-loop/prompt.md +306 -0
- package/src/templates/code-review/loops/02-quality-review-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/03-fix-loop/prompt.md +265 -0
- package/src/templates/code-review/loops/03-fix-loop/tracker.md +16 -0
- package/src/templates/code-review/ralphflow.yaml +98 -0
- package/src/templates/design-review/loops/00-explore-loop/ideas.md +3 -0
- package/src/templates/design-review/loops/00-explore-loop/prompt.md +207 -0
- package/src/templates/design-review/loops/00-explore-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/01-design-loop/designs.md +3 -0
- package/src/templates/design-review/loops/01-design-loop/prompt.md +201 -0
- package/src/templates/design-review/loops/01-design-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/02-review-loop/prompt.md +255 -0
- package/src/templates/design-review/loops/02-review-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/03-plan-loop/plans.md +3 -0
- package/src/templates/design-review/loops/03-plan-loop/prompt.md +247 -0
- package/src/templates/design-review/loops/03-plan-loop/tracker.md +16 -0
- package/src/templates/design-review/ralphflow.yaml +84 -0
- package/src/templates/systematic-debugging/loops/00-investigate-loop/bugs.md +3 -0
- package/src/templates/systematic-debugging/loops/00-investigate-loop/prompt.md +237 -0
- package/src/templates/systematic-debugging/loops/00-investigate-loop/tracker.md +16 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/hypotheses.md +3 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/prompt.md +312 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/tracker.md +18 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/fixes.md +3 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/prompt.md +342 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/tracker.md +18 -0
- package/src/templates/systematic-debugging/ralphflow.yaml +81 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/prompt.md +208 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/specs.md +3 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/tracker.md +16 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/prompt.md +323 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/test-cases.md +3 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/tracker.md +18 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/prompt.md +226 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/tracker.md +16 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/verifications.md +3 -0
- package/src/templates/tdd-implementation/ralphflow.yaml +73 -0
|
@@ -4,7 +4,7 @@ import { join, dirname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { stringify as stringifyYaml } from "yaml";
|
|
6
6
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
var BUILT_IN_TEMPLATES = ["code-implementation", "research"];
|
|
7
|
+
var BUILT_IN_TEMPLATES = ["code-implementation", "research", "tdd-implementation", "systematic-debugging", "design-review", "code-review"];
|
|
8
8
|
function resolveTemplatePath(templateName) {
|
|
9
9
|
const candidates = [
|
|
10
10
|
join(__dirname, "..", "templates", templateName),
|
package/dist/ralphflow.js
CHANGED
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
resolveFlowDir,
|
|
13
13
|
resolveLoop,
|
|
14
14
|
showStatus
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-CA4XP6KI.js";
|
|
16
16
|
|
|
17
17
|
// src/cli/index.ts
|
|
18
|
-
import { Command as
|
|
19
|
-
import
|
|
18
|
+
import { Command as Command8 } from "commander";
|
|
19
|
+
import chalk9 from "chalk";
|
|
20
20
|
import { exec } from "child_process";
|
|
21
21
|
|
|
22
22
|
// src/cli/init.ts
|
|
@@ -29,9 +29,9 @@ import { join } from "path";
|
|
|
29
29
|
import { createInterface } from "readline";
|
|
30
30
|
import chalk from "chalk";
|
|
31
31
|
function ask(rl, question) {
|
|
32
|
-
return new Promise((
|
|
32
|
+
return new Promise((resolve3) => {
|
|
33
33
|
rl.question(chalk.cyan("? ") + question + " ", (answer) => {
|
|
34
|
-
|
|
34
|
+
resolve3(answer.trim());
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -100,7 +100,7 @@ function listFlows(ralphFlowDir) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// src/cli/init.ts
|
|
103
|
-
var initCommand = new Command("init").description("Initialize a new RalphFlow flow").option("-t, --template <name>", "Template to use (code-implementation, research)").option("-n, --name <name>", "Custom name for the flow").action(async (opts) => {
|
|
103
|
+
var initCommand = new Command("init").description("Initialize a new RalphFlow flow").option("-t, --template <name>", "Template to use (code-implementation, research, tdd-implementation, systematic-debugging, design-review, code-review)").option("-n, --name <name>", "Custom name for the flow").action(async (opts) => {
|
|
104
104
|
try {
|
|
105
105
|
await initProject(process.cwd(), { template: opts.template, name: opts.name });
|
|
106
106
|
} catch (err) {
|
|
@@ -136,7 +136,7 @@ async function spawnClaude(options) {
|
|
|
136
136
|
if (model) {
|
|
137
137
|
args.unshift("--model", model);
|
|
138
138
|
}
|
|
139
|
-
return new Promise((
|
|
139
|
+
return new Promise((resolve3, reject) => {
|
|
140
140
|
const child = spawn("claude", args, {
|
|
141
141
|
cwd,
|
|
142
142
|
stdio: "inherit",
|
|
@@ -146,7 +146,7 @@ async function spawnClaude(options) {
|
|
|
146
146
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
147
147
|
});
|
|
148
148
|
child.on("close", (code, signal) => {
|
|
149
|
-
|
|
149
|
+
resolve3({
|
|
150
150
|
output: "",
|
|
151
151
|
exitCode: code,
|
|
152
152
|
signal
|
|
@@ -524,7 +524,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
|
|
|
524
524
|
try {
|
|
525
525
|
let dashboardHandle;
|
|
526
526
|
if (opts.ui) {
|
|
527
|
-
const { startDashboard } = await import("./server-
|
|
527
|
+
const { startDashboard } = await import("./server-64NQCIKJ.js");
|
|
528
528
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
529
529
|
}
|
|
530
530
|
await runLoop(loop, {
|
|
@@ -550,7 +550,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
|
|
|
550
550
|
try {
|
|
551
551
|
let dashboardHandle;
|
|
552
552
|
if (opts.ui) {
|
|
553
|
-
const { startDashboard } = await import("./server-
|
|
553
|
+
const { startDashboard } = await import("./server-64NQCIKJ.js");
|
|
554
554
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
555
555
|
}
|
|
556
556
|
await runE2E({
|
|
@@ -587,7 +587,7 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
|
|
|
587
587
|
// src/cli/dashboard.ts
|
|
588
588
|
import { Command as Command5 } from "commander";
|
|
589
589
|
var dashboardCommand = new Command5("dashboard").alias("ui").description("Start the web dashboard").option("-p, --port <port>", "Port number", "4242").action(async (opts) => {
|
|
590
|
-
const { startDashboard } = await import("./server-
|
|
590
|
+
const { startDashboard } = await import("./server-64NQCIKJ.js");
|
|
591
591
|
await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
|
|
592
592
|
});
|
|
593
593
|
|
|
@@ -1094,28 +1094,142 @@ var createTemplateCommand = new Command6("create-template").description("Create
|
|
|
1094
1094
|
}
|
|
1095
1095
|
});
|
|
1096
1096
|
|
|
1097
|
+
// src/cli/summarize.ts
|
|
1098
|
+
import { Command as Command7 } from "commander";
|
|
1099
|
+
import chalk8 from "chalk";
|
|
1100
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1101
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1102
|
+
var SUMMARIZE_PROMPT = `You are summarizing an archived RalphFlow workflow run.
|
|
1103
|
+
|
|
1104
|
+
## What to read
|
|
1105
|
+
|
|
1106
|
+
This directory is an archived app snapshot. It contains:
|
|
1107
|
+
- Loop subdirectories (e.g. \`00-story-loop/\`, \`01-tasks-loop/\`, \`02-delivery-loop/\`) \u2014 each has:
|
|
1108
|
+
- \`stories.md\` or \`tasks.md\` \u2014 the work items with \`## STORY-N:\` or \`## TASK-N:\` headers
|
|
1109
|
+
- \`tracker.md\` \u2014 completion state with \`- [x]\` (done) and \`- [ ]\` (incomplete) checkboxes, \`completed_tasks\`/\`completed_stories\` lists, and agent activity logs
|
|
1110
|
+
- \`ralphflow.yaml\` \u2014 pipeline configuration
|
|
1111
|
+
|
|
1112
|
+
Read ALL \`.md\` files across all subdirectories. Parse stories, task groups (\`# TASK-GROUP-N:\` headers in tasks.md), and individual tasks. Determine completion from tracker checkboxes: \`[x]\` = completed, \`[ ]\` = incomplete.
|
|
1113
|
+
|
|
1114
|
+
## What to write
|
|
1115
|
+
|
|
1116
|
+
Write a file called \`summary.md\` in the current directory with this exact structure:
|
|
1117
|
+
|
|
1118
|
+
\`\`\`
|
|
1119
|
+
# Archive Summary
|
|
1120
|
+
|
|
1121
|
+
**N stories \xB7 N task groups \xB7 N/N tasks completed \xB7 N agents**
|
|
1122
|
+
|
|
1123
|
+
## Pipeline
|
|
1124
|
+
|
|
1125
|
+
\\\`\\\`\\\`
|
|
1126
|
+
STORY-1: Title
|
|
1127
|
+
\u251C\u2500\u2500 TASK-GROUP-1: Title
|
|
1128
|
+
\u2502 \u251C\u2500\u2500 \u2713 TASK-1: Title
|
|
1129
|
+
\u2502 \u251C\u2500\u2500 \u2713 TASK-2: Title
|
|
1130
|
+
\u2502 \u2514\u2500\u2500 \u25CB TASK-3: Title
|
|
1131
|
+
\u2514\u2500\u2500 TASK-GROUP-2: Title
|
|
1132
|
+
\u2514\u2500\u2500 \u2713 TASK-4: Title
|
|
1133
|
+
|
|
1134
|
+
STORY-2: Title
|
|
1135
|
+
\u2514\u2500\u2500 TASK-GROUP-3: Title
|
|
1136
|
+
\u251C\u2500\u2500 \u2713 TASK-5: Title
|
|
1137
|
+
\u2514\u2500\u2500 \u2713 TASK-6: Title
|
|
1138
|
+
\\\`\\\`\\\`
|
|
1139
|
+
|
|
1140
|
+
## What was built
|
|
1141
|
+
|
|
1142
|
+
**STORY-1: Title** \u2014 1-2 sentence narrative of what was accomplished and why.
|
|
1143
|
+
|
|
1144
|
+
**STORY-2: Title** \u2014 1-2 sentence narrative.
|
|
1145
|
+
|
|
1146
|
+
## Key decisions
|
|
1147
|
+
|
|
1148
|
+
- Decision or trade-off noted in tracker logs or task descriptions
|
|
1149
|
+
- Another significant decision
|
|
1150
|
+
\`\`\`
|
|
1151
|
+
|
|
1152
|
+
Rules:
|
|
1153
|
+
- Use \`\u2713\` for completed tasks, \`\u25CB\` for incomplete
|
|
1154
|
+
- The ASCII tree uses box-drawing characters (\`\u251C\u2500\u2500 \u2514\u2500\u2500 \u2502\`)
|
|
1155
|
+
- Stats line counts: stories (from stories.md headers), task groups, tasks completed vs total, unique agents (from tracker agent columns)
|
|
1156
|
+
- Story narratives should capture *what was built and why* \u2014 not just list tasks
|
|
1157
|
+
- Key decisions come from tracker logs, task descriptions, or trade-offs visible in the work
|
|
1158
|
+
- Keep the whole file under 80 lines \u2014 it should fit on one screen
|
|
1159
|
+
- If no tasks.md exists (e.g. a research pipeline), adapt: use whatever entity headers exist (topics, stories, etc.)
|
|
1160
|
+
|
|
1161
|
+
After writing summary.md, you are done. Do not modify any other files.`;
|
|
1162
|
+
var summarizeCommand = new Command7("summarize").description("Generate a summary of an archived workflow run").argument("<app>", "App name (e.g., code-implementation)").argument("<archive-date>", "Archive timestamp (e.g., 2026-03-14_15-30)").option("-m, --model <model>", "Claude model to use").action(async (app, archiveDate, opts) => {
|
|
1163
|
+
if (app.includes("..") || app.includes("/") || app.includes("\\")) {
|
|
1164
|
+
console.error(chalk8.red("\n Invalid app name.\n"));
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
if (archiveDate.includes("..") || archiveDate.includes("/") || archiveDate.includes("\\")) {
|
|
1168
|
+
console.error(chalk8.red("\n Invalid archive date.\n"));
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
const archiveDir = resolve2(process.cwd(), ".ralph-flow", ".archives", app, archiveDate);
|
|
1172
|
+
if (!existsSync3(archiveDir)) {
|
|
1173
|
+
const appArchivesDir = join3(process.cwd(), ".ralph-flow", ".archives", app);
|
|
1174
|
+
if (!existsSync3(appArchivesDir)) {
|
|
1175
|
+
console.error(chalk8.red(`
|
|
1176
|
+
No archives found for app "${app}".`));
|
|
1177
|
+
console.error(chalk8.dim(` Archive an app first via the dashboard.
|
|
1178
|
+
`));
|
|
1179
|
+
} else {
|
|
1180
|
+
const { readdirSync: readdirSync3 } = await import("fs");
|
|
1181
|
+
const available = readdirSync3(appArchivesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
1182
|
+
console.error(chalk8.red(`
|
|
1183
|
+
Archive "${archiveDate}" not found for app "${app}".`));
|
|
1184
|
+
if (available.length > 0) {
|
|
1185
|
+
console.error(chalk8.dim(` Available archives:`));
|
|
1186
|
+
for (const a of available) {
|
|
1187
|
+
console.error(chalk8.dim(` ${a}`));
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
console.error("");
|
|
1191
|
+
}
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
console.log(chalk8.dim(`
|
|
1195
|
+
Summarizing archive: ${app}/${archiveDate}
|
|
1196
|
+
`));
|
|
1197
|
+
try {
|
|
1198
|
+
await spawnClaude({
|
|
1199
|
+
prompt: SUMMARIZE_PROMPT,
|
|
1200
|
+
model: opts.model,
|
|
1201
|
+
cwd: archiveDir
|
|
1202
|
+
});
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1205
|
+
console.error(chalk8.red(`
|
|
1206
|
+
${msg}
|
|
1207
|
+
`));
|
|
1208
|
+
process.exit(1);
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1097
1212
|
// src/cli/index.ts
|
|
1098
|
-
var program = new
|
|
1099
|
-
const
|
|
1100
|
-
const {
|
|
1101
|
-
await startDashboard({ cwd: process.cwd(), port });
|
|
1213
|
+
var program = new Command8().name("ralphflow").description("Multi-agent AI workflow orchestration for Claude Code").version("0.1.0").addCommand(initCommand).addCommand(runCommand).addCommand(e2eCommand).addCommand(statusCommand).addCommand(dashboardCommand).addCommand(createTemplateCommand).addCommand(summarizeCommand).action(async () => {
|
|
1214
|
+
const { startDashboard } = await import("./server-64NQCIKJ.js");
|
|
1215
|
+
const { port } = await startDashboard({ cwd: process.cwd() });
|
|
1102
1216
|
const url = `http://localhost:${port}`;
|
|
1103
1217
|
exec(`open "${url}"`, (err) => {
|
|
1104
1218
|
if (err) {
|
|
1105
|
-
console.log(
|
|
1219
|
+
console.log(chalk9.dim(` Open ${url} in your browser`));
|
|
1106
1220
|
}
|
|
1107
1221
|
});
|
|
1108
1222
|
});
|
|
1109
1223
|
process.on("SIGINT", () => {
|
|
1110
1224
|
console.log();
|
|
1111
|
-
console.log(
|
|
1225
|
+
console.log(chalk9.dim(" Interrupted."));
|
|
1112
1226
|
process.exit(130);
|
|
1113
1227
|
});
|
|
1114
1228
|
program.configureOutput({
|
|
1115
1229
|
writeErr: (str) => {
|
|
1116
1230
|
const clean = str.replace(/^error: /, "");
|
|
1117
1231
|
if (clean.trim()) {
|
|
1118
|
-
console.error(
|
|
1232
|
+
console.error(chalk9.red(` ${clean.trim()}`));
|
|
1119
1233
|
}
|
|
1120
1234
|
}
|
|
1121
1235
|
});
|
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
resolveFlowDir,
|
|
16
16
|
resolveTemplatePathWithCustom,
|
|
17
17
|
validateTemplateName
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CA4XP6KI.js";
|
|
19
19
|
|
|
20
20
|
// src/dashboard/server.ts
|
|
21
21
|
import { Hono as Hono2 } from "hono";
|
|
22
22
|
import { cors } from "hono/cors";
|
|
23
|
-
import {
|
|
23
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
24
24
|
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
25
25
|
import { join as join4, dirname, extname } from "path";
|
|
26
26
|
import { fileURLToPath } from "url";
|
|
@@ -45,13 +45,13 @@ function broadcastWs(wss, event) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
function createApiRoutes(cwd,
|
|
48
|
+
function createApiRoutes(cwd, portRef, wss) {
|
|
49
49
|
const api = new Hono();
|
|
50
50
|
api.get("/api/context", (c) => {
|
|
51
51
|
return c.json({
|
|
52
52
|
cwd,
|
|
53
53
|
projectName: basename(cwd),
|
|
54
|
-
port
|
|
54
|
+
port: portRef.value
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
api.get("/api/apps", (c) => {
|
|
@@ -175,24 +175,51 @@ function createApiRoutes(cwd, port = 4242, wss) {
|
|
|
175
175
|
mkdirSync(archiveDir, { recursive: true });
|
|
176
176
|
cpSync(flowDir, archiveDir, { recursive: true });
|
|
177
177
|
let templateDir;
|
|
178
|
+
let usedFallbackReset = false;
|
|
178
179
|
try {
|
|
179
180
|
templateDir = resolveTemplatePathWithCustom(config.name, cwd);
|
|
180
181
|
} catch {
|
|
182
|
+
usedFallbackReset = true;
|
|
183
|
+
console.warn(`[archive] Template "${config.name}" not found \u2014 using fallback reset`);
|
|
181
184
|
}
|
|
182
|
-
for (const loop of Object.
|
|
185
|
+
for (const [loopKey, loop] of Object.entries(config.loops)) {
|
|
186
|
+
const appTracker = join(flowDir, loop.tracker);
|
|
183
187
|
if (templateDir) {
|
|
184
188
|
const templateTracker = join(templateDir, "loops", loop.tracker);
|
|
185
|
-
const appTracker = join(flowDir, loop.tracker);
|
|
186
189
|
if (existsSync(templateTracker)) {
|
|
187
190
|
writeFileSync(appTracker, readFileSync(templateTracker, "utf-8"));
|
|
188
191
|
}
|
|
192
|
+
} else if (existsSync(appTracker)) {
|
|
193
|
+
const title = loop.name || loopKey;
|
|
194
|
+
writeFileSync(appTracker, `# ${title} \u2014 Tracker
|
|
195
|
+
|
|
196
|
+
- stage: idle
|
|
197
|
+
`);
|
|
189
198
|
}
|
|
190
|
-
if (loop.data_files
|
|
199
|
+
if (loop.data_files) {
|
|
191
200
|
for (const dataFile of loop.data_files) {
|
|
192
|
-
const templateData = join(templateDir, "loops", dataFile);
|
|
193
201
|
const appData = join(flowDir, dataFile);
|
|
194
|
-
if (
|
|
195
|
-
|
|
202
|
+
if (templateDir) {
|
|
203
|
+
const templateData = join(templateDir, "loops", dataFile);
|
|
204
|
+
if (existsSync(templateData)) {
|
|
205
|
+
writeFileSync(appData, readFileSync(templateData, "utf-8"));
|
|
206
|
+
}
|
|
207
|
+
} else if (existsSync(appData)) {
|
|
208
|
+
const fileName = basename(dataFile, ".md");
|
|
209
|
+
const header = fileName.charAt(0).toUpperCase() + fileName.slice(1);
|
|
210
|
+
writeFileSync(appData, `# ${header}
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (loop.directories) {
|
|
216
|
+
for (const dir of loop.directories) {
|
|
217
|
+
const dirPath = join(flowDir, dir);
|
|
218
|
+
if (existsSync(dirPath)) {
|
|
219
|
+
for (const entry of readdirSync(dirPath)) {
|
|
220
|
+
const entryPath = join(dirPath, entry);
|
|
221
|
+
rmSync(entryPath, { recursive: true, force: true });
|
|
222
|
+
}
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
225
|
}
|
|
@@ -224,7 +251,11 @@ function createApiRoutes(cwd, port = 4242, wss) {
|
|
|
224
251
|
}
|
|
225
252
|
}
|
|
226
253
|
const archivePath = `.ralph-flow/.archives/${appName}/${archiveTimestamp}`;
|
|
227
|
-
|
|
254
|
+
const result = { ok: true, archivePath, timestamp: archiveTimestamp };
|
|
255
|
+
if (usedFallbackReset) {
|
|
256
|
+
result.warning = `Template "${config.name}" not found \u2014 reset used fallback (minimal empty content)`;
|
|
257
|
+
}
|
|
258
|
+
return c.json(result);
|
|
228
259
|
} catch (err) {
|
|
229
260
|
const msg = err instanceof Error ? err.message : String(err);
|
|
230
261
|
return c.json({ error: `Archive failed: ${msg}` }, 500);
|
|
@@ -913,6 +944,7 @@ function removeNotificationHook(cwd) {
|
|
|
913
944
|
|
|
914
945
|
// src/dashboard/server.ts
|
|
915
946
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
947
|
+
var MAX_PORT_ATTEMPTS = 10;
|
|
916
948
|
var CONTENT_TYPES = {
|
|
917
949
|
".css": "text/css",
|
|
918
950
|
".js": "text/javascript",
|
|
@@ -933,15 +965,33 @@ function resolveUiDir() {
|
|
|
933
965
|
${candidates.map((c) => join4(c, "index.html")).join("\n")}`
|
|
934
966
|
);
|
|
935
967
|
}
|
|
968
|
+
function tryListen(server, hostname, port) {
|
|
969
|
+
return new Promise((resolve2, reject) => {
|
|
970
|
+
const onError = (err) => {
|
|
971
|
+
server.removeListener("listening", onListening);
|
|
972
|
+
reject(err);
|
|
973
|
+
};
|
|
974
|
+
const onListening = () => {
|
|
975
|
+
server.removeListener("error", onError);
|
|
976
|
+
const addr = server.address();
|
|
977
|
+
const boundPort = addr && typeof addr === "object" ? addr.port : port;
|
|
978
|
+
resolve2(boundPort);
|
|
979
|
+
};
|
|
980
|
+
server.once("error", onError);
|
|
981
|
+
server.once("listening", onListening);
|
|
982
|
+
server.listen(port, hostname);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
936
985
|
async function startDashboard(options) {
|
|
937
|
-
const { cwd, port = 4242 } = options;
|
|
986
|
+
const { cwd, port: requestedPort = 4242 } = options;
|
|
938
987
|
const app = new Hono2();
|
|
939
988
|
const wss = new WebSocketServer3({ noServer: true });
|
|
989
|
+
const portRef = { value: requestedPort };
|
|
940
990
|
app.use("*", cors({
|
|
941
991
|
origin: (origin) => origin || "*",
|
|
942
992
|
allowMethods: ["GET", "PUT", "POST", "DELETE"]
|
|
943
993
|
}));
|
|
944
|
-
const apiRoutes = createApiRoutes(cwd,
|
|
994
|
+
const apiRoutes = createApiRoutes(cwd, portRef, wss);
|
|
945
995
|
app.route("/", apiRoutes);
|
|
946
996
|
const uiDir = resolveUiDir();
|
|
947
997
|
app.get("/", (c) => {
|
|
@@ -958,11 +1008,25 @@ async function startDashboard(options) {
|
|
|
958
1008
|
const contentType = CONTENT_TYPES[extname(filePath)] || "text/plain";
|
|
959
1009
|
return c.text(content, 200, { "Content-Type": contentType });
|
|
960
1010
|
});
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1011
|
+
let server = createAdaptorServer({ fetch: app.fetch });
|
|
1012
|
+
let actualPort = requestedPort;
|
|
1013
|
+
for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
|
|
1014
|
+
const candidatePort = requestedPort + attempt;
|
|
1015
|
+
try {
|
|
1016
|
+
actualPort = await tryListen(server, "127.0.0.1", candidatePort);
|
|
1017
|
+
break;
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
const nodeErr = err;
|
|
1020
|
+
if (nodeErr.code !== "EADDRINUSE") throw err;
|
|
1021
|
+
if (attempt >= MAX_PORT_ATTEMPTS - 1) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`Could not find an available port (tried ${requestedPort}\u2013${requestedPort + MAX_PORT_ATTEMPTS - 1})`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
server = createAdaptorServer({ fetch: app.fetch });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
portRef.value = actualPort;
|
|
966
1030
|
server.on("upgrade", (request, socket, head) => {
|
|
967
1031
|
const url = new URL(request.url || "/", `http://${request.headers.host}`);
|
|
968
1032
|
if (url.pathname === "/ws") {
|
|
@@ -975,14 +1039,17 @@ async function startDashboard(options) {
|
|
|
975
1039
|
});
|
|
976
1040
|
const watcherHandle = setupWatcher(cwd, wss);
|
|
977
1041
|
try {
|
|
978
|
-
installNotificationHook(cwd,
|
|
1042
|
+
installNotificationHook(cwd, actualPort);
|
|
979
1043
|
console.log(chalk.dim(` Configured Claude hook \u2192 .claude/settings.local.json`));
|
|
980
1044
|
} catch (err) {
|
|
981
1045
|
const msg = err instanceof Error ? err.message : String(err);
|
|
982
1046
|
console.log(chalk.yellow(` \u26A0 Could not configure Claude hook: ${msg}`));
|
|
983
1047
|
}
|
|
984
1048
|
console.log();
|
|
985
|
-
|
|
1049
|
+
if (actualPort !== requestedPort) {
|
|
1050
|
+
console.log(chalk.yellow(` Port ${requestedPort} was busy, using ${actualPort} instead`));
|
|
1051
|
+
}
|
|
1052
|
+
console.log(chalk.bold(` Dashboard ${chalk.dim("\u2192")} http://localhost:${actualPort}`));
|
|
986
1053
|
console.log(chalk.dim(` Watching ${cwd}/.ralph-flow/`));
|
|
987
1054
|
console.log();
|
|
988
1055
|
let closed = false;
|
|
@@ -1014,7 +1081,7 @@ async function startDashboard(options) {
|
|
|
1014
1081
|
}
|
|
1015
1082
|
}
|
|
1016
1083
|
});
|
|
1017
|
-
return { close };
|
|
1084
|
+
return { close, port: actualPort };
|
|
1018
1085
|
}
|
|
1019
1086
|
export {
|
|
1020
1087
|
startDashboard
|
package/package.json
CHANGED
package/src/dashboard/ui/app.js
CHANGED
|
@@ -56,6 +56,9 @@ fetch('/api/context')
|
|
|
56
56
|
.then(r => r.json())
|
|
57
57
|
.then(ctx => {
|
|
58
58
|
dom.hostDisplay.textContent = ctx.projectName + ' :' + ctx.port;
|
|
59
|
+
dom.pageTitle.textContent = ctx.projectName;
|
|
60
|
+
document.title = ctx.projectName + ' \u00b7 RalphFlow Dashboard';
|
|
61
|
+
state.projectName = ctx.projectName;
|
|
59
62
|
})
|
|
60
63
|
.catch(() => { /* keep location.host as fallback */ });
|
|
61
64
|
|
|
@@ -183,7 +186,7 @@ document.getElementById('templatesNav').addEventListener('click', () => {
|
|
|
183
186
|
state.showTemplateWizard = false;
|
|
184
187
|
state.wizardStep = 0;
|
|
185
188
|
state.wizardData = null;
|
|
186
|
-
document.title = 'Templates
|
|
189
|
+
document.title = 'Templates \u00b7 ' + (state.projectName || 'RalphFlow Dashboard');
|
|
187
190
|
renderSidebar();
|
|
188
191
|
renderContent();
|
|
189
192
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Archive browsing: listing, file tree, file viewer.
|
|
2
2
|
|
|
3
3
|
import { state, actions } from './state.js';
|
|
4
|
-
import { fetchJson, esc } from './utils.js';
|
|
4
|
+
import { fetchJson, esc, renderMarkdown } from './utils.js';
|
|
5
5
|
|
|
6
6
|
export function switchAppTab(tab) {
|
|
7
7
|
if (tab === state.activeAppTab) return;
|
|
8
8
|
state.activeAppTab = tab;
|
|
9
9
|
state.expandedArchive = null;
|
|
10
10
|
state.archiveFilesCache = {};
|
|
11
|
+
state.archiveSummaryCache = {};
|
|
11
12
|
state.viewingArchiveFile = null;
|
|
12
13
|
actions.renderContent();
|
|
13
14
|
}
|
|
@@ -52,8 +53,21 @@ function renderArchivesView(container, appName) {
|
|
|
52
53
|
if (isExpanded) {
|
|
53
54
|
const files = state.archiveFilesCache[archive.timestamp];
|
|
54
55
|
if (files) {
|
|
56
|
+
// Show summary.md prominently at top if available, otherwise show CLI hint
|
|
57
|
+
const summaryContent = state.archiveSummaryCache[archive.timestamp];
|
|
58
|
+
if (summaryContent) {
|
|
59
|
+
html += `<div class="archive-summary">${renderMarkdown(summaryContent)}</div>`;
|
|
60
|
+
} else {
|
|
61
|
+
html += `<div class="archive-summary-hint">
|
|
62
|
+
<span class="archive-summary-hint-text">No summary yet. Generate one with:</span>
|
|
63
|
+
<code class="archive-summary-hint-cmd">npx ralphflow summarize ${esc(appName)} ${esc(archive.timestamp)}</code>
|
|
64
|
+
</div>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Filter summary.md from regular file list
|
|
68
|
+
const displayFiles = files.filter(f => f.path !== 'summary.md');
|
|
55
69
|
html += '<div class="archive-files">';
|
|
56
|
-
for (const file of
|
|
70
|
+
for (const file of displayFiles) {
|
|
57
71
|
const isActive = state.viewingArchiveFile === file.path;
|
|
58
72
|
html += `<div class="archive-file-item${isActive ? ' active' : ''}" data-archive-file="${esc(file.path)}" data-archive-ts="${esc(archive.timestamp)}">
|
|
59
73
|
<span class="archive-file-icon">📄</span>
|
|
@@ -129,6 +143,17 @@ async function toggleArchiveCard(appName, timestamp) {
|
|
|
129
143
|
try {
|
|
130
144
|
const files = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files`);
|
|
131
145
|
state.archiveFilesCache[timestamp] = files;
|
|
146
|
+
|
|
147
|
+
// Auto-fetch summary.md if present
|
|
148
|
+
const hasSummary = files.some(f => f.path === 'summary.md');
|
|
149
|
+
if (hasSummary) {
|
|
150
|
+
try {
|
|
151
|
+
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files/summary.md`);
|
|
152
|
+
state.archiveSummaryCache[timestamp] = data.content || '';
|
|
153
|
+
} catch {
|
|
154
|
+
// summary fetch failed — skip display
|
|
155
|
+
}
|
|
156
|
+
}
|
|
132
157
|
} catch {
|
|
133
158
|
state.archiveFilesCache[timestamp] = [];
|
|
134
159
|
}
|
|
@@ -604,7 +604,7 @@ async function submitDeleteApp(overlay, appName) {
|
|
|
604
604
|
if (state.selectedApp && state.selectedApp.appName === appName) {
|
|
605
605
|
state.selectedApp = null;
|
|
606
606
|
state.selectedLoop = null;
|
|
607
|
-
document.title = 'RalphFlow Dashboard';
|
|
607
|
+
document.title = state.projectName ? state.projectName + ' \u00b7 RalphFlow Dashboard' : 'RalphFlow Dashboard';
|
|
608
608
|
}
|
|
609
609
|
actions.fetchApps();
|
|
610
610
|
} catch (err) {
|
|
@@ -79,7 +79,7 @@ export function selectApp(app) {
|
|
|
79
79
|
state.viewingTemplateName = null;
|
|
80
80
|
state.viewingTemplateConfig = null;
|
|
81
81
|
state.viewingTemplatePrompts = {};
|
|
82
|
-
document.title = app.appName + '
|
|
82
|
+
document.title = app.appName + ' \u00b7 ' + (state.projectName || 'RalphFlow Dashboard');
|
|
83
83
|
renderSidebar();
|
|
84
84
|
actions.renderContent();
|
|
85
85
|
actions.fetchAppStatus(app.appName);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export const $ = (sel) => document.querySelector(sel);
|
|
5
5
|
|
|
6
6
|
export const dom = {
|
|
7
|
+
pageTitle: $('#pageTitle'),
|
|
7
8
|
hostDisplay: $('#hostDisplay'),
|
|
8
9
|
sidebarApps: $('#sidebarApps'),
|
|
9
10
|
content: $('#content'),
|
|
@@ -34,7 +35,9 @@ export const state = {
|
|
|
34
35
|
archivesData: [],
|
|
35
36
|
expandedArchive: null,
|
|
36
37
|
archiveFilesCache: {},
|
|
38
|
+
archiveSummaryCache: {},
|
|
37
39
|
viewingArchiveFile: null,
|
|
40
|
+
projectName: null,
|
|
38
41
|
currentPage: 'app',
|
|
39
42
|
templatesList: [],
|
|
40
43
|
showTemplateBuilder: false,
|
|
@@ -1195,6 +1195,62 @@ body {
|
|
|
1195
1195
|
transform: rotate(90deg);
|
|
1196
1196
|
}
|
|
1197
1197
|
|
|
1198
|
+
/* Archive summary display */
|
|
1199
|
+
.archive-summary {
|
|
1200
|
+
border-top: 1px solid var(--border);
|
|
1201
|
+
padding: 16px 20px;
|
|
1202
|
+
background: var(--bg);
|
|
1203
|
+
font-family: var(--mono);
|
|
1204
|
+
font-size: 13px;
|
|
1205
|
+
line-height: 1.6;
|
|
1206
|
+
color: var(--text);
|
|
1207
|
+
overflow-x: auto;
|
|
1208
|
+
}
|
|
1209
|
+
.archive-summary h1,
|
|
1210
|
+
.archive-summary h2,
|
|
1211
|
+
.archive-summary h3 {
|
|
1212
|
+
color: var(--text);
|
|
1213
|
+
margin: 12px 0 6px;
|
|
1214
|
+
font-size: 14px;
|
|
1215
|
+
}
|
|
1216
|
+
.archive-summary h1 { font-size: 15px; margin-top: 0; }
|
|
1217
|
+
.archive-summary .md-code-block {
|
|
1218
|
+
background: var(--bg-surface);
|
|
1219
|
+
border: 1px solid var(--border);
|
|
1220
|
+
border-radius: var(--radius);
|
|
1221
|
+
padding: 12px 16px;
|
|
1222
|
+
margin: 8px 0;
|
|
1223
|
+
overflow-x: auto;
|
|
1224
|
+
white-space: pre;
|
|
1225
|
+
font-size: 12px;
|
|
1226
|
+
line-height: 1.5;
|
|
1227
|
+
}
|
|
1228
|
+
.archive-summary .md-code-block code {
|
|
1229
|
+
font-family: var(--mono);
|
|
1230
|
+
}
|
|
1231
|
+
.archive-summary-hint {
|
|
1232
|
+
border-top: 1px solid var(--border);
|
|
1233
|
+
padding: 12px 20px;
|
|
1234
|
+
display: flex;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
gap: 10px;
|
|
1237
|
+
flex-wrap: wrap;
|
|
1238
|
+
}
|
|
1239
|
+
.archive-summary-hint-text {
|
|
1240
|
+
font-size: 12px;
|
|
1241
|
+
color: var(--text-dim);
|
|
1242
|
+
}
|
|
1243
|
+
.archive-summary-hint-cmd {
|
|
1244
|
+
font-family: var(--mono);
|
|
1245
|
+
font-size: 11px;
|
|
1246
|
+
color: var(--text-dim);
|
|
1247
|
+
background: var(--bg);
|
|
1248
|
+
border: 1px solid var(--border);
|
|
1249
|
+
border-radius: var(--radius);
|
|
1250
|
+
padding: 3px 8px;
|
|
1251
|
+
user-select: all;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1198
1254
|
/* Archive file browser */
|
|
1199
1255
|
.archive-files {
|
|
1200
1256
|
border-top: 1px solid var(--border);
|