Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
114c3f2
feat: auto-publish RC when dev merges to main
namastex888 Oct 21, 2025
dd541b8
fix: remove pnpm version conflict in GitHub Action
namastex888 Oct 21, 2025
3641a21
chore: merge main into dev, resolve pnpm version conflict
namastex888 Oct 21, 2025
586fc10
fix: add contents write permission to workflow for git push
namastex888 Oct 21, 2025
4881428
fix: use gh CLI for automated version bump via PR workflow
namastex888 Oct 21, 2025
7384746
chore: merge main into dev, keep pull-requests write permission
namastex888 Oct 21, 2025
4c0d86f
fix: make workflow idempotent and continue on duplicate publishes
namastex888 Oct 21, 2025
640603e
chore: merge main into dev, keep idempotent workflow
namastex888 Oct 21, 2025
24427bf
fix: remove release label from auto-PR (label doesn't exist)
namastex888 Oct 21, 2025
1af0a2b
fix: surgical fixes for init template selection and executor list
namastex888 Oct 21, 2025
003bb0a
chore: pre-release v2.4.2-rc.22
namastex888 Oct 21, 2025
f1ac4d7
fix: checkout tag in publish workflow
namastex888 Oct 21, 2025
6986de3
fix: resolve merge conflict in publish-rc-on-merge workflow
namastex888 Oct 21, 2025
bf09d3e
chore: bump automagik-forge to 0.4.2
namastex888 Oct 21, 2025
9f4bfbb
Learn: Genie Must Not Duplicate Forge Work (Orchestration Boundary Vi…
namastex888 Oct 21, 2025
982d469
fix: include template directories in npm package
namastex888 Oct 21, 2025
a8c1d95
fix: include templates in npm package + smart launcher
namastex888 Oct 21, 2025
7bcc62e
fix: resolve merge conflicts - keep smart launcher versions
namastex888 Oct 21, 2025
a6061aa
Merge branch 'main' into dev
namastex888 Oct 21, 2025
44166c7
fix: init preserves collective structure, adds git check
namastex888 Oct 21, 2025
095916e
fix: template selection menu not displaying in shell scripts
namastex888 Oct 21, 2025
525e18b
wip: Ink-powered onboarding (wizard + chat UI)
namastex888 Oct 21, 2025
c3b5809
🚧 WIP: Interactive Onboarding Architecture
namastex888 Oct 21, 2025
3d27856
chore: bump version to 2.4.2-rc.26
namastex888 Oct 21, 2025
e9dc2e1
feat: Interactive onboarding with Ink wizard (ES modules)
namastex888 Oct 21, 2025
2811715
chore: bump version to 2.4.2-rc.27
namastex888 Oct 21, 2025
36f011c
fix: Graceful Ctrl+C shutdown with running task detection (#168)
namastex888 Oct 21, 2025
a09217b
chore: pre-release v2.4.2-rc.28
namastex888 Oct 21, 2025
d4836ca
Merge branch 'main' into dev - resolved conflicts (kept Ink wizard)
namastex888 Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 55 additions & 21 deletions .genie/cli/dist/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,41 @@ async function runInit(parsed, _config, _paths) {
const flags = parseFlags(parsed.commandArgs);
const cwd = process.cwd();
const packageRoot = (0, paths_1.getPackageRoot)();
// Determine template type
// Direct: genie init code/create (automation-friendly)
// Interactive: genie init (human discovery)
const template = flags.template || await promptTemplateChoice();
// Check if running in interactive mode (TTY) or automation mode (--yes flag or explicit template)
const isInteractive = process.stdout.isTTY && !flags.yes && !flags.template;
let template;
let executor;
let model;
let shouldInitGit = false;
if (isInteractive) {
// Use dynamic import to load ESM Ink components
// @ts-expect-error - .mjs file exists at runtime
const { runInitWizard } = await import('../views/init-wizard.mjs');
const templates = [
{ value: 'code', label: 'πŸ’» Code', description: 'Full-stack development with Git, testing, CI/CD' },
{ value: 'create', label: '✍️ Create', description: 'Research, writing, content creation' }
];
const executors = Object.keys(executor_registry_1.EXECUTORS).map(key => ({
label: executor_registry_1.EXECUTORS[key].label,
value: key
}));
const hasGit = await (0, fs_utils_1.pathExists)(path_1.default.join(cwd, '.git'));
const wizardConfig = await runInitWizard({
templates,
executors,
hasGit
});
template = wizardConfig.template;
executor = wizardConfig.executor;
model = wizardConfig.model;
shouldInitGit = wizardConfig.initGit;
}
else {
// Automation mode: use flags or defaults
template = (flags.template || 'code');
executor = Object.keys(executor_registry_1.EXECUTORS)[0] || 'codex';
model = undefined;
}
const templateGenie = (0, paths_1.getTemplateGeniePath)(template);
const targetGenie = (0, paths_1.resolveTargetGeniePath)(cwd);
const templateExists = await (0, fs_utils_1.pathExists)(templateGenie);
Expand All @@ -45,7 +76,7 @@ async function runInit(parsed, _config, _paths) {
const { executor, model } = await selectExecutorAndModel(flags);
await applyExecutorDefaults(targetGenie, executor, model);
await (0, mcp_config_1.configureBothExecutors)(cwd);
await runInstallViaCli(cwd, template);
// TODO: Add install chat flow here (future enhancement)
await (0, view_helpers_1.emitView)(buildInitSummaryView({ executor, model, templateSource: templateGenie, target: targetGenie }), parsed.options);
return;
}
Expand All @@ -68,15 +99,9 @@ async function runInit(parsed, _config, _paths) {
]), parsed.options);
return;
}
// Check for .git directory, offer initialization if missing
const gitDir = path_1.default.join(cwd, '.git');
const hasGit = await (0, fs_utils_1.pathExists)(gitDir);
if (!hasGit) {
console.log('');
console.log('⚠️ No .git directory found');
console.log('🧞 Forge requires git to track work');
console.log('');
if (flags.yes || await promptYesNo('Initialize git repository?', true)) {
// Initialize git if needed (wizard already prompted in interactive mode)
if (shouldInitGit || (!isInteractive && !await (0, fs_utils_1.pathExists)(path_1.default.join(cwd, '.git')))) {
if (!isInteractive && flags.yes) {
const { execSync } = await import('child_process');
execSync('git init', { cwd, stdio: 'inherit' });
try {
Expand All @@ -85,12 +110,16 @@ async function runInit(parsed, _config, _paths) {
catch {
// Ignore if branch rename fails (already on main)
}
console.log('βœ… Git initialized');
console.log('');
}
else {
console.log('⚠️ Skipping git init (Forge may not work correctly)');
console.log('');
else if (shouldInitGit) {
const { execSync } = await import('child_process');
execSync('git init', { cwd, stdio: 'inherit' });
try {
execSync('git branch -m main', { cwd, stdio: 'pipe' });
}
catch {
// Ignore if branch rename fails (already on main)
}
}
}
const backupId = (0, fs_utils_1.toIsoId)();
Expand Down Expand Up @@ -147,13 +176,18 @@ async function runInit(parsed, _config, _paths) {
}
}
await (0, fs_utils_1.ensureDir)(backupsRoot);
const { executor, model } = await selectExecutorAndModel(flags);
// Use wizard selections in interactive mode, or select defaults in automation mode
if (!isInteractive && !executor) {
const selected = await selectExecutorAndModel(flags);
executor = selected.executor;
model = selected.model;
}
await writeVersionState(cwd, backupId, false);
await initializeProviderStatus(cwd);
await applyExecutorDefaults(targetGenie, executor, model);
// Configure MCP servers for both Codex and Claude Code
await (0, mcp_config_1.configureBothExecutors)(cwd);
await runInstallViaCli(cwd, template);
// TODO: Add install chat flow here (future enhancement)
const summary = { executor, model, backupId, templateSource: templateGenie, target: targetGenie };
await (0, view_helpers_1.emitView)(buildInitSummaryView(summary), parsed.options);
}
Expand Down
33 changes: 33 additions & 0 deletions .genie/cli/dist/genie-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,37 @@ async function startGenieServer() {
console.log(genieGradient('━'.repeat(60)));
console.log(genieGradient('πŸ›‘ Shutting down Genie...'));
console.log(genieGradient('━'.repeat(60)));
// Check for running tasks before killing Forge
const runningTasks = await (0, forge_manager_1.getRunningTasks)(baseUrl);
if (runningTasks.length > 0) {
console.log('');
console.log('⚠️ WARNING: Running tasks detected!');
console.log('');
console.log(`${runningTasks.length} task(s) are currently running:`);
console.log('');
runningTasks.forEach((task, index) => {
console.log(`${index + 1}. ${task.projectName} β†’ ${task.taskTitle}`);
console.log(` ${task.url}`);
console.log('');
});
// Prompt for confirmation
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise((resolve) => {
readline.question('Kill these tasks and shutdown? [y/N]: ', resolve);
});
readline.close();
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
console.log('');
console.log('❌ Shutdown cancelled. Tasks are still running.');
console.log(' Press Ctrl+C again to force shutdown.');
console.log('');
isShuttingDown = false; // Reset flag to allow retry
return;
}
}
// Calculate session stats
const sessionDuration = Date.now() - startTime;
const uptimeStr = formatUptime(sessionDuration);
Expand All @@ -448,6 +479,8 @@ async function startGenieServer() {
mcpChild.kill('SIGTERM');
console.log('πŸ“‘ MCP server stopped');
}
// Kill Forge child process immediately (prevents orphaned processes)
(0, forge_manager_1.killForgeProcess)();
// Stop Forge and wait for completion
try {
const stopped = await (0, forge_manager_1.stopForge)(logDir);
Expand Down
70 changes: 70 additions & 0 deletions .genie/cli/dist/lib/forge-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.isForgeRunning = isForgeRunning;
exports.waitForForgeReady = waitForForgeReady;
exports.startForgeInBackground = startForgeInBackground;
exports.getRunningTasks = getRunningTasks;
exports.killForgeProcess = killForgeProcess;
exports.stopForge = stopForge;
exports.restartForge = restartForge;
exports.getForgeProcess = getForgeProcess;
Expand All @@ -17,6 +19,8 @@ const forge_js_1 = require("../../../../forge.js");
const DEFAULT_BASE_URL = process.env.FORGE_BASE_URL || 'http://localhost:8887';
const HEALTH_CHECK_TIMEOUT = 3000; // 3s per health check
const MAX_HEALTH_RETRIES = 3;
// Track Forge child process for graceful shutdown
let forgeChildProcess = null;
/**
* Health check with retry logic and exponential backoff
*/
Expand Down Expand Up @@ -184,6 +188,8 @@ function startForgeInBackground(opts) {
fs_1.default.appendFileSync(logPath, `\n[EARLY EXIT] Process exited with code ${code}, signal ${signal}\n`);
}
});
// Track child process for graceful shutdown
forgeChildProcess = child;
// Detach so it survives parent exit
child.unref();
// Close our handle to log file (child has inherited it)
Expand All @@ -208,6 +214,70 @@ function startForgeInBackground(opts) {
}
};
}
/**
* Check for running task attempts and return them with URLs
*/
async function getRunningTasks(baseUrl = DEFAULT_BASE_URL) {
try {
const client = new forge_js_1.ForgeClient(baseUrl, process.env.FORGE_TOKEN);
// Get all projects
const projects = await client.listProjects();
const runningTasks = [];
// Check each project for running attempts
for (const project of projects) {
const tasks = await client.listTasks(project.id);
for (const task of tasks) {
// Check if task has running attempts
const attempts = await client.listAttempts(project.id, task.id);
const runningAttempts = attempts.filter((a) => a.status === 'running' || a.status === 'pending');
for (const attempt of runningAttempts) {
runningTasks.push({
projectId: project.id,
projectName: project.name || 'Unnamed Project',
taskId: task.id,
taskTitle: task.title || 'Untitled Task',
attemptId: attempt.id,
url: `${baseUrl}/projects/${project.id}/tasks/${task.id}/attempts/${attempt.id}`
});
}
}
}
return runningTasks;
}
catch (error) {
// If we can't check, return empty array (don't block shutdown)
return [];
}
}
/**
* Kill Forge child process immediately (for Ctrl+C shutdown)
* Sends SIGTERM to the entire process group
*/
function killForgeProcess() {
if (!forgeChildProcess || forgeChildProcess.killed) {
return;
}
try {
const pid = forgeChildProcess.pid;
if (pid) {
// Kill the entire process group (negative PID)
// This ensures all child processes are terminated
try {
process.kill(-pid, 'SIGTERM');
}
catch (err) {
// If process group kill fails, try killing the process directly
forgeChildProcess.kill('SIGTERM');
}
}
}
catch (error) {
// Ignore errors - process might already be dead
}
finally {
forgeChildProcess = null;
}
}
/**
* Stop Forge process with verification
*/
Expand Down
Loading
Loading