cc-mirror 1.2.1 → 1.4.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 +51 -0
- package/dist/cc-mirror.mjs +1136 -20
- package/dist/skills/orchestration/SKILL.md +17 -15
- package/dist/skills/task-manager/SKILL.md +86 -0
- package/dist/tui.mjs +92 -17
- package/package.json +1 -1
package/dist/cc-mirror.mjs
CHANGED
|
@@ -39,9 +39,17 @@ var parseArgs = (argv) => {
|
|
|
39
39
|
if (value2) opts.env.push(value2);
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
|
-
const [key, inlineValue] = arg.startsWith("--") ? arg.slice(2).split("=") : [
|
|
42
|
+
const [key, inlineValue] = arg.startsWith("--") ? arg.slice(2).split("=") : [void 0, void 0];
|
|
43
43
|
if (!key) continue;
|
|
44
|
-
|
|
44
|
+
let value;
|
|
45
|
+
if (inlineValue !== void 0) {
|
|
46
|
+
value = inlineValue;
|
|
47
|
+
} else {
|
|
48
|
+
const next = args[0];
|
|
49
|
+
if (next && !next.startsWith("-")) {
|
|
50
|
+
value = args.shift();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
45
53
|
if (value !== void 0) {
|
|
46
54
|
opts[key] = value;
|
|
47
55
|
} else {
|
|
@@ -104,6 +112,7 @@ COMMANDS
|
|
|
104
112
|
remove <name> Remove a variant
|
|
105
113
|
doctor Health check all variants
|
|
106
114
|
tweak <name> Launch tweakcc customization
|
|
115
|
+
tasks [operation] Manage team tasks (list, show, create, update, delete, clean)
|
|
107
116
|
|
|
108
117
|
OPTIONS (create/quick)
|
|
109
118
|
--name <name> Variant name (becomes CLI command)
|
|
@@ -2456,6 +2465,63 @@ var removeOrchestratorSkill = (configDir) => {
|
|
|
2456
2465
|
return { status: "failed", message };
|
|
2457
2466
|
}
|
|
2458
2467
|
};
|
|
2468
|
+
var TASK_MANAGER_SKILL_NAME = "task-manager";
|
|
2469
|
+
var findBundledTaskManagerSkillDir = () => {
|
|
2470
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
2471
|
+
const thisDir = path7.dirname(thisFile);
|
|
2472
|
+
const devPath = path7.join(thisDir, "..", "skills", TASK_MANAGER_SKILL_NAME);
|
|
2473
|
+
if (fs6.existsSync(devPath)) return devPath;
|
|
2474
|
+
const distPath = path7.join(thisDir, "skills", TASK_MANAGER_SKILL_NAME);
|
|
2475
|
+
if (fs6.existsSync(distPath)) return distPath;
|
|
2476
|
+
const distPath2 = path7.join(thisDir, "..", "skills", TASK_MANAGER_SKILL_NAME);
|
|
2477
|
+
if (fs6.existsSync(distPath2)) return distPath2;
|
|
2478
|
+
return null;
|
|
2479
|
+
};
|
|
2480
|
+
var installTaskManagerSkill = (configDir) => {
|
|
2481
|
+
const sourceDir = findBundledTaskManagerSkillDir();
|
|
2482
|
+
if (!sourceDir) {
|
|
2483
|
+
return { status: "failed", message: "bundled task-manager skill not found" };
|
|
2484
|
+
}
|
|
2485
|
+
const skillsDir = path7.join(configDir, "skills");
|
|
2486
|
+
const targetDir = path7.join(skillsDir, TASK_MANAGER_SKILL_NAME);
|
|
2487
|
+
const markerPath = path7.join(targetDir, MANAGED_MARKER);
|
|
2488
|
+
try {
|
|
2489
|
+
ensureDir2(skillsDir);
|
|
2490
|
+
if (fs6.existsSync(targetDir) && !fs6.existsSync(markerPath)) {
|
|
2491
|
+
return { status: "skipped", message: "existing skill is user-managed", path: targetDir };
|
|
2492
|
+
}
|
|
2493
|
+
if (fs6.existsSync(targetDir)) {
|
|
2494
|
+
fs6.rmSync(targetDir, { recursive: true, force: true });
|
|
2495
|
+
}
|
|
2496
|
+
copyDir(sourceDir, targetDir);
|
|
2497
|
+
fs6.writeFileSync(
|
|
2498
|
+
markerPath,
|
|
2499
|
+
JSON.stringify({ managedBy: "cc-mirror", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
2500
|
+
);
|
|
2501
|
+
return { status: "installed", path: targetDir };
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2504
|
+
return { status: "failed", message };
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
var removeTaskManagerSkill = (configDir) => {
|
|
2508
|
+
const skillsDir = path7.join(configDir, "skills");
|
|
2509
|
+
const targetDir = path7.join(skillsDir, TASK_MANAGER_SKILL_NAME);
|
|
2510
|
+
const markerPath = path7.join(targetDir, MANAGED_MARKER);
|
|
2511
|
+
if (!fs6.existsSync(targetDir)) {
|
|
2512
|
+
return { status: "skipped", message: "skill not installed" };
|
|
2513
|
+
}
|
|
2514
|
+
if (!fs6.existsSync(markerPath)) {
|
|
2515
|
+
return { status: "skipped", message: "skill is user-managed, not removing" };
|
|
2516
|
+
}
|
|
2517
|
+
try {
|
|
2518
|
+
fs6.rmSync(targetDir, { recursive: true, force: true });
|
|
2519
|
+
return { status: "removed", path: targetDir };
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2522
|
+
return { status: "failed", message };
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2459
2525
|
var spawnAsync = (cmd, args) => {
|
|
2460
2526
|
return new Promise((resolve) => {
|
|
2461
2527
|
const child = spawn3(cmd, args, { stdio: "pipe" });
|
|
@@ -2612,7 +2678,7 @@ var TeamModeStep = class {
|
|
|
2612
2678
|
this.patchCli(ctx);
|
|
2613
2679
|
}
|
|
2614
2680
|
patchCli(ctx) {
|
|
2615
|
-
const { state,
|
|
2681
|
+
const { state, paths } = ctx;
|
|
2616
2682
|
const cliPath = path9.join(paths.npmDir, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
2617
2683
|
const backupPath = `${cliPath}.backup`;
|
|
2618
2684
|
if (!fs8.existsSync(cliPath)) {
|
|
@@ -2643,8 +2709,8 @@ var TeamModeStep = class {
|
|
|
2643
2709
|
try {
|
|
2644
2710
|
const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf8"));
|
|
2645
2711
|
settings.env = settings.env || {};
|
|
2646
|
-
if (!settings.env.
|
|
2647
|
-
settings.env.
|
|
2712
|
+
if (!settings.env.CLAUDE_CODE_TEAM_MODE) {
|
|
2713
|
+
settings.env.CLAUDE_CODE_TEAM_MODE = "1";
|
|
2648
2714
|
}
|
|
2649
2715
|
if (!settings.env.CLAUDE_CODE_AGENT_TYPE) {
|
|
2650
2716
|
settings.env.CLAUDE_CODE_AGENT_TYPE = "team-lead";
|
|
@@ -2666,6 +2732,12 @@ var TeamModeStep = class {
|
|
|
2666
2732
|
} else if (skillResult.status === "failed") {
|
|
2667
2733
|
state.notes.push(`Warning: orchestrator skill install failed: ${skillResult.message}`);
|
|
2668
2734
|
}
|
|
2735
|
+
const taskSkillResult = installTaskManagerSkill(paths.configDir);
|
|
2736
|
+
if (taskSkillResult.status === "installed") {
|
|
2737
|
+
state.notes.push("Task manager skill installed");
|
|
2738
|
+
} else if (taskSkillResult.status === "failed") {
|
|
2739
|
+
state.notes.push(`Warning: task-manager skill install failed: ${taskSkillResult.message}`);
|
|
2740
|
+
}
|
|
2669
2741
|
const systemPromptsDir = path9.join(paths.tweakDir, "system-prompts");
|
|
2670
2742
|
const copiedFiles = copyTeamPackPrompts(systemPromptsDir);
|
|
2671
2743
|
if (copiedFiles.length > 0) {
|
|
@@ -3615,23 +3687,23 @@ var writeWrapper = (wrapperPath, configDir, binaryPath, runtime = "node") => {
|
|
|
3615
3687
|
'if [[ "${CC_MIRROR_UNSET_AUTH_TOKEN:-0}" != "0" ]]; then',
|
|
3616
3688
|
" unset ANTHROPIC_AUTH_TOKEN",
|
|
3617
3689
|
"fi",
|
|
3618
|
-
"# Dynamic team name:
|
|
3619
|
-
|
|
3690
|
+
"# Dynamic team name: purely directory-based, with optional TEAM modifier",
|
|
3691
|
+
"# Check for CLAUDE_CODE_TEAM_MODE (not TEAM_NAME) to avoid Claude Code overwriting",
|
|
3692
|
+
'if [[ -n "${CLAUDE_CODE_TEAM_MODE:-}" ]]; then',
|
|
3620
3693
|
" __cc_git_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)",
|
|
3621
3694
|
' __cc_folder_name=$(basename "$__cc_git_root")',
|
|
3622
|
-
' if [[ -n "$TEAM" ]]; then',
|
|
3623
|
-
" #
|
|
3624
|
-
' export CLAUDE_CODE_TEAM_NAME="${
|
|
3695
|
+
' if [[ -n "${TEAM:-}" ]]; then',
|
|
3696
|
+
" # Folder name + TEAM modifier",
|
|
3697
|
+
' export CLAUDE_CODE_TEAM_NAME="${__cc_folder_name}-${TEAM}"',
|
|
3625
3698
|
" else",
|
|
3626
|
-
" #
|
|
3627
|
-
' export CLAUDE_CODE_TEAM_NAME="${
|
|
3699
|
+
" # Just folder name (pure directory-based)",
|
|
3700
|
+
' export CLAUDE_CODE_TEAM_NAME="${__cc_folder_name}"',
|
|
3628
3701
|
" fi",
|
|
3629
|
-
'elif [[ -n "$TEAM" ]]; then',
|
|
3630
|
-
" # TEAM
|
|
3702
|
+
'elif [[ -n "${TEAM:-}" ]]; then',
|
|
3703
|
+
" # TEAM env var set without team mode in settings - use folder + TEAM",
|
|
3631
3704
|
" __cc_git_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)",
|
|
3632
3705
|
' __cc_folder_name=$(basename "$__cc_git_root")',
|
|
3633
|
-
'
|
|
3634
|
-
' export CLAUDE_CODE_TEAM_NAME="${__cc_variant_name}-${__cc_folder_name}-${TEAM}"',
|
|
3706
|
+
' export CLAUDE_CODE_TEAM_NAME="${__cc_folder_name}-${TEAM}"',
|
|
3635
3707
|
"fi",
|
|
3636
3708
|
...splash,
|
|
3637
3709
|
execLine,
|
|
@@ -4111,9 +4183,15 @@ var TeamModeUpdateStep = class {
|
|
|
4111
4183
|
} else if (skillResult.status === "failed") {
|
|
4112
4184
|
state.notes.push(`Warning: orchestrator skill removal failed: ${skillResult.message}`);
|
|
4113
4185
|
}
|
|
4186
|
+
const taskSkillResult = removeTaskManagerSkill(meta.configDir);
|
|
4187
|
+
if (taskSkillResult.status === "removed") {
|
|
4188
|
+
state.notes.push("Task manager skill removed");
|
|
4189
|
+
} else if (taskSkillResult.status === "failed") {
|
|
4190
|
+
state.notes.push(`Warning: task-manager skill removal failed: ${taskSkillResult.message}`);
|
|
4191
|
+
}
|
|
4114
4192
|
}
|
|
4115
4193
|
patchCli(ctx) {
|
|
4116
|
-
const { state, meta,
|
|
4194
|
+
const { state, meta, paths } = ctx;
|
|
4117
4195
|
const cliPath = path18.join(paths.npmDir, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
4118
4196
|
const backupPath = `${cliPath}.backup`;
|
|
4119
4197
|
if (!fs13.existsSync(cliPath)) {
|
|
@@ -4145,8 +4223,8 @@ var TeamModeUpdateStep = class {
|
|
|
4145
4223
|
try {
|
|
4146
4224
|
const settings = JSON.parse(fs13.readFileSync(settingsPath, "utf8"));
|
|
4147
4225
|
settings.env = settings.env || {};
|
|
4148
|
-
if (!settings.env.
|
|
4149
|
-
settings.env.
|
|
4226
|
+
if (!settings.env.CLAUDE_CODE_TEAM_MODE) {
|
|
4227
|
+
settings.env.CLAUDE_CODE_TEAM_MODE = "1";
|
|
4150
4228
|
}
|
|
4151
4229
|
if (!settings.env.CLAUDE_CODE_AGENT_TYPE) {
|
|
4152
4230
|
settings.env.CLAUDE_CODE_AGENT_TYPE = "team-lead";
|
|
@@ -4169,6 +4247,12 @@ var TeamModeUpdateStep = class {
|
|
|
4169
4247
|
} else if (skillResult.status === "failed") {
|
|
4170
4248
|
state.notes.push(`Warning: orchestrator skill install failed: ${skillResult.message}`);
|
|
4171
4249
|
}
|
|
4250
|
+
const taskSkillResult = installTaskManagerSkill(meta.configDir);
|
|
4251
|
+
if (taskSkillResult.status === "installed") {
|
|
4252
|
+
state.notes.push("Task manager skill installed");
|
|
4253
|
+
} else if (taskSkillResult.status === "failed") {
|
|
4254
|
+
state.notes.push(`Warning: task-manager skill install failed: ${taskSkillResult.message}`);
|
|
4255
|
+
}
|
|
4172
4256
|
const systemPromptsDir = path18.join(meta.tweakDir, "system-prompts");
|
|
4173
4257
|
const copiedFiles = copyTeamPackPrompts(systemPromptsDir);
|
|
4174
4258
|
if (copiedFiles.length > 0) {
|
|
@@ -5028,6 +5112,1034 @@ async function runCreateCommand({ opts, quickMode }) {
|
|
|
5028
5112
|
}
|
|
5029
5113
|
}
|
|
5030
5114
|
|
|
5115
|
+
// src/core/tasks/store.ts
|
|
5116
|
+
import fs15 from "node:fs";
|
|
5117
|
+
import path25 from "node:path";
|
|
5118
|
+
function getTasksDir(rootDir, variant, team) {
|
|
5119
|
+
return path25.join(rootDir, variant, "config", "tasks", team);
|
|
5120
|
+
}
|
|
5121
|
+
function listTaskIds(tasksDir) {
|
|
5122
|
+
if (!fs15.existsSync(tasksDir)) return [];
|
|
5123
|
+
return fs15.readdirSync(tasksDir).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", "")).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
5124
|
+
}
|
|
5125
|
+
function loadTask(tasksDir, id) {
|
|
5126
|
+
const taskPath = path25.join(tasksDir, `${id}.json`);
|
|
5127
|
+
if (!fs15.existsSync(taskPath)) return null;
|
|
5128
|
+
return readJson(taskPath);
|
|
5129
|
+
}
|
|
5130
|
+
function loadAllTasks(tasksDir) {
|
|
5131
|
+
const ids = listTaskIds(tasksDir);
|
|
5132
|
+
return ids.map((id) => loadTask(tasksDir, id)).filter((task) => task !== null);
|
|
5133
|
+
}
|
|
5134
|
+
function saveTask(tasksDir, task) {
|
|
5135
|
+
fs15.mkdirSync(tasksDir, { recursive: true });
|
|
5136
|
+
const taskPath = path25.join(tasksDir, `${task.id}.json`);
|
|
5137
|
+
writeJson(taskPath, task);
|
|
5138
|
+
}
|
|
5139
|
+
function deleteTask(tasksDir, id) {
|
|
5140
|
+
const taskPath = path25.join(tasksDir, `${id}.json`);
|
|
5141
|
+
if (!fs15.existsSync(taskPath)) return false;
|
|
5142
|
+
fs15.unlinkSync(taskPath);
|
|
5143
|
+
return true;
|
|
5144
|
+
}
|
|
5145
|
+
function getNextTaskId(tasksDir) {
|
|
5146
|
+
const ids = listTaskIds(tasksDir);
|
|
5147
|
+
if (ids.length === 0) return "1";
|
|
5148
|
+
const maxId = Math.max(...ids.map((id) => parseInt(id, 10)));
|
|
5149
|
+
return String(maxId + 1);
|
|
5150
|
+
}
|
|
5151
|
+
function createTask(tasksDir, subject, description, opts) {
|
|
5152
|
+
const id = getNextTaskId(tasksDir);
|
|
5153
|
+
const task = {
|
|
5154
|
+
id,
|
|
5155
|
+
subject,
|
|
5156
|
+
description,
|
|
5157
|
+
status: "open",
|
|
5158
|
+
owner: opts?.owner,
|
|
5159
|
+
references: [],
|
|
5160
|
+
blocks: opts?.blocks || [],
|
|
5161
|
+
blockedBy: opts?.blockedBy || [],
|
|
5162
|
+
comments: []
|
|
5163
|
+
};
|
|
5164
|
+
saveTask(tasksDir, task);
|
|
5165
|
+
return task;
|
|
5166
|
+
}
|
|
5167
|
+
function listTeams(rootDir, variant) {
|
|
5168
|
+
const tasksRoot = path25.join(rootDir, variant, "config", "tasks");
|
|
5169
|
+
if (!fs15.existsSync(tasksRoot)) return [];
|
|
5170
|
+
return fs15.readdirSync(tasksRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// src/core/tasks/resolve.ts
|
|
5174
|
+
import fs16 from "node:fs";
|
|
5175
|
+
import path26 from "node:path";
|
|
5176
|
+
import { execSync } from "node:child_process";
|
|
5177
|
+
function detectVariantFromEnv() {
|
|
5178
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
5179
|
+
if (!configDir) return null;
|
|
5180
|
+
const match = configDir.match(/\.cc-mirror\/([^/]+)\/config/);
|
|
5181
|
+
return match ? match[1] : null;
|
|
5182
|
+
}
|
|
5183
|
+
function detectCurrentTeam(cwd) {
|
|
5184
|
+
const teamFromEnv = process.env.CLAUDE_CODE_TEAM_NAME;
|
|
5185
|
+
if (teamFromEnv) {
|
|
5186
|
+
return teamFromEnv;
|
|
5187
|
+
}
|
|
5188
|
+
const workDir = cwd || process.cwd();
|
|
5189
|
+
let gitRoot;
|
|
5190
|
+
try {
|
|
5191
|
+
gitRoot = execSync("git rev-parse --show-toplevel 2>/dev/null", {
|
|
5192
|
+
cwd: workDir,
|
|
5193
|
+
encoding: "utf8"
|
|
5194
|
+
}).trim();
|
|
5195
|
+
} catch {
|
|
5196
|
+
gitRoot = workDir;
|
|
5197
|
+
}
|
|
5198
|
+
const folderName = path26.basename(gitRoot);
|
|
5199
|
+
const teamModifier = process.env.TEAM;
|
|
5200
|
+
if (teamModifier) {
|
|
5201
|
+
return `${folderName}-${teamModifier}`;
|
|
5202
|
+
}
|
|
5203
|
+
return folderName;
|
|
5204
|
+
}
|
|
5205
|
+
function listVariantsWithTasks(rootDir) {
|
|
5206
|
+
const variants = listVariants(rootDir);
|
|
5207
|
+
return variants.map((v) => v.name).filter((name) => {
|
|
5208
|
+
const tasksRoot = path26.join(rootDir, name, "config", "tasks");
|
|
5209
|
+
return fs16.existsSync(tasksRoot);
|
|
5210
|
+
});
|
|
5211
|
+
}
|
|
5212
|
+
function resolveContext(opts) {
|
|
5213
|
+
const { rootDir, variant, team, allVariants, allTeams, cwd } = opts;
|
|
5214
|
+
const locations = [];
|
|
5215
|
+
let variants;
|
|
5216
|
+
if (allVariants) {
|
|
5217
|
+
variants = listVariantsWithTasks(rootDir);
|
|
5218
|
+
} else if (variant) {
|
|
5219
|
+
variants = [variant];
|
|
5220
|
+
} else {
|
|
5221
|
+
const envVariant = detectVariantFromEnv();
|
|
5222
|
+
if (envVariant) {
|
|
5223
|
+
variants = [envVariant];
|
|
5224
|
+
} else {
|
|
5225
|
+
const variantsWithTasks = listVariantsWithTasks(rootDir);
|
|
5226
|
+
variants = variantsWithTasks.length > 0 ? [variantsWithTasks[0]] : [];
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
for (const v of variants) {
|
|
5230
|
+
let teams;
|
|
5231
|
+
if (allTeams) {
|
|
5232
|
+
teams = listTeams(rootDir, v);
|
|
5233
|
+
} else if (team) {
|
|
5234
|
+
teams = [team];
|
|
5235
|
+
} else {
|
|
5236
|
+
const detectedTeam = detectCurrentTeam(cwd);
|
|
5237
|
+
const availableTeams = listTeams(rootDir, v);
|
|
5238
|
+
if (availableTeams.includes(detectedTeam)) {
|
|
5239
|
+
teams = [detectedTeam];
|
|
5240
|
+
} else if (availableTeams.length > 0) {
|
|
5241
|
+
teams = availableTeams;
|
|
5242
|
+
} else {
|
|
5243
|
+
teams = [];
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
for (const t of teams) {
|
|
5247
|
+
const tasksDir = path26.join(rootDir, v, "config", "tasks", t);
|
|
5248
|
+
if (fs16.existsSync(tasksDir)) {
|
|
5249
|
+
locations.push({ variant: v, team: t, tasksDir });
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
return { locations };
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
// src/core/tasks/queries.ts
|
|
5257
|
+
function isBlocked(task, allTasks) {
|
|
5258
|
+
if (task.blockedBy.length === 0) return false;
|
|
5259
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
5260
|
+
return task.blockedBy.some((id) => {
|
|
5261
|
+
const blockingTask = taskMap.get(id);
|
|
5262
|
+
return blockingTask && blockingTask.status === "open";
|
|
5263
|
+
});
|
|
5264
|
+
}
|
|
5265
|
+
function isBlocking(task) {
|
|
5266
|
+
return task.blocks.length > 0;
|
|
5267
|
+
}
|
|
5268
|
+
function filterTasks(tasks, filter, allTasks) {
|
|
5269
|
+
let filtered = [...tasks];
|
|
5270
|
+
const taskContext = allTasks || tasks;
|
|
5271
|
+
if (filter.status && filter.status !== "all") {
|
|
5272
|
+
filtered = filtered.filter((t) => t.status === filter.status);
|
|
5273
|
+
}
|
|
5274
|
+
if (filter.blocked !== void 0) {
|
|
5275
|
+
if (filter.blocked) {
|
|
5276
|
+
filtered = filtered.filter((t) => isBlocked(t, taskContext));
|
|
5277
|
+
} else {
|
|
5278
|
+
filtered = filtered.filter((t) => !isBlocked(t, taskContext));
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
if (filter.blocking !== void 0) {
|
|
5282
|
+
if (filter.blocking) {
|
|
5283
|
+
filtered = filtered.filter((t) => isBlocking(t));
|
|
5284
|
+
} else {
|
|
5285
|
+
filtered = filtered.filter((t) => !isBlocking(t));
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
if (filter.owner) {
|
|
5289
|
+
filtered = filtered.filter((t) => t.owner === filter.owner);
|
|
5290
|
+
}
|
|
5291
|
+
if (filter.limit && filter.limit > 0) {
|
|
5292
|
+
filtered = filtered.slice(0, filter.limit);
|
|
5293
|
+
}
|
|
5294
|
+
return filtered;
|
|
5295
|
+
}
|
|
5296
|
+
function getTaskSummary(tasks) {
|
|
5297
|
+
const open = tasks.filter((t) => t.status === "open");
|
|
5298
|
+
const resolved = tasks.filter((t) => t.status === "resolved");
|
|
5299
|
+
const blocked = open.filter((t) => isBlocked(t, tasks));
|
|
5300
|
+
return {
|
|
5301
|
+
total: tasks.length,
|
|
5302
|
+
open: open.length,
|
|
5303
|
+
resolved: resolved.length,
|
|
5304
|
+
blocked: blocked.length
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
function sortTasksById(tasks) {
|
|
5308
|
+
return [...tasks].sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
// src/cli/commands/tasks/output.ts
|
|
5312
|
+
function truncate(text, maxLen) {
|
|
5313
|
+
if (text.length <= maxLen) return text;
|
|
5314
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5315
|
+
}
|
|
5316
|
+
function pad(text, width) {
|
|
5317
|
+
return text.padEnd(width);
|
|
5318
|
+
}
|
|
5319
|
+
function formatTaskTable(tasks, location, summary) {
|
|
5320
|
+
const lines = [];
|
|
5321
|
+
const { variant, team } = location;
|
|
5322
|
+
lines.push(`TASKS (${variant} / ${team}) - ${summary.open} open, ${summary.resolved} resolved`);
|
|
5323
|
+
lines.push("\u2500".repeat(70));
|
|
5324
|
+
lines.push(`${pad("ID", 5)} ${pad("STATUS", 10)} ${pad("SUBJECT", 45)} BLOCKED`);
|
|
5325
|
+
lines.push("\u2500".repeat(70));
|
|
5326
|
+
for (const task of tasks) {
|
|
5327
|
+
const blocked = isBlocked(task, tasks) ? "\u25CF" : "";
|
|
5328
|
+
const status = task.status;
|
|
5329
|
+
const subject = truncate(task.subject, 45);
|
|
5330
|
+
lines.push(`${pad(task.id, 5)} ${pad(status, 10)} ${pad(subject, 45)} ${blocked}`);
|
|
5331
|
+
}
|
|
5332
|
+
lines.push("\u2500".repeat(70));
|
|
5333
|
+
if (summary.blocked > 0) {
|
|
5334
|
+
lines.push("\u25CF = blocked by unresolved tasks");
|
|
5335
|
+
}
|
|
5336
|
+
return lines.join("\n");
|
|
5337
|
+
}
|
|
5338
|
+
function formatMultiLocationTaskTable(tasksByLocation) {
|
|
5339
|
+
const sections = [];
|
|
5340
|
+
for (const { location, tasks, summary } of tasksByLocation) {
|
|
5341
|
+
sections.push(formatTaskTable(tasks, location, summary));
|
|
5342
|
+
sections.push("");
|
|
5343
|
+
}
|
|
5344
|
+
return sections.join("\n");
|
|
5345
|
+
}
|
|
5346
|
+
function formatTaskDetail(task, location, allTasks) {
|
|
5347
|
+
const lines = [];
|
|
5348
|
+
const { variant, team } = location;
|
|
5349
|
+
lines.push(`TASK #${task.id} (${variant} / ${team})`);
|
|
5350
|
+
lines.push("\u2550".repeat(60));
|
|
5351
|
+
lines.push("");
|
|
5352
|
+
lines.push(`Subject: ${task.subject}`);
|
|
5353
|
+
lines.push(`Status: ${task.status}`);
|
|
5354
|
+
lines.push(`Owner: ${task.owner || "(unassigned)"}`);
|
|
5355
|
+
lines.push("");
|
|
5356
|
+
if (task.description) {
|
|
5357
|
+
lines.push("Description:");
|
|
5358
|
+
const descLines = task.description.split("\n");
|
|
5359
|
+
for (const line of descLines.slice(0, 10)) {
|
|
5360
|
+
lines.push(` ${line}`);
|
|
5361
|
+
}
|
|
5362
|
+
if (descLines.length > 10) {
|
|
5363
|
+
lines.push(` ... (${descLines.length - 10} more lines)`);
|
|
5364
|
+
}
|
|
5365
|
+
lines.push("");
|
|
5366
|
+
}
|
|
5367
|
+
if (task.blockedBy.length > 0 || task.blocks.length > 0) {
|
|
5368
|
+
lines.push("Dependencies:");
|
|
5369
|
+
if (task.blockedBy.length > 0) {
|
|
5370
|
+
const blockedByStatus = task.blockedBy.map((id) => {
|
|
5371
|
+
const t = allTasks.find((t2) => t2.id === id);
|
|
5372
|
+
return t ? `#${id} (${t.status})` : `#${id} (?)`;
|
|
5373
|
+
});
|
|
5374
|
+
lines.push(` Blocked by: ${blockedByStatus.join(", ")}`);
|
|
5375
|
+
}
|
|
5376
|
+
if (task.blocks.length > 0) {
|
|
5377
|
+
lines.push(` Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
|
|
5378
|
+
}
|
|
5379
|
+
lines.push("");
|
|
5380
|
+
}
|
|
5381
|
+
if (task.references.length > 0) {
|
|
5382
|
+
lines.push(`References: ${task.references.map((id) => `#${id}`).join(", ")}`);
|
|
5383
|
+
lines.push("");
|
|
5384
|
+
}
|
|
5385
|
+
if (task.comments.length > 0) {
|
|
5386
|
+
lines.push(`Comments (${task.comments.length}):`);
|
|
5387
|
+
for (const comment of task.comments) {
|
|
5388
|
+
lines.push(` \u250C\u2500 ${comment.author} ${"\u2500".repeat(Math.max(0, 50 - comment.author.length))}`);
|
|
5389
|
+
const commentLines = comment.content.split("\n");
|
|
5390
|
+
for (const line of commentLines) {
|
|
5391
|
+
lines.push(` \u2502 ${line}`);
|
|
5392
|
+
}
|
|
5393
|
+
lines.push(" \u2514" + "\u2500".repeat(55));
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
return lines.join("\n");
|
|
5397
|
+
}
|
|
5398
|
+
function formatTasksJson(tasks, location, summary) {
|
|
5399
|
+
return JSON.stringify(
|
|
5400
|
+
{
|
|
5401
|
+
variant: location.variant,
|
|
5402
|
+
team: location.team,
|
|
5403
|
+
tasks,
|
|
5404
|
+
summary
|
|
5405
|
+
},
|
|
5406
|
+
null,
|
|
5407
|
+
2
|
|
5408
|
+
);
|
|
5409
|
+
}
|
|
5410
|
+
function formatTaskJson(task, location) {
|
|
5411
|
+
return JSON.stringify(
|
|
5412
|
+
{
|
|
5413
|
+
variant: location.variant,
|
|
5414
|
+
team: location.team,
|
|
5415
|
+
task
|
|
5416
|
+
},
|
|
5417
|
+
null,
|
|
5418
|
+
2
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
5421
|
+
function formatMultiLocationJson(tasksByLocation) {
|
|
5422
|
+
return JSON.stringify(
|
|
5423
|
+
{
|
|
5424
|
+
locations: tasksByLocation.map(({ location, tasks, summary }) => ({
|
|
5425
|
+
variant: location.variant,
|
|
5426
|
+
team: location.team,
|
|
5427
|
+
tasks,
|
|
5428
|
+
summary
|
|
5429
|
+
}))
|
|
5430
|
+
},
|
|
5431
|
+
null,
|
|
5432
|
+
2
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
function formatCleanResults(results) {
|
|
5436
|
+
const lines = [];
|
|
5437
|
+
for (const { location, deleted, dryRun } of results) {
|
|
5438
|
+
const action = dryRun ? "Would delete" : "Deleted";
|
|
5439
|
+
lines.push(`${location.variant} / ${location.team}: ${action} ${deleted.length} tasks`);
|
|
5440
|
+
if (deleted.length > 0 && deleted.length <= 10) {
|
|
5441
|
+
lines.push(` IDs: ${deleted.join(", ")}`);
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
return lines.join("\n");
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
// src/cli/commands/tasks/list.ts
|
|
5448
|
+
function runTasksList(opts) {
|
|
5449
|
+
const context = resolveContext({
|
|
5450
|
+
rootDir: opts.rootDir,
|
|
5451
|
+
variant: opts.variant,
|
|
5452
|
+
team: opts.team,
|
|
5453
|
+
allVariants: opts.allVariants,
|
|
5454
|
+
allTeams: opts.allTeams
|
|
5455
|
+
});
|
|
5456
|
+
if (context.locations.length === 0) {
|
|
5457
|
+
console.log("No task locations found. Check variant and team settings.");
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
const tasksByLocation = context.locations.map((location) => {
|
|
5461
|
+
const allTasks = loadAllTasks(location.tasksDir);
|
|
5462
|
+
const filteredTasks = filterTasks(
|
|
5463
|
+
allTasks,
|
|
5464
|
+
{
|
|
5465
|
+
status: opts.status || "open",
|
|
5466
|
+
blocked: opts.blocked,
|
|
5467
|
+
blocking: opts.blocking,
|
|
5468
|
+
owner: opts.owner,
|
|
5469
|
+
limit: opts.limit
|
|
5470
|
+
},
|
|
5471
|
+
allTasks
|
|
5472
|
+
);
|
|
5473
|
+
const sortedTasks = sortTasksById(filteredTasks);
|
|
5474
|
+
const summary = getTaskSummary(allTasks);
|
|
5475
|
+
return { location, tasks: sortedTasks, summary };
|
|
5476
|
+
});
|
|
5477
|
+
if (opts.json) {
|
|
5478
|
+
if (tasksByLocation.length === 1) {
|
|
5479
|
+
const { location, tasks, summary } = tasksByLocation[0];
|
|
5480
|
+
console.log(formatTasksJson(tasks, location, summary));
|
|
5481
|
+
} else {
|
|
5482
|
+
console.log(formatMultiLocationJson(tasksByLocation));
|
|
5483
|
+
}
|
|
5484
|
+
} else {
|
|
5485
|
+
if (tasksByLocation.length === 1) {
|
|
5486
|
+
const { location, tasks, summary } = tasksByLocation[0];
|
|
5487
|
+
console.log(formatTaskTable(tasks, location, summary));
|
|
5488
|
+
} else {
|
|
5489
|
+
console.log(formatMultiLocationTaskTable(tasksByLocation));
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5494
|
+
// src/cli/commands/tasks/show.ts
|
|
5495
|
+
function runTasksShow(opts) {
|
|
5496
|
+
const context = resolveContext({
|
|
5497
|
+
rootDir: opts.rootDir,
|
|
5498
|
+
variant: opts.variant,
|
|
5499
|
+
team: opts.team
|
|
5500
|
+
});
|
|
5501
|
+
if (context.locations.length === 0) {
|
|
5502
|
+
console.error("No task locations found. Check variant and team settings.");
|
|
5503
|
+
process.exitCode = 1;
|
|
5504
|
+
return;
|
|
5505
|
+
}
|
|
5506
|
+
for (const location of context.locations) {
|
|
5507
|
+
const task = loadTask(location.tasksDir, opts.taskId);
|
|
5508
|
+
if (task) {
|
|
5509
|
+
const allTasks = loadAllTasks(location.tasksDir);
|
|
5510
|
+
if (opts.json) {
|
|
5511
|
+
console.log(formatTaskJson(task, location));
|
|
5512
|
+
} else {
|
|
5513
|
+
console.log(formatTaskDetail(task, location, allTasks));
|
|
5514
|
+
}
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
console.error(`Task #${opts.taskId} not found.`);
|
|
5519
|
+
process.exitCode = 1;
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// src/cli/commands/tasks/create.ts
|
|
5523
|
+
function runTasksCreate(opts) {
|
|
5524
|
+
const context = resolveContext({
|
|
5525
|
+
rootDir: opts.rootDir,
|
|
5526
|
+
variant: opts.variant,
|
|
5527
|
+
team: opts.team
|
|
5528
|
+
});
|
|
5529
|
+
let location = context.locations[0];
|
|
5530
|
+
if (!location) {
|
|
5531
|
+
const team = opts.team || detectCurrentTeam();
|
|
5532
|
+
const variant = opts.variant;
|
|
5533
|
+
if (!variant) {
|
|
5534
|
+
console.error("No variant specified. Use --variant to specify a variant.");
|
|
5535
|
+
process.exitCode = 1;
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
const tasksDir = getTasksDir(opts.rootDir, variant, team);
|
|
5539
|
+
location = { variant, team, tasksDir };
|
|
5540
|
+
}
|
|
5541
|
+
const task = createTask(location.tasksDir, opts.subject, opts.description || "", {
|
|
5542
|
+
owner: opts.owner,
|
|
5543
|
+
blocks: opts.blocks,
|
|
5544
|
+
blockedBy: opts.blockedBy
|
|
5545
|
+
});
|
|
5546
|
+
if (opts.json) {
|
|
5547
|
+
console.log(formatTaskJson(task, location));
|
|
5548
|
+
} else {
|
|
5549
|
+
console.log(`Created task #${task.id}: ${task.subject}`);
|
|
5550
|
+
console.log(`Location: ${location.variant} / ${location.team}`);
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
// src/cli/commands/tasks/update.ts
|
|
5555
|
+
function runTasksUpdate(opts) {
|
|
5556
|
+
const context = resolveContext({
|
|
5557
|
+
rootDir: opts.rootDir,
|
|
5558
|
+
variant: opts.variant,
|
|
5559
|
+
team: opts.team
|
|
5560
|
+
});
|
|
5561
|
+
if (context.locations.length === 0) {
|
|
5562
|
+
console.error("No task locations found. Check variant and team settings.");
|
|
5563
|
+
process.exitCode = 1;
|
|
5564
|
+
return;
|
|
5565
|
+
}
|
|
5566
|
+
for (const location of context.locations) {
|
|
5567
|
+
const task = loadTask(location.tasksDir, opts.taskId);
|
|
5568
|
+
if (!task) continue;
|
|
5569
|
+
if (opts.subject) task.subject = opts.subject;
|
|
5570
|
+
if (opts.description) task.description = opts.description;
|
|
5571
|
+
if (opts.status) task.status = opts.status;
|
|
5572
|
+
if (opts.owner !== void 0) task.owner = opts.owner || void 0;
|
|
5573
|
+
if (opts.addBlocks) {
|
|
5574
|
+
for (const id of opts.addBlocks) {
|
|
5575
|
+
if (!task.blocks.includes(id)) task.blocks.push(id);
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
if (opts.removeBlocks) {
|
|
5579
|
+
task.blocks = task.blocks.filter((id) => !opts.removeBlocks.includes(id));
|
|
5580
|
+
}
|
|
5581
|
+
if (opts.addBlockedBy) {
|
|
5582
|
+
for (const id of opts.addBlockedBy) {
|
|
5583
|
+
if (!task.blockedBy.includes(id)) task.blockedBy.push(id);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
if (opts.removeBlockedBy) {
|
|
5587
|
+
task.blockedBy = task.blockedBy.filter((id) => !opts.removeBlockedBy.includes(id));
|
|
5588
|
+
}
|
|
5589
|
+
if (opts.addComment) {
|
|
5590
|
+
task.comments.push({
|
|
5591
|
+
author: opts.commentAuthor || "cli",
|
|
5592
|
+
content: opts.addComment
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
saveTask(location.tasksDir, task);
|
|
5596
|
+
if (opts.json) {
|
|
5597
|
+
console.log(formatTaskJson(task, location));
|
|
5598
|
+
} else {
|
|
5599
|
+
console.log(`Updated task #${task.id}: ${task.subject}`);
|
|
5600
|
+
}
|
|
5601
|
+
return;
|
|
5602
|
+
}
|
|
5603
|
+
console.error(`Task #${opts.taskId} not found.`);
|
|
5604
|
+
process.exitCode = 1;
|
|
5605
|
+
}
|
|
5606
|
+
|
|
5607
|
+
// src/cli/commands/tasks/delete.ts
|
|
5608
|
+
import * as readline2 from "node:readline";
|
|
5609
|
+
async function confirm(prompt2) {
|
|
5610
|
+
const rl = readline2.createInterface({
|
|
5611
|
+
input: process.stdin,
|
|
5612
|
+
output: process.stdout
|
|
5613
|
+
});
|
|
5614
|
+
return new Promise((resolve) => {
|
|
5615
|
+
rl.question(prompt2, (answer) => {
|
|
5616
|
+
rl.close();
|
|
5617
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
5618
|
+
});
|
|
5619
|
+
});
|
|
5620
|
+
}
|
|
5621
|
+
async function runTasksDelete(opts) {
|
|
5622
|
+
const context = resolveContext({
|
|
5623
|
+
rootDir: opts.rootDir,
|
|
5624
|
+
variant: opts.variant,
|
|
5625
|
+
team: opts.team
|
|
5626
|
+
});
|
|
5627
|
+
if (context.locations.length === 0) {
|
|
5628
|
+
console.error("No task locations found. Check variant and team settings.");
|
|
5629
|
+
process.exitCode = 1;
|
|
5630
|
+
return;
|
|
5631
|
+
}
|
|
5632
|
+
for (const location of context.locations) {
|
|
5633
|
+
const task = loadTask(location.tasksDir, opts.taskId);
|
|
5634
|
+
if (!task) continue;
|
|
5635
|
+
if (!opts.force) {
|
|
5636
|
+
const confirmed = await confirm(`Delete task #${task.id} "${task.subject}"? [y/N] `);
|
|
5637
|
+
if (!confirmed) {
|
|
5638
|
+
console.log("Cancelled.");
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
const deleted = deleteTask(location.tasksDir, opts.taskId);
|
|
5643
|
+
if (deleted) {
|
|
5644
|
+
if (opts.json) {
|
|
5645
|
+
console.log(JSON.stringify({ deleted: true, taskId: opts.taskId }));
|
|
5646
|
+
} else {
|
|
5647
|
+
console.log(`Deleted task #${opts.taskId}`);
|
|
5648
|
+
}
|
|
5649
|
+
} else {
|
|
5650
|
+
console.error(`Failed to delete task #${opts.taskId}`);
|
|
5651
|
+
process.exitCode = 1;
|
|
5652
|
+
}
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5655
|
+
console.error(`Task #${opts.taskId} not found.`);
|
|
5656
|
+
process.exitCode = 1;
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// src/cli/commands/tasks/clean.ts
|
|
5660
|
+
import fs17 from "node:fs";
|
|
5661
|
+
import path27 from "node:path";
|
|
5662
|
+
import * as readline3 from "node:readline";
|
|
5663
|
+
async function confirm2(prompt2) {
|
|
5664
|
+
const rl = readline3.createInterface({
|
|
5665
|
+
input: process.stdin,
|
|
5666
|
+
output: process.stdout
|
|
5667
|
+
});
|
|
5668
|
+
return new Promise((resolve) => {
|
|
5669
|
+
rl.question(prompt2, (answer) => {
|
|
5670
|
+
rl.close();
|
|
5671
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
5672
|
+
});
|
|
5673
|
+
});
|
|
5674
|
+
}
|
|
5675
|
+
function getTaskAge(tasksDir, taskId) {
|
|
5676
|
+
const taskPath = path27.join(tasksDir, `${taskId}.json`);
|
|
5677
|
+
try {
|
|
5678
|
+
const stats = fs17.statSync(taskPath);
|
|
5679
|
+
const now = Date.now();
|
|
5680
|
+
const mtime = stats.mtime.getTime();
|
|
5681
|
+
return Math.floor((now - mtime) / (1e3 * 60 * 60 * 24));
|
|
5682
|
+
} catch {
|
|
5683
|
+
return null;
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
function filterTasksForClean(tasks, tasksDir, opts) {
|
|
5687
|
+
let candidates = [...tasks];
|
|
5688
|
+
if (opts.resolved) {
|
|
5689
|
+
candidates = candidates.filter((t) => t.status === "resolved");
|
|
5690
|
+
}
|
|
5691
|
+
if (opts.olderThan !== void 0) {
|
|
5692
|
+
candidates = candidates.filter((t) => {
|
|
5693
|
+
const age = getTaskAge(tasksDir, t.id);
|
|
5694
|
+
return age !== null && age >= opts.olderThan;
|
|
5695
|
+
});
|
|
5696
|
+
}
|
|
5697
|
+
return candidates;
|
|
5698
|
+
}
|
|
5699
|
+
async function runTasksClean(opts) {
|
|
5700
|
+
const context = resolveContext({
|
|
5701
|
+
rootDir: opts.rootDir,
|
|
5702
|
+
variant: opts.variant,
|
|
5703
|
+
team: opts.team,
|
|
5704
|
+
allVariants: opts.allVariants,
|
|
5705
|
+
allTeams: opts.allTeams
|
|
5706
|
+
});
|
|
5707
|
+
if (context.locations.length === 0) {
|
|
5708
|
+
console.log("No task locations found. Check variant and team settings.");
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
if (!opts.resolved && opts.olderThan === void 0) {
|
|
5712
|
+
console.error("Error: Specify at least one filter (--resolved or --older-than).");
|
|
5713
|
+
process.exitCode = 1;
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
const results = [];
|
|
5717
|
+
let totalToDelete = 0;
|
|
5718
|
+
for (const location of context.locations) {
|
|
5719
|
+
const tasks = loadAllTasks(location.tasksDir);
|
|
5720
|
+
const toDelete = filterTasksForClean(tasks, location.tasksDir, opts);
|
|
5721
|
+
totalToDelete += toDelete.length;
|
|
5722
|
+
results.push({
|
|
5723
|
+
location,
|
|
5724
|
+
deleted: toDelete.map((t) => t.id),
|
|
5725
|
+
dryRun: opts.dryRun || false
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
if (totalToDelete === 0) {
|
|
5729
|
+
console.log("No tasks match the cleanup criteria.");
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
console.log(formatCleanResults(results));
|
|
5733
|
+
if (opts.dryRun) {
|
|
5734
|
+
console.log(`
|
|
5735
|
+
Dry run: ${totalToDelete} tasks would be deleted.`);
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
if (!opts.force) {
|
|
5739
|
+
const confirmed = await confirm2(`
|
|
5740
|
+
Delete ${totalToDelete} tasks? [y/N] `);
|
|
5741
|
+
if (!confirmed) {
|
|
5742
|
+
console.log("Cancelled.");
|
|
5743
|
+
return;
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
let deletedCount = 0;
|
|
5747
|
+
for (const result of results) {
|
|
5748
|
+
for (const taskId of result.deleted) {
|
|
5749
|
+
if (deleteTask(result.location.tasksDir, taskId)) {
|
|
5750
|
+
deletedCount++;
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
if (opts.json) {
|
|
5755
|
+
console.log(
|
|
5756
|
+
JSON.stringify({
|
|
5757
|
+
deleted: deletedCount,
|
|
5758
|
+
locations: results.map((r) => ({
|
|
5759
|
+
variant: r.location.variant,
|
|
5760
|
+
team: r.location.team,
|
|
5761
|
+
taskIds: r.deleted
|
|
5762
|
+
}))
|
|
5763
|
+
})
|
|
5764
|
+
);
|
|
5765
|
+
} else {
|
|
5766
|
+
console.log(`
|
|
5767
|
+
Deleted ${deletedCount} tasks.`);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
// src/cli/commands/tasks/graph.ts
|
|
5772
|
+
function buildDependencyLine(task, allTasks, depth, visited) {
|
|
5773
|
+
const lines = [];
|
|
5774
|
+
const indent = " ".repeat(depth);
|
|
5775
|
+
const prefix = depth === 0 ? "" : "\u2514\u2500 ";
|
|
5776
|
+
if (visited.has(task.id)) {
|
|
5777
|
+
lines.push(`${indent}${prefix}#${task.id} (circular ref)`);
|
|
5778
|
+
return lines;
|
|
5779
|
+
}
|
|
5780
|
+
visited.add(task.id);
|
|
5781
|
+
const statusIcon = task.status === "resolved" ? "\u2713" : isBlocked(task, allTasks) ? "\u25CF" : "\u25CB";
|
|
5782
|
+
lines.push(`${indent}${prefix}[${statusIcon}] #${task.id}: ${task.subject.slice(0, 50)}`);
|
|
5783
|
+
const blockedTasks = allTasks.filter((t) => t.blockedBy.includes(task.id));
|
|
5784
|
+
for (const blocked of blockedTasks) {
|
|
5785
|
+
lines.push(...buildDependencyLine(blocked, allTasks, depth + 1, new Set(visited)));
|
|
5786
|
+
}
|
|
5787
|
+
return lines;
|
|
5788
|
+
}
|
|
5789
|
+
function formatTaskGraph(tasks, variant, team) {
|
|
5790
|
+
const lines = [];
|
|
5791
|
+
lines.push(`TASK DEPENDENCY GRAPH (${variant} / ${team})`);
|
|
5792
|
+
lines.push("\u2550".repeat(60));
|
|
5793
|
+
lines.push("");
|
|
5794
|
+
lines.push("Legend: [\u2713] resolved [\u25CB] open [\u25CF] blocked");
|
|
5795
|
+
lines.push("");
|
|
5796
|
+
const roots = tasks.filter((t) => t.blockedBy.length === 0);
|
|
5797
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5798
|
+
for (const root of roots) {
|
|
5799
|
+
if (!visited.has(root.id)) {
|
|
5800
|
+
const treeLines = buildDependencyLine(root, tasks, 0, /* @__PURE__ */ new Set());
|
|
5801
|
+
lines.push(...treeLines);
|
|
5802
|
+
lines.push("");
|
|
5803
|
+
for (const line of treeLines) {
|
|
5804
|
+
const match = line.match(/#(\d+)/);
|
|
5805
|
+
if (match) visited.add(match[1]);
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
const orphans = tasks.filter((t) => !visited.has(t.id) && t.blockedBy.length > 0);
|
|
5810
|
+
if (orphans.length > 0) {
|
|
5811
|
+
lines.push("\u2500".repeat(40));
|
|
5812
|
+
lines.push("Orphan tasks (blockedBy non-existent tasks):");
|
|
5813
|
+
for (const task of orphans) {
|
|
5814
|
+
const statusIcon = task.status === "resolved" ? "\u2713" : "\u25CB";
|
|
5815
|
+
lines.push(` [${statusIcon}] #${task.id}: ${task.subject.slice(0, 50)}`);
|
|
5816
|
+
lines.push(` blockedBy: ${task.blockedBy.join(", ")}`);
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
lines.push("");
|
|
5820
|
+
lines.push("\u2500".repeat(60));
|
|
5821
|
+
const open = tasks.filter((t) => t.status === "open");
|
|
5822
|
+
const blocked = open.filter((t) => isBlocked(t, tasks));
|
|
5823
|
+
const ready = open.filter((t) => !isBlocked(t, tasks));
|
|
5824
|
+
lines.push(`Total: ${tasks.length} | Open: ${open.length} | Ready: ${ready.length} | Blocked: ${blocked.length}`);
|
|
5825
|
+
return lines.join("\n");
|
|
5826
|
+
}
|
|
5827
|
+
function runTasksGraph(opts) {
|
|
5828
|
+
const context = resolveContext({
|
|
5829
|
+
rootDir: opts.rootDir,
|
|
5830
|
+
variant: opts.variant,
|
|
5831
|
+
team: opts.team
|
|
5832
|
+
});
|
|
5833
|
+
if (context.locations.length === 0) {
|
|
5834
|
+
console.log("No task locations found. Check variant and team settings.");
|
|
5835
|
+
return;
|
|
5836
|
+
}
|
|
5837
|
+
const location = context.locations[0];
|
|
5838
|
+
const tasks = loadAllTasks(location.tasksDir);
|
|
5839
|
+
if (tasks.length === 0) {
|
|
5840
|
+
console.log(`No tasks found in ${location.variant} / ${location.team}`);
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
console.log(formatTaskGraph(tasks, location.variant, location.team));
|
|
5844
|
+
}
|
|
5845
|
+
|
|
5846
|
+
// src/cli/commands/tasks/archive.ts
|
|
5847
|
+
import fs18 from "node:fs";
|
|
5848
|
+
import path28 from "node:path";
|
|
5849
|
+
function getArchiveDir(tasksDir) {
|
|
5850
|
+
return path28.join(path28.dirname(tasksDir), "archive", path28.basename(tasksDir));
|
|
5851
|
+
}
|
|
5852
|
+
function archiveTask(tasksDir, task) {
|
|
5853
|
+
const archiveDir = getArchiveDir(tasksDir);
|
|
5854
|
+
fs18.mkdirSync(archiveDir, { recursive: true });
|
|
5855
|
+
const archivedTask = {
|
|
5856
|
+
...task,
|
|
5857
|
+
archivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5858
|
+
};
|
|
5859
|
+
const archivePath = path28.join(archiveDir, `${task.id}.json`);
|
|
5860
|
+
writeJson(archivePath, archivedTask);
|
|
5861
|
+
return deleteTask(tasksDir, task.id);
|
|
5862
|
+
}
|
|
5863
|
+
async function runTasksArchive(opts) {
|
|
5864
|
+
const context = resolveContext({
|
|
5865
|
+
rootDir: opts.rootDir,
|
|
5866
|
+
variant: opts.variant,
|
|
5867
|
+
team: opts.team
|
|
5868
|
+
});
|
|
5869
|
+
if (context.locations.length === 0) {
|
|
5870
|
+
console.error("No task locations found. Check variant and team settings.");
|
|
5871
|
+
process.exitCode = 1;
|
|
5872
|
+
return;
|
|
5873
|
+
}
|
|
5874
|
+
const location = context.locations[0];
|
|
5875
|
+
if (opts.taskId) {
|
|
5876
|
+
const task = loadTask(location.tasksDir, opts.taskId);
|
|
5877
|
+
if (!task) {
|
|
5878
|
+
console.error(`Task #${opts.taskId} not found.`);
|
|
5879
|
+
process.exitCode = 1;
|
|
5880
|
+
return;
|
|
5881
|
+
}
|
|
5882
|
+
if (opts.dryRun) {
|
|
5883
|
+
console.log(`Would archive task #${task.id}: ${task.subject}`);
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
if (archiveTask(location.tasksDir, task)) {
|
|
5887
|
+
if (opts.json) {
|
|
5888
|
+
console.log(JSON.stringify({ archived: [task.id] }));
|
|
5889
|
+
} else {
|
|
5890
|
+
console.log(`Archived task #${task.id}: ${task.subject}`);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
if (opts.resolved) {
|
|
5896
|
+
const tasks = loadAllTasks(location.tasksDir);
|
|
5897
|
+
const resolvedTasks = tasks.filter((t) => t.status === "resolved");
|
|
5898
|
+
if (resolvedTasks.length === 0) {
|
|
5899
|
+
console.log("No resolved tasks to archive.");
|
|
5900
|
+
return;
|
|
5901
|
+
}
|
|
5902
|
+
if (opts.dryRun) {
|
|
5903
|
+
console.log(`Would archive ${resolvedTasks.length} resolved tasks:`);
|
|
5904
|
+
for (const task of resolvedTasks.slice(0, 10)) {
|
|
5905
|
+
console.log(` #${task.id}: ${task.subject.slice(0, 50)}`);
|
|
5906
|
+
}
|
|
5907
|
+
if (resolvedTasks.length > 10) {
|
|
5908
|
+
console.log(` ... and ${resolvedTasks.length - 10} more`);
|
|
5909
|
+
}
|
|
5910
|
+
return;
|
|
5911
|
+
}
|
|
5912
|
+
const archived = [];
|
|
5913
|
+
for (const task of resolvedTasks) {
|
|
5914
|
+
if (archiveTask(location.tasksDir, task)) {
|
|
5915
|
+
archived.push(task.id);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
if (opts.json) {
|
|
5919
|
+
console.log(JSON.stringify({ archived }));
|
|
5920
|
+
} else {
|
|
5921
|
+
console.log(`Archived ${archived.length} tasks to:`);
|
|
5922
|
+
console.log(` ${getArchiveDir(location.tasksDir)}`);
|
|
5923
|
+
}
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
console.error("Specify --resolved to archive all resolved tasks, or provide a task ID.");
|
|
5927
|
+
process.exitCode = 1;
|
|
5928
|
+
}
|
|
5929
|
+
|
|
5930
|
+
// src/cli/commands/tasks.ts
|
|
5931
|
+
function parseIds(value) {
|
|
5932
|
+
if (!value) return void 0;
|
|
5933
|
+
return value.split(",").map((s) => s.trim());
|
|
5934
|
+
}
|
|
5935
|
+
function showTasksHelp() {
|
|
5936
|
+
console.log(`
|
|
5937
|
+
cc-mirror tasks - Manage team tasks
|
|
5938
|
+
|
|
5939
|
+
USAGE:
|
|
5940
|
+
cc-mirror tasks [operation] [id] [options]
|
|
5941
|
+
|
|
5942
|
+
OPERATIONS:
|
|
5943
|
+
list List tasks (default if no operation specified)
|
|
5944
|
+
show <id> Show detailed task info
|
|
5945
|
+
create Create a new task
|
|
5946
|
+
update <id> Update an existing task
|
|
5947
|
+
delete <id> Delete a task (permanent)
|
|
5948
|
+
archive [id] Move task(s) to archive (preserves history)
|
|
5949
|
+
clean Bulk delete tasks (permanent)
|
|
5950
|
+
graph Show task dependency graph
|
|
5951
|
+
|
|
5952
|
+
GLOBAL OPTIONS:
|
|
5953
|
+
--variant <name> Target variant (auto-detects if omitted)
|
|
5954
|
+
--all-variants Show tasks across all variants
|
|
5955
|
+
--team <name> Target team name
|
|
5956
|
+
--all Show all teams in variant(s)
|
|
5957
|
+
--json Output as JSON
|
|
5958
|
+
--help Show this help
|
|
5959
|
+
|
|
5960
|
+
LIST OPTIONS:
|
|
5961
|
+
--status <s> Filter: open, resolved, all (default: open)
|
|
5962
|
+
--blocked Show only blocked tasks
|
|
5963
|
+
--blocking Show only tasks blocking others
|
|
5964
|
+
--owner <id> Filter by owner
|
|
5965
|
+
--limit <n> Limit results (default: 50)
|
|
5966
|
+
|
|
5967
|
+
CREATE OPTIONS:
|
|
5968
|
+
--subject <text> Task subject (required)
|
|
5969
|
+
--description <t> Task description
|
|
5970
|
+
--owner <id> Assign owner
|
|
5971
|
+
--blocks <ids> Comma-separated task IDs this task blocks
|
|
5972
|
+
--blocked-by <ids> Comma-separated task IDs that block this task
|
|
5973
|
+
|
|
5974
|
+
UPDATE OPTIONS:
|
|
5975
|
+
--subject <text> Update subject
|
|
5976
|
+
--description <t> Update description
|
|
5977
|
+
--status <s> Set status: open or resolved
|
|
5978
|
+
--owner <id> Set owner (empty string to unassign)
|
|
5979
|
+
--add-blocks <ids> Add blocking relationships
|
|
5980
|
+
--remove-blocks <ids> Remove blocking relationships
|
|
5981
|
+
--add-blocked-by <ids> Add blocked-by relationships
|
|
5982
|
+
--remove-blocked-by <ids> Remove blocked-by relationships
|
|
5983
|
+
--add-comment <text> Add a comment
|
|
5984
|
+
--comment-author <id> Comment author (default: cli)
|
|
5985
|
+
|
|
5986
|
+
CLEAN OPTIONS:
|
|
5987
|
+
--resolved Delete all resolved tasks
|
|
5988
|
+
--older-than <n> Delete tasks older than N days
|
|
5989
|
+
--dry-run Preview without deleting
|
|
5990
|
+
--force Skip confirmation
|
|
5991
|
+
|
|
5992
|
+
EXAMPLES:
|
|
5993
|
+
cc-mirror tasks # List open tasks
|
|
5994
|
+
cc-mirror tasks --status all # List all tasks
|
|
5995
|
+
cc-mirror tasks show 5 # Show task #5
|
|
5996
|
+
cc-mirror tasks create --subject "Fix bug" --description "..."
|
|
5997
|
+
cc-mirror tasks update 5 --status resolved
|
|
5998
|
+
cc-mirror tasks delete 5 --force
|
|
5999
|
+
cc-mirror tasks clean --resolved --dry-run
|
|
6000
|
+
`);
|
|
6001
|
+
}
|
|
6002
|
+
async function runTasksCommand({ opts }) {
|
|
6003
|
+
const rootDir = opts.root || DEFAULT_ROOT;
|
|
6004
|
+
const positional = opts._ || [];
|
|
6005
|
+
if (opts.help || opts.h) {
|
|
6006
|
+
showTasksHelp();
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
const operation = positional[0];
|
|
6010
|
+
const taskId = positional[1];
|
|
6011
|
+
const variant = opts.variant;
|
|
6012
|
+
const team = opts.team;
|
|
6013
|
+
const allVariants = Boolean(opts["all-variants"]);
|
|
6014
|
+
const allTeams = Boolean(opts.all);
|
|
6015
|
+
const json = Boolean(opts.json);
|
|
6016
|
+
switch (operation) {
|
|
6017
|
+
case "show": {
|
|
6018
|
+
if (!taskId) {
|
|
6019
|
+
console.error("Error: Task ID required. Usage: cc-mirror tasks show <id>");
|
|
6020
|
+
process.exitCode = 1;
|
|
6021
|
+
return;
|
|
6022
|
+
}
|
|
6023
|
+
runTasksShow({ rootDir, taskId, variant, team, json });
|
|
6024
|
+
break;
|
|
6025
|
+
}
|
|
6026
|
+
case "create": {
|
|
6027
|
+
const subject = opts.subject;
|
|
6028
|
+
if (!subject) {
|
|
6029
|
+
console.error("Error: --subject required for create.");
|
|
6030
|
+
process.exitCode = 1;
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
runTasksCreate({
|
|
6034
|
+
rootDir,
|
|
6035
|
+
subject,
|
|
6036
|
+
description: opts.description,
|
|
6037
|
+
variant,
|
|
6038
|
+
team,
|
|
6039
|
+
owner: opts.owner,
|
|
6040
|
+
blocks: parseIds(opts.blocks),
|
|
6041
|
+
blockedBy: parseIds(opts["blocked-by"]),
|
|
6042
|
+
json
|
|
6043
|
+
});
|
|
6044
|
+
break;
|
|
6045
|
+
}
|
|
6046
|
+
case "update": {
|
|
6047
|
+
if (!taskId) {
|
|
6048
|
+
console.error("Error: Task ID required. Usage: cc-mirror tasks update <id>");
|
|
6049
|
+
process.exitCode = 1;
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
runTasksUpdate({
|
|
6053
|
+
rootDir,
|
|
6054
|
+
taskId,
|
|
6055
|
+
variant,
|
|
6056
|
+
team,
|
|
6057
|
+
subject: opts.subject,
|
|
6058
|
+
description: opts.description,
|
|
6059
|
+
status: opts.status,
|
|
6060
|
+
owner: opts.owner,
|
|
6061
|
+
addBlocks: parseIds(opts["add-blocks"]),
|
|
6062
|
+
removeBlocks: parseIds(opts["remove-blocks"]),
|
|
6063
|
+
addBlockedBy: parseIds(opts["add-blocked-by"]),
|
|
6064
|
+
removeBlockedBy: parseIds(opts["remove-blocked-by"]),
|
|
6065
|
+
addComment: opts["add-comment"],
|
|
6066
|
+
commentAuthor: opts["comment-author"],
|
|
6067
|
+
json
|
|
6068
|
+
});
|
|
6069
|
+
break;
|
|
6070
|
+
}
|
|
6071
|
+
case "delete": {
|
|
6072
|
+
if (!taskId) {
|
|
6073
|
+
console.error("Error: Task ID required. Usage: cc-mirror tasks delete <id>");
|
|
6074
|
+
process.exitCode = 1;
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
6077
|
+
await runTasksDelete({
|
|
6078
|
+
rootDir,
|
|
6079
|
+
taskId,
|
|
6080
|
+
variant,
|
|
6081
|
+
team,
|
|
6082
|
+
force: Boolean(opts.force),
|
|
6083
|
+
json
|
|
6084
|
+
});
|
|
6085
|
+
break;
|
|
6086
|
+
}
|
|
6087
|
+
case "clean": {
|
|
6088
|
+
await runTasksClean({
|
|
6089
|
+
rootDir,
|
|
6090
|
+
variant,
|
|
6091
|
+
team,
|
|
6092
|
+
allVariants,
|
|
6093
|
+
allTeams,
|
|
6094
|
+
resolved: Boolean(opts.resolved),
|
|
6095
|
+
olderThan: opts["older-than"] !== void 0 ? Number(opts["older-than"]) : void 0,
|
|
6096
|
+
dryRun: Boolean(opts["dry-run"]),
|
|
6097
|
+
force: Boolean(opts.force),
|
|
6098
|
+
json
|
|
6099
|
+
});
|
|
6100
|
+
break;
|
|
6101
|
+
}
|
|
6102
|
+
case "graph": {
|
|
6103
|
+
runTasksGraph({ rootDir, variant, team });
|
|
6104
|
+
break;
|
|
6105
|
+
}
|
|
6106
|
+
case "archive": {
|
|
6107
|
+
await runTasksArchive({
|
|
6108
|
+
rootDir,
|
|
6109
|
+
variant,
|
|
6110
|
+
team,
|
|
6111
|
+
taskId,
|
|
6112
|
+
resolved: Boolean(opts.resolved),
|
|
6113
|
+
dryRun: Boolean(opts["dry-run"]),
|
|
6114
|
+
force: Boolean(opts.force),
|
|
6115
|
+
json
|
|
6116
|
+
});
|
|
6117
|
+
break;
|
|
6118
|
+
}
|
|
6119
|
+
case "list":
|
|
6120
|
+
case void 0: {
|
|
6121
|
+
runTasksList({
|
|
6122
|
+
rootDir,
|
|
6123
|
+
variant,
|
|
6124
|
+
team,
|
|
6125
|
+
allVariants,
|
|
6126
|
+
allTeams,
|
|
6127
|
+
status: opts.status || "open",
|
|
6128
|
+
blocked: opts.blocked !== void 0 ? Boolean(opts.blocked) : void 0,
|
|
6129
|
+
blocking: opts.blocking !== void 0 ? Boolean(opts.blocking) : void 0,
|
|
6130
|
+
owner: opts.owner,
|
|
6131
|
+
limit: opts.limit !== void 0 ? Number(opts.limit) : 50,
|
|
6132
|
+
json
|
|
6133
|
+
});
|
|
6134
|
+
break;
|
|
6135
|
+
}
|
|
6136
|
+
default:
|
|
6137
|
+
console.error(`Unknown operation: ${operation}`);
|
|
6138
|
+
console.error('Run "cc-mirror tasks --help" for usage.');
|
|
6139
|
+
process.exitCode = 1;
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
|
|
5031
6143
|
// src/cli/index.ts
|
|
5032
6144
|
var main = async () => {
|
|
5033
6145
|
const argv = process.argv.slice(2);
|
|
@@ -5039,7 +6151,8 @@ var main = async () => {
|
|
|
5039
6151
|
const opts = parseArgs(argv);
|
|
5040
6152
|
const quickMode = cmd === "quick" || Boolean(opts.quick || opts.simple);
|
|
5041
6153
|
if (cmd === "quick") cmd = "create";
|
|
5042
|
-
|
|
6154
|
+
const commandsWithOwnHelp = ["tasks"];
|
|
6155
|
+
if (cmd === "help" || cmd === "--help" || opts.help && !commandsWithOwnHelp.includes(cmd)) {
|
|
5043
6156
|
printHelp();
|
|
5044
6157
|
return;
|
|
5045
6158
|
}
|
|
@@ -5070,6 +6183,9 @@ var main = async () => {
|
|
|
5070
6183
|
case "create":
|
|
5071
6184
|
await runCreateCommand({ opts, quickMode });
|
|
5072
6185
|
break;
|
|
6186
|
+
case "tasks":
|
|
6187
|
+
await runTasksCommand({ opts });
|
|
6188
|
+
break;
|
|
5073
6189
|
default:
|
|
5074
6190
|
printHelp();
|
|
5075
6191
|
}
|