Skip to content

Commit 8ac4d9e

Browse files
Askirclaude
andcommitted
feat(cli): add project selector to 0pflow run
Scan ~/0pflow/ for existing projects and present a selection list with "Create new project" as the last entry, instead of going straight to the create flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b86050 commit 8ac4d9e

File tree

1 file changed

+113
-38
lines changed

1 file changed

+113
-38
lines changed

packages/core/src/cli/run.ts

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { exec, execSync } from "node:child_process";
2-
import { existsSync, readFileSync } from "node:fs";
2+
import { existsSync, readFileSync, readdirSync } from "node:fs";
33
import { homedir } from "node:os";
44
import { promisify } from "node:util";
55
import { join, resolve } from "node:path";
@@ -167,6 +167,45 @@ function isExisting0pflow(): boolean {
167167
}
168168
}
169169

170+
interface ProjectInfo {
171+
name: string;
172+
path: string;
173+
}
174+
175+
/**
176+
* Scan ~/0pflow/ for directories that contain a package.json with 0pflow as a dependency.
177+
*/
178+
function discoverProjects(): ProjectInfo[] {
179+
const baseDir = join(homedir(), "0pflow");
180+
if (!existsSync(baseDir)) return [];
181+
182+
const projects: ProjectInfo[] = [];
183+
let entries: string[];
184+
try {
185+
entries = readdirSync(baseDir);
186+
} catch {
187+
return [];
188+
}
189+
190+
for (const entry of entries) {
191+
const projectDir = join(baseDir, entry);
192+
const pkgPath = join(projectDir, "package.json");
193+
try {
194+
if (!existsSync(pkgPath)) continue;
195+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
196+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
197+
if ("0pflow" in deps) {
198+
projects.push({ name: entry, path: projectDir });
199+
}
200+
} catch {
201+
continue;
202+
}
203+
}
204+
205+
projects.sort((a, b) => a.name.localeCompare(b.name));
206+
return projects;
207+
}
208+
170209
const WELCOME_PROMPT =
171210
"Welcome to your 0pflow project! What workflow would you like to create? Here are some ideas:\n\n" +
172211
'- "Enrich leads from a CSV file with company data"\n' +
@@ -175,6 +214,53 @@ const WELCOME_PROMPT =
175214
'- "Score and route inbound leads based on firmographics"\n\n' +
176215
"Describe what you'd like to automate and I'll help you build it with /create-workflow.";
177216

217+
async function launchExistingProject(projectPath: string): Promise<void> {
218+
// Check if database is paused and start it if needed
219+
try {
220+
const { findEnvFile } = await import("./env.js");
221+
const envPath = findEnvFile(projectPath);
222+
if (envPath) {
223+
const envContent = readFileSync(envPath, "utf-8");
224+
const parsed = dotenv.parse(envContent);
225+
226+
if (parsed.DATABASE_URL) {
227+
const serviceId = extractServiceIdFromUrl(parsed.DATABASE_URL);
228+
if (serviceId) {
229+
const status = startDatabaseIfNeeded(serviceId, false);
230+
if (status === "started") {
231+
const s = p.spinner();
232+
s.start("Database was paused, waiting for it to start...");
233+
const ready = await waitForDatabase(serviceId, 3 * 60 * 1000);
234+
if (ready) {
235+
s.stop(pc.green("Database is ready"));
236+
} else {
237+
s.stop(pc.yellow("Database is starting (taking longer than expected)"));
238+
}
239+
}
240+
}
241+
}
242+
}
243+
} catch {
244+
// Continue without database check if env loading fails
245+
}
246+
247+
const mode = await p.select({
248+
message: "Launch mode",
249+
options: [
250+
{ value: "normal" as const, label: "Launch" },
251+
{ value: "yolo" as const, label: "Launch with --dangerously-skip-permissions" },
252+
],
253+
});
254+
255+
if (p.isCancel(mode)) {
256+
p.cancel("Cancelled.");
257+
process.exit(0);
258+
}
259+
260+
p.outro(pc.green("Launching..."));
261+
await launchDevServer(projectPath, { yolo: mode === "yolo" });
262+
}
263+
178264
async function launchDevServer(cwd: string, { yolo = false }: { yolo?: boolean } = {}): Promise<void> {
179265
// Load .env from the app directory (not process.cwd(), which may be a parent)
180266
try {
@@ -235,53 +321,42 @@ export async function runRun(): Promise<void> {
235321
}
236322
}
237323

238-
// ── Existing project → launch ───────────────────────────────────────
324+
// ── Existing project (CWD) → launch directly ───────────────────────
239325
if (isExisting0pflow()) {
240-
// Check if database is paused and start it if needed
241-
try {
242-
const { findEnvFile } = await import("./env.js");
243-
const envPath = findEnvFile(process.cwd());
244-
if (envPath) {
245-
const envContent = readFileSync(envPath, "utf-8");
246-
const parsed = dotenv.parse(envContent);
247-
248-
if (parsed.DATABASE_URL) {
249-
const serviceId = extractServiceIdFromUrl(parsed.DATABASE_URL);
250-
if (serviceId) {
251-
const status = startDatabaseIfNeeded(serviceId, false);
252-
if (status === "started") {
253-
const s = p.spinner();
254-
s.start("Database was paused, waiting for it to start...");
255-
const ready = await waitForDatabase(serviceId, 3 * 60 * 1000);
256-
if (ready) {
257-
s.stop(pc.green("Database is ready"));
258-
} else {
259-
s.stop(pc.yellow("Database is starting (taking longer than expected)"));
260-
}
261-
}
262-
}
263-
}
264-
}
265-
} catch {
266-
// Continue without database check if env loading fails
267-
}
326+
await launchExistingProject(process.cwd());
327+
return;
328+
}
268329

269-
const mode = await p.select({
270-
message: "Launch mode",
330+
// ── Discover existing projects in ~/0pflow/ ───────────────────────
331+
const projects = discoverProjects();
332+
333+
if (projects.length > 0) {
334+
const CREATE_NEW = "__create_new__";
335+
336+
const projectChoice = await p.select({
337+
message: "Select a project",
271338
options: [
272-
{ value: "normal" as const, label: "Launch" },
273-
{ value: "yolo" as const, label: "Launch with --dangerously-skip-permissions" },
339+
...projects.map((proj) => ({
340+
value: proj.path,
341+
label: proj.name,
342+
hint: proj.path,
343+
})),
344+
{
345+
value: CREATE_NEW,
346+
label: pc.green("+ Create new project"),
347+
},
274348
],
275349
});
276350

277-
if (p.isCancel(mode)) {
351+
if (p.isCancel(projectChoice)) {
278352
p.cancel("Cancelled.");
279353
process.exit(0);
280354
}
281355

282-
p.outro(pc.green("Launching..."));
283-
await launchDevServer(process.cwd(), { yolo: mode === "yolo" });
284-
return;
356+
if (projectChoice !== CREATE_NEW) {
357+
await launchExistingProject(projectChoice);
358+
return;
359+
}
285360
}
286361

287362
// ── Project name ────────────────────────────────────────────────────

0 commit comments

Comments
 (0)