feat(KB-503): merge kb/kb-503
- feat(KB-503): complete Step 5 other command project support - fix(KB-503): align task resolution with shared project context - fix(KB-503): preserve local task command fallback - test(KB-503): complete Step 4 task command coverage - test(KB-503): harden Step 3 CLI parser coverage - fix(KB-503): keep settings set scope explicit - fix(KB-503): allow implicit project resolution for project settings - fix(KB-503): enforce explicit settings scope rules - fix(KB-503): restore shared settings resolution behavior - fix(KB-503): align settings scope and project flag behavior - feat(KB-503): complete Step 5 — add project-aware settings, git, and backup behavior - fix(KB-503): enforce settings scope for global and project modes - fix(KB-503): preserve project-scoped settings and cwd-aware git helpers - fix(KB-503): add scoped settings behavior and git project tests - feat(KB-503): complete Step 5 — add project-aware git and backup coverage - test(KB-503): cover remaining project-aware task command paths - fix(KB-503): use shared resolution flow for all task commands - fix(KB-503): restore legacy task fallback and correct task mocks - fix(KB-503): align task resolution order and kb storage paths - fix(KB-503): restore local task store fallback without project flag - test(KB-503): cover remaining project-aware task handlers - fix(KB-503): preserve branch naming and avoid duplicate log resolution - test(KB-503): expand project-aware task command coverage - fix(KB-503): restore cwd fallback for path-based task commands - fix(KB-503): add project-aware task output and tests - fix(KB-503): harden project command output and coverage - fix(KB-503): preserve legacy task resolution without project flag - test(KB-503): add bin routing coverage for project flag parsing - feat(KB-503): complete Step 3 — CLI argument parsing updates - fix(KB-503): add defaultProjectId to GlobalSettings type and fix TypeScript errors - test(KB-503): fix resolveProject mock in task tests for runTaskLogs follow mode - docs(KB-503): add multi-project CLI documentation and changeset - test(KB-503): add project-context mock to task tests - test(KB-503): update git tests for new cwd parameter - feat(KB-503): Step 3 — CLI argument parsing updates with --project flag support - feat(KB-503): Steps 1-2 — project context utilities and project subcommands
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus, type AgentLogType, type AgentLogEntry } from "@fusion/core";
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column, type MergeResult, type StepStatus, type AgentLogType, type AgentLogEntry, type Task } from "@fusion/core";
|
||||
import { aiMergeTask } from "@fusion/engine";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||
@@ -7,19 +7,83 @@ import { watchFile, unwatchFile, statSync, existsSync, readFileSync } from "node
|
||||
import { join } from "node:path";
|
||||
import { GitHubClient } from "@fusion/dashboard";
|
||||
import { isGhAvailable, isGhAuthenticated, getCurrentRepo } from "@fusion/core/gh-cli";
|
||||
import { getStore as getStoreFromResolver, resolveProject } from "../project-resolver.js";
|
||||
import { resolveProject, type ProjectContext } from "../project-context.js";
|
||||
|
||||
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
|
||||
|
||||
async function getStore(projectName?: string): Promise<TaskStore> {
|
||||
interface CommandContext {
|
||||
store: TaskStore;
|
||||
projectPath: string;
|
||||
projectName?: string;
|
||||
explicit: boolean;
|
||||
}
|
||||
|
||||
function asLocalProjectContext(store: TaskStore): ProjectContext {
|
||||
const cwd = process.cwd();
|
||||
return {
|
||||
projectId: cwd,
|
||||
projectPath: cwd,
|
||||
projectName: cwd.split("/").filter(Boolean).at(-1) ?? "current-project",
|
||||
isRegistered: false,
|
||||
store,
|
||||
};
|
||||
}
|
||||
|
||||
async function getCommandContext(projectName?: string): Promise<CommandContext> {
|
||||
if (projectName) {
|
||||
return getStoreFromResolver({ project: projectName });
|
||||
const context = await resolveProject(projectName);
|
||||
return {
|
||||
store: context.store,
|
||||
projectPath: context.projectPath,
|
||||
projectName: context.projectName,
|
||||
explicit: true,
|
||||
};
|
||||
}
|
||||
return getStoreFromResolver();
|
||||
|
||||
try {
|
||||
const context = await resolveProject(undefined);
|
||||
return {
|
||||
store: context.store,
|
||||
projectPath: context.projectPath,
|
||||
projectName: context.projectName,
|
||||
explicit: false,
|
||||
};
|
||||
} catch (error) {
|
||||
const store = new TaskStore(process.cwd());
|
||||
await store.init();
|
||||
return {
|
||||
store,
|
||||
projectPath: process.cwd(),
|
||||
projectName: undefined,
|
||||
explicit: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getStore(projectName?: string): Promise<TaskStore> {
|
||||
return (await getCommandContext(projectName)).store;
|
||||
}
|
||||
|
||||
async function getProjectContext(projectName?: string): Promise<ProjectContext | undefined> {
|
||||
if (projectName) {
|
||||
return resolveProject(projectName);
|
||||
}
|
||||
|
||||
try {
|
||||
return await resolveProject(undefined);
|
||||
} catch {
|
||||
const store = await getStore();
|
||||
return asLocalProjectContext(store);
|
||||
}
|
||||
}
|
||||
|
||||
async function getProjectPath(projectName?: string): Promise<string> {
|
||||
return (await getCommandContext(projectName)).projectPath;
|
||||
}
|
||||
|
||||
export async function runTaskCreate(descriptionArg?: string, attachFiles?: string[], depends?: string[], projectName?: string) {
|
||||
let description = descriptionArg;
|
||||
const projectContext = await getProjectContext(projectName);
|
||||
|
||||
if (!description) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
@@ -32,7 +96,7 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const store = await getStore(projectName);
|
||||
const store = projectContext?.store ?? await getStore(projectName);
|
||||
const task = await store.createTask({ description: description.trim(), dependencies: depends });
|
||||
|
||||
const label = task.description.length > 60
|
||||
@@ -40,12 +104,15 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin
|
||||
: task.description;
|
||||
|
||||
console.log();
|
||||
if (projectContext) {
|
||||
console.log(` Project: ${projectContext.projectName}`);
|
||||
}
|
||||
console.log(` ✓ Created ${task.id}: ${label}`);
|
||||
console.log(` Column: triage`);
|
||||
if (task.dependencies.length > 0) {
|
||||
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
||||
}
|
||||
console.log(` Path: .fusion/tasks/${task.id}/`);
|
||||
console.log(` Path: .kb/tasks/${task.id}/`);
|
||||
|
||||
if (attachFiles && attachFiles.length > 0) {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
@@ -80,7 +147,8 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin
|
||||
}
|
||||
|
||||
export async function runTaskList(projectName?: string) {
|
||||
const store = await getStore(projectName);
|
||||
const projectContext = await getProjectContext(projectName);
|
||||
const store = projectContext?.store ?? await getStore(projectName);
|
||||
const tasks = await store.listTasks();
|
||||
|
||||
if (tasks.length === 0) {
|
||||
@@ -89,6 +157,10 @@ export async function runTaskList(projectName?: string) {
|
||||
}
|
||||
|
||||
console.log();
|
||||
if (projectContext && projectName) {
|
||||
console.log(` Tasks for project '${projectContext.projectName}':`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
for (const col of COLUMNS) {
|
||||
const colTasks = tasks.filter((t) => t.column === col);
|
||||
@@ -218,7 +290,8 @@ function filterEntries(entries: AgentLogEntry[], options: LogsOptions): AgentLog
|
||||
}
|
||||
|
||||
export async function runTaskLogs(id: string, options: LogsOptions = {}, projectName?: string) {
|
||||
const store = await getStore(projectName);
|
||||
const projectContext = await getProjectContext(projectName);
|
||||
const store = projectContext?.store ?? await getStore(projectName);
|
||||
|
||||
// Verify task exists
|
||||
try {
|
||||
@@ -236,15 +309,19 @@ export async function runTaskLogs(id: string, options: LogsOptions = {}, project
|
||||
return;
|
||||
}
|
||||
|
||||
if (projectContext && projectName) {
|
||||
console.log(` Logs for project '${projectContext.projectName}':`);
|
||||
}
|
||||
|
||||
// Print existing entries (filtered)
|
||||
const filteredEntries = filterEntries(entries, options);
|
||||
printEntries(filteredEntries);
|
||||
|
||||
// Follow mode: watch for new entries
|
||||
if (options.follow) {
|
||||
const store = await getStore(projectName);
|
||||
const projectPath = (await resolveProject(projectName)).projectPath;
|
||||
const logPath = join(projectPath, ".fusion", "tasks", id, "agent.log");
|
||||
const followStore = store;
|
||||
const projectPath = projectContext?.projectPath ?? process.cwd();
|
||||
const logPath = join(projectPath, ".kb", "tasks", id, "agent.log");
|
||||
|
||||
if (!existsSync(logPath)) {
|
||||
console.log(`\n Waiting for log file to be created...`);
|
||||
@@ -364,7 +441,7 @@ export async function runTaskShow(id: string, projectName?: string) {
|
||||
|
||||
export async function runTaskMerge(id: string, projectName?: string) {
|
||||
const store = await getStore(projectName);
|
||||
const { projectPath } = await resolveProject(projectName);
|
||||
const projectPath = await getProjectPath(projectName);
|
||||
|
||||
console.log(`\n Merging ${id} with AI...\n`);
|
||||
|
||||
@@ -437,7 +514,7 @@ export async function runTaskAttach(id: string, filePath: string, projectName?:
|
||||
console.log();
|
||||
console.log(` ✓ Attached to ${id}: ${attachment.originalName}`);
|
||||
console.log(` File: ${attachment.filename} (${sizeKB} KB)`);
|
||||
console.log(` Path: .fusion/tasks/${id}/attachments/${attachment.filename}`);
|
||||
console.log(` Path: .kb/tasks/${id}/attachments/${attachment.filename}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -480,7 +557,7 @@ export async function runTaskDuplicate(id: string, projectName?: string) {
|
||||
|
||||
console.log();
|
||||
console.log(` ✓ Duplicated ${id} → ${newTask.id}`);
|
||||
console.log(` Path: .fusion/tasks/${newTask.id}/`);
|
||||
console.log(` Path: .kb/tasks/${newTask.id}/`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -512,7 +589,7 @@ export async function runTaskRefine(id: string, feedbackArg?: string, projectNam
|
||||
console.log(` ✓ Created refinement ${newTask.id} for ${id}`);
|
||||
console.log(` Column: triage`);
|
||||
console.log(` Dependency: ${id}`);
|
||||
console.log(` Path: .fusion/tasks/${newTask.id}/`);
|
||||
console.log(` Path: .kb/tasks/${newTask.id}/`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -977,7 +1054,7 @@ export async function runTaskSteer(id: string, message?: string, projectName?: s
|
||||
// Add steering comment
|
||||
let task;
|
||||
try {
|
||||
task = await store.addComment(id, trimmed, "user");
|
||||
task = await store.addSteeringComment(id, trimmed, "user");
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
console.error(`Error: Task not found: ${id}`);
|
||||
@@ -989,7 +1066,7 @@ export async function runTaskSteer(id: string, message?: string, projectName?: s
|
||||
// Show success with preview
|
||||
const preview = trimmed.length > 60 ? trimmed.slice(0, 60) + "…" : trimmed;
|
||||
console.log();
|
||||
console.log(` ✓ Comment added to ${task.id}`);
|
||||
console.log(` ✓ Steering comment added to ${task.id}`);
|
||||
console.log(` "${preview}"`);
|
||||
console.log();
|
||||
}
|
||||
@@ -1043,7 +1120,8 @@ export async function runTaskPrCreate(id: string, options: PrCreateOptions = {},
|
||||
owner = o;
|
||||
repo = r;
|
||||
} else {
|
||||
const gitRepo = getCurrentRepo(process.cwd());
|
||||
const projectPath = await getProjectPath(projectName);
|
||||
const gitRepo = getCurrentRepo(projectPath);
|
||||
if (!gitRepo) {
|
||||
console.error("Error: Could not determine GitHub repository. Set GITHUB_REPOSITORY env var or configure git remote.");
|
||||
process.exit(1);
|
||||
@@ -1060,8 +1138,8 @@ export async function runTaskPrCreate(id: string, options: PrCreateOptions = {},
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build branch name
|
||||
const branchName = `kb/${id.toLowerCase()}`;
|
||||
// Build branch name using the established project convention
|
||||
const branchName = `fusion/${id.toLowerCase()}`;
|
||||
|
||||
// Build PR title
|
||||
let title: string;
|
||||
@@ -1347,7 +1425,8 @@ export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, proj
|
||||
|
||||
try {
|
||||
showThinking();
|
||||
const result = await createSession("127.0.0.1", initialPlan.trim(), store, process.cwd());
|
||||
const projectPath = await getProjectPath(projectName);
|
||||
const result = await createSession("127.0.0.1", initialPlan.trim(), store, projectPath);
|
||||
clearThinking();
|
||||
sessionId = result.sessionId;
|
||||
firstQuestion = result.firstQuestion;
|
||||
@@ -1421,7 +1500,7 @@ export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, proj
|
||||
|
||||
try {
|
||||
showThinking();
|
||||
result = await submitResponse(sessionId, response as Record<string, unknown>) as typeof result;
|
||||
result = await submitResponse(sessionId, response) as typeof result;
|
||||
clearThinking();
|
||||
} catch (err) {
|
||||
clearThinking();
|
||||
@@ -1468,7 +1547,7 @@ export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, proj
|
||||
if (task.dependencies.length > 0) {
|
||||
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
||||
}
|
||||
console.log(` Path: .fusion/tasks/${task.id}/`);
|
||||
console.log(` Path: .kb/tasks/${task.id}/`);
|
||||
console.log();
|
||||
} else {
|
||||
console.log("\n Task creation cancelled.\n");
|
||||
|
||||
Reference in New Issue
Block a user