taskplane 0.1.12 → 0.1.13
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 +188 -180
- package/bin/taskplane.mjs +1047 -1007
- package/package.json +1 -1
package/bin/taskplane.mjs
CHANGED
|
@@ -1,1007 +1,1047 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Taskplane CLI — Project scaffolding, diagnostics, uninstall, and dashboard launcher.
|
|
5
|
-
*
|
|
6
|
-
* This CLI handles what the pi package system cannot: project-local config
|
|
7
|
-
* scaffolding, installation health checks, and dashboard management.
|
|
8
|
-
*
|
|
9
|
-
* Extensions, skills, and themes are delivered via `pi install npm:taskplane`
|
|
10
|
-
* and auto-discovered by pi. This CLI is for everything else.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import readline from "node:readline";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { execSync, spawn } from "node:child_process";
|
|
18
|
-
|
|
19
|
-
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
-
const __dirname = path.dirname(__filename);
|
|
23
|
-
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
24
|
-
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
|
|
25
|
-
const DASHBOARD_SERVER = path.join(PACKAGE_ROOT, "dashboard", "server.cjs");
|
|
26
|
-
|
|
27
|
-
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
const c = {
|
|
30
|
-
reset: "\x1b[0m",
|
|
31
|
-
bold: "\x1b[1m",
|
|
32
|
-
dim: "\x1b[2m",
|
|
33
|
-
red: "\x1b[31m",
|
|
34
|
-
green: "\x1b[32m",
|
|
35
|
-
yellow: "\x1b[33m",
|
|
36
|
-
blue: "\x1b[34m",
|
|
37
|
-
cyan: "\x1b[36m",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const OK = `${c.green}✅${c.reset}`;
|
|
41
|
-
const WARN = `${c.yellow}⚠️${c.reset}`;
|
|
42
|
-
const FAIL = `${c.red}❌${c.reset}`;
|
|
43
|
-
const INFO = `${c.cyan}ℹ${c.reset}`;
|
|
44
|
-
|
|
45
|
-
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
function die(msg) {
|
|
48
|
-
console.error(`${FAIL} ${msg}`);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function today() {
|
|
53
|
-
return new Date().toISOString().slice(0, 10);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function slugify(str) {
|
|
57
|
-
return str
|
|
58
|
-
.toLowerCase()
|
|
59
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
60
|
-
.replace(/^-|-$/g, "");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Prompt the user for input. Returns the answer or defaultValue. */
|
|
64
|
-
function ask(question, defaultValue) {
|
|
65
|
-
const rl = readline.createInterface({
|
|
66
|
-
input: process.stdin,
|
|
67
|
-
output: process.stdout,
|
|
68
|
-
});
|
|
69
|
-
const suffix = defaultValue != null ? ` ${c.dim}(${defaultValue})${c.reset}` : "";
|
|
70
|
-
return new Promise((resolve) => {
|
|
71
|
-
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
72
|
-
rl.close();
|
|
73
|
-
resolve(answer.trim() || defaultValue || "");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Prompt yes/no. Returns boolean. */
|
|
79
|
-
async function confirm(question, defaultYes = true) {
|
|
80
|
-
const hint = defaultYes ? "Y/n" : "y/N";
|
|
81
|
-
const answer = await ask(`${question} [${hint}]`);
|
|
82
|
-
if (!answer) return defaultYes;
|
|
83
|
-
return answer.toLowerCase().startsWith("y");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Read a YAML file if it exists, return null otherwise. */
|
|
87
|
-
function readYaml(filePath) {
|
|
88
|
-
try {
|
|
89
|
-
// Dynamic import of yaml — it's a dependency of the package
|
|
90
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
91
|
-
// Simple YAML value extraction (avoids requiring yaml at top level for fast startup)
|
|
92
|
-
return raw;
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Check if a command exists on PATH. */
|
|
99
|
-
function commandExists(cmd) {
|
|
100
|
-
try {
|
|
101
|
-
const which = process.platform === "win32" ? "where" : "which";
|
|
102
|
-
execSync(`${which} ${cmd}`, { stdio: "pipe" });
|
|
103
|
-
return true;
|
|
104
|
-
} catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Get command version string. */
|
|
110
|
-
function getVersion(cmd, flag = "--version") {
|
|
111
|
-
try {
|
|
112
|
-
return execSync(`${cmd} ${flag}`, { stdio: "pipe" }).toString().trim();
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Write a file, creating parent directories as needed. Optionally skip if exists. */
|
|
119
|
-
function writeFile(dest, content, { skipIfExists = false, label = "" } = {}) {
|
|
120
|
-
if (skipIfExists && fs.existsSync(dest)) {
|
|
121
|
-
if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
125
|
-
fs.writeFileSync(dest, content, "utf-8");
|
|
126
|
-
if (label) console.log(` ${c.green}create${c.reset} ${label}`);
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Copy a file from templates, creating parent dirs. */
|
|
131
|
-
function copyTemplate(src, dest, { skipIfExists = false, label = "" } = {}) {
|
|
132
|
-
if (skipIfExists && fs.existsSync(dest)) {
|
|
133
|
-
if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
const content = fs.readFileSync(src, "utf-8");
|
|
137
|
-
return writeFile(dest, content, { label });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Replace {{variables}} in template content. */
|
|
141
|
-
function interpolate(content, vars) {
|
|
142
|
-
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ─── Stack Detection ────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
function detectStack(projectRoot) {
|
|
148
|
-
const checks = [
|
|
149
|
-
{ file: "package.json", stack: "node", test: "npm test", build: "npm run build" },
|
|
150
|
-
{ file: "go.mod", stack: "go", test: "go test ./...", build: "go build ./..." },
|
|
151
|
-
{ file: "Cargo.toml", stack: "rust", test: "cargo test", build: "cargo build" },
|
|
152
|
-
{ file: "pyproject.toml", stack: "python", test: "pytest", build: "" },
|
|
153
|
-
{ file: "pom.xml", stack: "java-maven", test: "mvn test", build: "mvn package" },
|
|
154
|
-
{ file: "build.gradle", stack: "java-gradle", test: "gradle test", build: "gradle build" },
|
|
155
|
-
];
|
|
156
|
-
for (const { file, stack, test, build } of checks) {
|
|
157
|
-
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
158
|
-
return { stack, test, build };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return { stack: "unknown", test: "", build: "" };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ─── YAML Generation ────────────────────────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
function generateTaskRunnerYaml(vars) {
|
|
167
|
-
return `# ═══════════════════════════════════════════════════════════════════════
|
|
168
|
-
# Task Runner Configuration — ${vars.project_name}
|
|
169
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
170
|
-
#
|
|
171
|
-
# This file configures the /task command (task-runner extension).
|
|
172
|
-
# Edit freely — this file is owned by you, not the package.
|
|
173
|
-
|
|
174
|
-
# ── Task Areas ────────────────────────────────────────────────────────
|
|
175
|
-
# Define where tasks live. Each area has a folder path, ID prefix, and
|
|
176
|
-
# a CONTEXT.md file that provides domain context to agents.
|
|
177
|
-
|
|
178
|
-
task_areas:
|
|
179
|
-
${vars.default_area}:
|
|
180
|
-
path: "${vars.tasks_root}"
|
|
181
|
-
prefix: "${vars.default_prefix}"
|
|
182
|
-
context: "${vars.tasks_root}/CONTEXT.md"
|
|
183
|
-
|
|
184
|
-
# ── Reference Docs ────────────────────────────────────────────────────
|
|
185
|
-
# Docs that tasks can reference in their "Context to Read First" section.
|
|
186
|
-
# Add your project's architecture docs, API specs, etc.
|
|
187
|
-
|
|
188
|
-
reference_docs: {}
|
|
189
|
-
|
|
190
|
-
# ── Standards ─────────────────────────────────────────────────────────
|
|
191
|
-
# Coding standards and rules. Agents follow these during implementation.
|
|
192
|
-
|
|
193
|
-
standards: {}
|
|
194
|
-
|
|
195
|
-
# ── Testing ───────────────────────────────────────────────────────────
|
|
196
|
-
# Commands that agents run to verify their work.
|
|
197
|
-
|
|
198
|
-
testing:
|
|
199
|
-
commands:${vars.test_cmd ? `\n unit: "${vars.test_cmd}"` : ""}${vars.build_cmd ? `\n build: "${vars.build_cmd}"` : ""}
|
|
200
|
-
`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function generateOrchestratorYaml(vars) {
|
|
204
|
-
return `# ═══════════════════════════════════════════════════════════════════════
|
|
205
|
-
# Parallel Task Orchestrator Configuration — ${vars.project_name}
|
|
206
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
207
|
-
#
|
|
208
|
-
# This file configures the /orch commands (task-orchestrator extension).
|
|
209
|
-
# Edit freely — this file is owned by you, not the package.
|
|
210
|
-
|
|
211
|
-
orchestrator:
|
|
212
|
-
max_lanes: ${vars.max_lanes}
|
|
213
|
-
worktree_location: "subdirectory"
|
|
214
|
-
worktree_prefix: "${vars.worktree_prefix}"
|
|
215
|
-
integration_branch: "${vars.integration_branch}"
|
|
216
|
-
batch_id_format: "timestamp"
|
|
217
|
-
spawn_mode: "subprocess"
|
|
218
|
-
tmux_prefix: "${vars.tmux_prefix}"
|
|
219
|
-
|
|
220
|
-
dependencies:
|
|
221
|
-
source: "prompt"
|
|
222
|
-
cache: true
|
|
223
|
-
|
|
224
|
-
assignment:
|
|
225
|
-
strategy: "affinity-first"
|
|
226
|
-
size_weights:
|
|
227
|
-
S: 1
|
|
228
|
-
M: 2
|
|
229
|
-
L: 4
|
|
230
|
-
|
|
231
|
-
pre_warm:
|
|
232
|
-
auto_detect: false
|
|
233
|
-
commands: {}
|
|
234
|
-
always: []
|
|
235
|
-
|
|
236
|
-
merge:
|
|
237
|
-
model: ""
|
|
238
|
-
tools: "read,write,edit,bash,grep,find,ls"
|
|
239
|
-
verify: []
|
|
240
|
-
order: "fewest-files-first"
|
|
241
|
-
|
|
242
|
-
failure:
|
|
243
|
-
on_task_failure: "skip-dependents"
|
|
244
|
-
on_merge_failure: "pause"
|
|
245
|
-
stall_timeout: 30
|
|
246
|
-
max_worker_minutes: 30
|
|
247
|
-
abort_grace_period: 60
|
|
248
|
-
|
|
249
|
-
monitoring:
|
|
250
|
-
poll_interval: 5
|
|
251
|
-
`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
255
|
-
// COMMANDS
|
|
256
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
257
|
-
|
|
258
|
-
/** Auto-commit task files to git so they're available in orchestrator worktrees. */
|
|
259
|
-
async function autoCommitTaskFiles(projectRoot, tasksRoot) {
|
|
260
|
-
// Check if we're in a git repo
|
|
261
|
-
try {
|
|
262
|
-
execSync("git rev-parse --is-inside-work-tree", { cwd: projectRoot, stdio: "pipe" });
|
|
263
|
-
} catch {
|
|
264
|
-
// Not a git repo — skip silently
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Stage the tasks directory (not .pi/ — that's gitignored)
|
|
269
|
-
const tasksDir = path.join(projectRoot, tasksRoot);
|
|
270
|
-
if (!fs.existsSync(tasksDir)) return;
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
// Check if there's anything new to commit
|
|
274
|
-
execSync(`git add "${tasksRoot}"`, { cwd: projectRoot, stdio: "pipe" });
|
|
275
|
-
const status = execSync("git diff --cached --name-only", { cwd: projectRoot, stdio: "pipe" })
|
|
276
|
-
.toString()
|
|
277
|
-
.trim();
|
|
278
|
-
|
|
279
|
-
if (!status) return; // nothing staged
|
|
280
|
-
|
|
281
|
-
execSync('git commit -m "chore: initialize taskplane tasks"', {
|
|
282
|
-
cwd: projectRoot,
|
|
283
|
-
stdio: "pipe",
|
|
284
|
-
});
|
|
285
|
-
console.log(`\n ${c.green}git${c.reset} committed ${tasksRoot}/ to git`);
|
|
286
|
-
console.log(` ${c.dim}(orchestrator worktrees require committed files)${c.reset}`);
|
|
287
|
-
} catch (err) {
|
|
288
|
-
// Git commit failed — warn but don't block init
|
|
289
|
-
console.log(`\n ${WARN} Could not auto-commit task files to git.`);
|
|
290
|
-
console.log(` ${c.dim}Run manually before using /orch: git add ${tasksRoot} && git commit -m "add taskplane tasks"${c.reset}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function discoverTaskAreaPaths(projectRoot) {
|
|
295
|
-
const runnerPath = path.join(projectRoot, ".pi", "task-runner.yaml");
|
|
296
|
-
if (!fs.existsSync(runnerPath)) return [];
|
|
297
|
-
|
|
298
|
-
const raw = readYaml(runnerPath);
|
|
299
|
-
if (!raw) return [];
|
|
300
|
-
|
|
301
|
-
const lines = raw.split(/\r?\n/);
|
|
302
|
-
let inTaskAreas = false;
|
|
303
|
-
const paths = new Set();
|
|
304
|
-
|
|
305
|
-
for (const line of lines) {
|
|
306
|
-
const trimmed = line.trim();
|
|
307
|
-
|
|
308
|
-
if (!inTaskAreas) {
|
|
309
|
-
if (/^task_areas:\s*$/.test(trimmed)) {
|
|
310
|
-
inTaskAreas = true;
|
|
311
|
-
}
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// End of task_areas block when we hit next top-level key
|
|
316
|
-
if (/^[A-Za-z0-9_]+\s*:\s*$/.test(line)) {
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const m = line.match(/^\s{4}path:\s*["']?([^"'\n#]+)["']?\s*(?:#.*)?$/);
|
|
321
|
-
if (m?.[1]) {
|
|
322
|
-
paths.add(m[1].trim());
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return [...paths];
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function pruneEmptyDir(dirPath) {
|
|
330
|
-
try {
|
|
331
|
-
if (!fs.existsSync(dirPath)) return false;
|
|
332
|
-
if (fs.readdirSync(dirPath).length !== 0) return false;
|
|
333
|
-
fs.rmdirSync(dirPath);
|
|
334
|
-
return true;
|
|
335
|
-
} catch {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function listExampleTaskTemplates() {
|
|
341
|
-
const tasksTemplatesDir = path.join(TEMPLATES_DIR, "tasks");
|
|
342
|
-
try {
|
|
343
|
-
return fs.readdirSync(tasksTemplatesDir, { withFileTypes: true })
|
|
344
|
-
.filter((entry) => entry.isDirectory() && /^EXAMPLE-\d+/i.test(entry.name))
|
|
345
|
-
.map((entry) => entry.name)
|
|
346
|
-
.sort();
|
|
347
|
-
} catch {
|
|
348
|
-
return [];
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function cmdUninstall(args) {
|
|
353
|
-
const projectRoot = process.cwd();
|
|
354
|
-
const dryRun = args.includes("--dry-run");
|
|
355
|
-
const yes = args.includes("--yes") || args.includes("-y");
|
|
356
|
-
const removePackage = args.includes("--package") || args.includes("--all") || args.includes("--package-only");
|
|
357
|
-
const packageOnly = args.includes("--package-only");
|
|
358
|
-
const removeProject = !packageOnly;
|
|
359
|
-
const removeTasks = removeProject && (args.includes("--remove-tasks") || args.includes("--all"));
|
|
360
|
-
const local = args.includes("--local");
|
|
361
|
-
const global = args.includes("--global");
|
|
362
|
-
|
|
363
|
-
if (local && global) {
|
|
364
|
-
die("Choose either --local or --global, not both.");
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
console.log(`\n${c.bold}Taskplane Uninstall${c.reset}\n`);
|
|
368
|
-
|
|
369
|
-
const managedFiles = [
|
|
370
|
-
".pi/task-runner.yaml",
|
|
371
|
-
".pi/task-orchestrator.yaml",
|
|
372
|
-
".pi/taskplane.json",
|
|
373
|
-
".pi/agents/task-worker.md",
|
|
374
|
-
".pi/agents/task-reviewer.md",
|
|
375
|
-
".pi/agents/task-merger.md",
|
|
376
|
-
".pi/batch-state.json",
|
|
377
|
-
".pi/batch-history.json",
|
|
378
|
-
".pi/orch-abort-signal",
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
const sidecarPrefixes = [
|
|
382
|
-
"lane-state-",
|
|
383
|
-
"worker-conversation-",
|
|
384
|
-
"merge-result-",
|
|
385
|
-
"merge-request-",
|
|
386
|
-
];
|
|
387
|
-
|
|
388
|
-
const filesToDelete = managedFiles
|
|
389
|
-
.map(rel => ({ rel, abs: path.join(projectRoot, rel) }))
|
|
390
|
-
.filter(({ abs }) => fs.existsSync(abs));
|
|
391
|
-
|
|
392
|
-
const piDir = path.join(projectRoot, ".pi");
|
|
393
|
-
const sidecarsToDelete = fs.existsSync(piDir)
|
|
394
|
-
? fs.readdirSync(piDir)
|
|
395
|
-
.filter(name => sidecarPrefixes.some(prefix => name.startsWith(prefix)))
|
|
396
|
-
.map(name => ({ rel: path.join(".pi", name), abs: path.join(piDir, name) }))
|
|
397
|
-
: [];
|
|
398
|
-
|
|
399
|
-
let taskDirsToDelete = [];
|
|
400
|
-
if (removeTasks) {
|
|
401
|
-
const areaPaths = discoverTaskAreaPaths(projectRoot);
|
|
402
|
-
const rootPrefix = path.resolve(projectRoot) + path.sep;
|
|
403
|
-
taskDirsToDelete = areaPaths
|
|
404
|
-
.map(rel => ({ rel, abs: path.resolve(projectRoot, rel) }))
|
|
405
|
-
.filter(({ abs }) => abs.startsWith(rootPrefix) && fs.existsSync(abs));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const inferredInstallType = /[\\/]\.pi[\\/]/.test(PACKAGE_ROOT) ? "local" : "global";
|
|
409
|
-
const packageScope = local ? "local" : global ? "global" : inferredInstallType;
|
|
410
|
-
const piRemoveCmd = packageScope === "local"
|
|
411
|
-
? "pi remove -l npm:taskplane"
|
|
412
|
-
: "pi remove npm:taskplane";
|
|
413
|
-
|
|
414
|
-
if (!removeProject && !removePackage) {
|
|
415
|
-
console.log(` ${WARN} Nothing to do. Use one of:`);
|
|
416
|
-
console.log(` ${c.cyan}taskplane uninstall${c.reset} # remove project-scaffolded files`);
|
|
417
|
-
console.log(` ${c.cyan}taskplane uninstall --package${c.reset} # remove installed package via pi`);
|
|
418
|
-
console.log();
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (removeProject) {
|
|
423
|
-
console.log(`${c.bold}Project cleanup:${c.reset}`);
|
|
424
|
-
if (filesToDelete.length === 0 && sidecarsToDelete.length === 0 && taskDirsToDelete.length === 0) {
|
|
425
|
-
console.log(` ${c.dim}No Taskplane-managed project files found.${c.reset}`);
|
|
426
|
-
}
|
|
427
|
-
for (const f of filesToDelete) console.log(` - remove ${f.rel}`);
|
|
428
|
-
for (const f of sidecarsToDelete) console.log(` - remove ${f.rel}`);
|
|
429
|
-
for (const d of taskDirsToDelete) console.log(` - remove dir ${d.rel}`);
|
|
430
|
-
if (removeTasks && taskDirsToDelete.length === 0) {
|
|
431
|
-
console.log(` ${c.dim}No task area directories found from .pi/task-runner.yaml.${c.reset}`);
|
|
432
|
-
}
|
|
433
|
-
if (!removeTasks) {
|
|
434
|
-
console.log(` ${c.dim}Task directories are preserved by default (use --remove-tasks to delete them).${c.reset}`);
|
|
435
|
-
}
|
|
436
|
-
console.log();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (removePackage) {
|
|
440
|
-
console.log(`${c.bold}Package cleanup:${c.reset}`);
|
|
441
|
-
console.log(` - run ${piRemoveCmd}`);
|
|
442
|
-
console.log(` ${c.dim}(removes extensions, skills, and dashboard files from this install scope)${c.reset}`);
|
|
443
|
-
console.log();
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (dryRun) {
|
|
447
|
-
console.log(`${INFO} Dry run complete. No files were changed.\n`);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!yes) {
|
|
452
|
-
const proceed = await confirm("Proceed with uninstall?", false);
|
|
453
|
-
if (!proceed) {
|
|
454
|
-
console.log(" Aborted.");
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (removeTasks) {
|
|
458
|
-
const taskConfirm = await confirm("This will delete task area directories recursively. Continue?", false);
|
|
459
|
-
if (!taskConfirm) {
|
|
460
|
-
console.log(" Aborted.");
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let removedCount = 0;
|
|
467
|
-
let failedCount = 0;
|
|
468
|
-
|
|
469
|
-
if (removeProject) {
|
|
470
|
-
for (const item of [...filesToDelete, ...sidecarsToDelete]) {
|
|
471
|
-
try {
|
|
472
|
-
fs.unlinkSync(item.abs);
|
|
473
|
-
removedCount++;
|
|
474
|
-
} catch (err) {
|
|
475
|
-
failedCount++;
|
|
476
|
-
console.log(` ${WARN} Failed to remove ${item.rel}: ${err.message}`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
for (const dir of taskDirsToDelete) {
|
|
481
|
-
try {
|
|
482
|
-
fs.rmSync(dir.abs, { recursive: true, force: true });
|
|
483
|
-
removedCount++;
|
|
484
|
-
} catch (err) {
|
|
485
|
-
failedCount++;
|
|
486
|
-
console.log(` ${WARN} Failed to remove directory ${dir.rel}: ${err.message}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Best-effort cleanup of empty folders
|
|
491
|
-
pruneEmptyDir(path.join(projectRoot, ".pi", "agents"));
|
|
492
|
-
pruneEmptyDir(path.join(projectRoot, ".pi"));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (removePackage) {
|
|
496
|
-
if (!commandExists("pi")) {
|
|
497
|
-
failedCount++;
|
|
498
|
-
console.log(` ${FAIL} pi is not on PATH; could not run: ${piRemoveCmd}`);
|
|
499
|
-
} else {
|
|
500
|
-
try {
|
|
501
|
-
execSync(piRemoveCmd, { cwd: projectRoot, stdio: "inherit" });
|
|
502
|
-
} catch {
|
|
503
|
-
failedCount++;
|
|
504
|
-
console.log(` ${FAIL} Package uninstall failed: ${piRemoveCmd}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
console.log();
|
|
510
|
-
if (failedCount === 0) {
|
|
511
|
-
console.log(`${OK} ${c.bold}Uninstall complete.${c.reset}`);
|
|
512
|
-
if (removeProject) {
|
|
513
|
-
console.log(` Removed ${removedCount} project artifact(s).`);
|
|
514
|
-
}
|
|
515
|
-
console.log();
|
|
516
|
-
} else {
|
|
517
|
-
console.log(`${FAIL} Uninstall completed with ${failedCount} error(s).`);
|
|
518
|
-
if (removeProject) {
|
|
519
|
-
console.log(` Removed ${removedCount} project artifact(s).`);
|
|
520
|
-
}
|
|
521
|
-
console.log();
|
|
522
|
-
process.exit(1);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ─── init ───────────────────────────────────────────────────────────────────
|
|
527
|
-
|
|
528
|
-
async function cmdInit(args) {
|
|
529
|
-
const projectRoot = process.cwd();
|
|
530
|
-
const force = args.includes("--force");
|
|
531
|
-
const dryRun = args.includes("--dry-run");
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
{
|
|
773
|
-
{
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Check
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
console.log(`
|
|
898
|
-
console.log(
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
${
|
|
934
|
-
${
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
${
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
taskplane
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
${c.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Taskplane CLI — Project scaffolding, diagnostics, uninstall, and dashboard launcher.
|
|
5
|
+
*
|
|
6
|
+
* This CLI handles what the pi package system cannot: project-local config
|
|
7
|
+
* scaffolding, installation health checks, and dashboard management.
|
|
8
|
+
*
|
|
9
|
+
* Extensions, skills, and themes are delivered via `pi install npm:taskplane`
|
|
10
|
+
* and auto-discovered by pi. This CLI is for everything else.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import readline from "node:readline";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { execSync, spawn } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
24
|
+
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
|
|
25
|
+
const DASHBOARD_SERVER = path.join(PACKAGE_ROOT, "dashboard", "server.cjs");
|
|
26
|
+
|
|
27
|
+
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const c = {
|
|
30
|
+
reset: "\x1b[0m",
|
|
31
|
+
bold: "\x1b[1m",
|
|
32
|
+
dim: "\x1b[2m",
|
|
33
|
+
red: "\x1b[31m",
|
|
34
|
+
green: "\x1b[32m",
|
|
35
|
+
yellow: "\x1b[33m",
|
|
36
|
+
blue: "\x1b[34m",
|
|
37
|
+
cyan: "\x1b[36m",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const OK = `${c.green}✅${c.reset}`;
|
|
41
|
+
const WARN = `${c.yellow}⚠️${c.reset}`;
|
|
42
|
+
const FAIL = `${c.red}❌${c.reset}`;
|
|
43
|
+
const INFO = `${c.cyan}ℹ${c.reset}`;
|
|
44
|
+
|
|
45
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function die(msg) {
|
|
48
|
+
console.error(`${FAIL} ${msg}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function today() {
|
|
53
|
+
return new Date().toISOString().slice(0, 10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function slugify(str) {
|
|
57
|
+
return str
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
60
|
+
.replace(/^-|-$/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Prompt the user for input. Returns the answer or defaultValue. */
|
|
64
|
+
function ask(question, defaultValue) {
|
|
65
|
+
const rl = readline.createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout,
|
|
68
|
+
});
|
|
69
|
+
const suffix = defaultValue != null ? ` ${c.dim}(${defaultValue})${c.reset}` : "";
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
72
|
+
rl.close();
|
|
73
|
+
resolve(answer.trim() || defaultValue || "");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Prompt yes/no. Returns boolean. */
|
|
79
|
+
async function confirm(question, defaultYes = true) {
|
|
80
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
81
|
+
const answer = await ask(`${question} [${hint}]`);
|
|
82
|
+
if (!answer) return defaultYes;
|
|
83
|
+
return answer.toLowerCase().startsWith("y");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read a YAML file if it exists, return null otherwise. */
|
|
87
|
+
function readYaml(filePath) {
|
|
88
|
+
try {
|
|
89
|
+
// Dynamic import of yaml — it's a dependency of the package
|
|
90
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
91
|
+
// Simple YAML value extraction (avoids requiring yaml at top level for fast startup)
|
|
92
|
+
return raw;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if a command exists on PATH. */
|
|
99
|
+
function commandExists(cmd) {
|
|
100
|
+
try {
|
|
101
|
+
const which = process.platform === "win32" ? "where" : "which";
|
|
102
|
+
execSync(`${which} ${cmd}`, { stdio: "pipe" });
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Get command version string. */
|
|
110
|
+
function getVersion(cmd, flag = "--version") {
|
|
111
|
+
try {
|
|
112
|
+
return execSync(`${cmd} ${flag}`, { stdio: "pipe" }).toString().trim();
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Write a file, creating parent directories as needed. Optionally skip if exists. */
|
|
119
|
+
function writeFile(dest, content, { skipIfExists = false, label = "" } = {}) {
|
|
120
|
+
if (skipIfExists && fs.existsSync(dest)) {
|
|
121
|
+
if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
125
|
+
fs.writeFileSync(dest, content, "utf-8");
|
|
126
|
+
if (label) console.log(` ${c.green}create${c.reset} ${label}`);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Copy a file from templates, creating parent dirs. */
|
|
131
|
+
function copyTemplate(src, dest, { skipIfExists = false, label = "" } = {}) {
|
|
132
|
+
if (skipIfExists && fs.existsSync(dest)) {
|
|
133
|
+
if (label) console.log(` ${c.dim}skip${c.reset} ${label} (already exists)`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const content = fs.readFileSync(src, "utf-8");
|
|
137
|
+
return writeFile(dest, content, { label });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Replace {{variables}} in template content. */
|
|
141
|
+
function interpolate(content, vars) {
|
|
142
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Stack Detection ────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function detectStack(projectRoot) {
|
|
148
|
+
const checks = [
|
|
149
|
+
{ file: "package.json", stack: "node", test: "npm test", build: "npm run build" },
|
|
150
|
+
{ file: "go.mod", stack: "go", test: "go test ./...", build: "go build ./..." },
|
|
151
|
+
{ file: "Cargo.toml", stack: "rust", test: "cargo test", build: "cargo build" },
|
|
152
|
+
{ file: "pyproject.toml", stack: "python", test: "pytest", build: "" },
|
|
153
|
+
{ file: "pom.xml", stack: "java-maven", test: "mvn test", build: "mvn package" },
|
|
154
|
+
{ file: "build.gradle", stack: "java-gradle", test: "gradle test", build: "gradle build" },
|
|
155
|
+
];
|
|
156
|
+
for (const { file, stack, test, build } of checks) {
|
|
157
|
+
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
158
|
+
return { stack, test, build };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { stack: "unknown", test: "", build: "" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── YAML Generation ────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function generateTaskRunnerYaml(vars) {
|
|
167
|
+
return `# ═══════════════════════════════════════════════════════════════════════
|
|
168
|
+
# Task Runner Configuration — ${vars.project_name}
|
|
169
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
170
|
+
#
|
|
171
|
+
# This file configures the /task command (task-runner extension).
|
|
172
|
+
# Edit freely — this file is owned by you, not the package.
|
|
173
|
+
|
|
174
|
+
# ── Task Areas ────────────────────────────────────────────────────────
|
|
175
|
+
# Define where tasks live. Each area has a folder path, ID prefix, and
|
|
176
|
+
# a CONTEXT.md file that provides domain context to agents.
|
|
177
|
+
|
|
178
|
+
task_areas:
|
|
179
|
+
${vars.default_area}:
|
|
180
|
+
path: "${vars.tasks_root}"
|
|
181
|
+
prefix: "${vars.default_prefix}"
|
|
182
|
+
context: "${vars.tasks_root}/CONTEXT.md"
|
|
183
|
+
|
|
184
|
+
# ── Reference Docs ────────────────────────────────────────────────────
|
|
185
|
+
# Docs that tasks can reference in their "Context to Read First" section.
|
|
186
|
+
# Add your project's architecture docs, API specs, etc.
|
|
187
|
+
|
|
188
|
+
reference_docs: {}
|
|
189
|
+
|
|
190
|
+
# ── Standards ─────────────────────────────────────────────────────────
|
|
191
|
+
# Coding standards and rules. Agents follow these during implementation.
|
|
192
|
+
|
|
193
|
+
standards: {}
|
|
194
|
+
|
|
195
|
+
# ── Testing ───────────────────────────────────────────────────────────
|
|
196
|
+
# Commands that agents run to verify their work.
|
|
197
|
+
|
|
198
|
+
testing:
|
|
199
|
+
commands:${vars.test_cmd ? `\n unit: "${vars.test_cmd}"` : ""}${vars.build_cmd ? `\n build: "${vars.build_cmd}"` : ""}
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function generateOrchestratorYaml(vars) {
|
|
204
|
+
return `# ═══════════════════════════════════════════════════════════════════════
|
|
205
|
+
# Parallel Task Orchestrator Configuration — ${vars.project_name}
|
|
206
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
207
|
+
#
|
|
208
|
+
# This file configures the /orch commands (task-orchestrator extension).
|
|
209
|
+
# Edit freely — this file is owned by you, not the package.
|
|
210
|
+
|
|
211
|
+
orchestrator:
|
|
212
|
+
max_lanes: ${vars.max_lanes}
|
|
213
|
+
worktree_location: "subdirectory"
|
|
214
|
+
worktree_prefix: "${vars.worktree_prefix}"
|
|
215
|
+
integration_branch: "${vars.integration_branch}"
|
|
216
|
+
batch_id_format: "timestamp"
|
|
217
|
+
spawn_mode: "subprocess"
|
|
218
|
+
tmux_prefix: "${vars.tmux_prefix}"
|
|
219
|
+
|
|
220
|
+
dependencies:
|
|
221
|
+
source: "prompt"
|
|
222
|
+
cache: true
|
|
223
|
+
|
|
224
|
+
assignment:
|
|
225
|
+
strategy: "affinity-first"
|
|
226
|
+
size_weights:
|
|
227
|
+
S: 1
|
|
228
|
+
M: 2
|
|
229
|
+
L: 4
|
|
230
|
+
|
|
231
|
+
pre_warm:
|
|
232
|
+
auto_detect: false
|
|
233
|
+
commands: {}
|
|
234
|
+
always: []
|
|
235
|
+
|
|
236
|
+
merge:
|
|
237
|
+
model: ""
|
|
238
|
+
tools: "read,write,edit,bash,grep,find,ls"
|
|
239
|
+
verify: []
|
|
240
|
+
order: "fewest-files-first"
|
|
241
|
+
|
|
242
|
+
failure:
|
|
243
|
+
on_task_failure: "skip-dependents"
|
|
244
|
+
on_merge_failure: "pause"
|
|
245
|
+
stall_timeout: 30
|
|
246
|
+
max_worker_minutes: 30
|
|
247
|
+
abort_grace_period: 60
|
|
248
|
+
|
|
249
|
+
monitoring:
|
|
250
|
+
poll_interval: 5
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
255
|
+
// COMMANDS
|
|
256
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
/** Auto-commit task files to git so they're available in orchestrator worktrees. */
|
|
259
|
+
async function autoCommitTaskFiles(projectRoot, tasksRoot) {
|
|
260
|
+
// Check if we're in a git repo
|
|
261
|
+
try {
|
|
262
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd: projectRoot, stdio: "pipe" });
|
|
263
|
+
} catch {
|
|
264
|
+
// Not a git repo — skip silently
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Stage the tasks directory (not .pi/ — that's gitignored)
|
|
269
|
+
const tasksDir = path.join(projectRoot, tasksRoot);
|
|
270
|
+
if (!fs.existsSync(tasksDir)) return;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Check if there's anything new to commit
|
|
274
|
+
execSync(`git add "${tasksRoot}"`, { cwd: projectRoot, stdio: "pipe" });
|
|
275
|
+
const status = execSync("git diff --cached --name-only", { cwd: projectRoot, stdio: "pipe" })
|
|
276
|
+
.toString()
|
|
277
|
+
.trim();
|
|
278
|
+
|
|
279
|
+
if (!status) return; // nothing staged
|
|
280
|
+
|
|
281
|
+
execSync('git commit -m "chore: initialize taskplane tasks"', {
|
|
282
|
+
cwd: projectRoot,
|
|
283
|
+
stdio: "pipe",
|
|
284
|
+
});
|
|
285
|
+
console.log(`\n ${c.green}git${c.reset} committed ${tasksRoot}/ to git`);
|
|
286
|
+
console.log(` ${c.dim}(orchestrator worktrees require committed files)${c.reset}`);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
// Git commit failed — warn but don't block init
|
|
289
|
+
console.log(`\n ${WARN} Could not auto-commit task files to git.`);
|
|
290
|
+
console.log(` ${c.dim}Run manually before using /orch: git add ${tasksRoot} && git commit -m "add taskplane tasks"${c.reset}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function discoverTaskAreaPaths(projectRoot) {
|
|
295
|
+
const runnerPath = path.join(projectRoot, ".pi", "task-runner.yaml");
|
|
296
|
+
if (!fs.existsSync(runnerPath)) return [];
|
|
297
|
+
|
|
298
|
+
const raw = readYaml(runnerPath);
|
|
299
|
+
if (!raw) return [];
|
|
300
|
+
|
|
301
|
+
const lines = raw.split(/\r?\n/);
|
|
302
|
+
let inTaskAreas = false;
|
|
303
|
+
const paths = new Set();
|
|
304
|
+
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
|
|
308
|
+
if (!inTaskAreas) {
|
|
309
|
+
if (/^task_areas:\s*$/.test(trimmed)) {
|
|
310
|
+
inTaskAreas = true;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// End of task_areas block when we hit next top-level key
|
|
316
|
+
if (/^[A-Za-z0-9_]+\s*:\s*$/.test(line)) {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const m = line.match(/^\s{4}path:\s*["']?([^"'\n#]+)["']?\s*(?:#.*)?$/);
|
|
321
|
+
if (m?.[1]) {
|
|
322
|
+
paths.add(m[1].trim());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...paths];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function pruneEmptyDir(dirPath) {
|
|
330
|
+
try {
|
|
331
|
+
if (!fs.existsSync(dirPath)) return false;
|
|
332
|
+
if (fs.readdirSync(dirPath).length !== 0) return false;
|
|
333
|
+
fs.rmdirSync(dirPath);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function listExampleTaskTemplates() {
|
|
341
|
+
const tasksTemplatesDir = path.join(TEMPLATES_DIR, "tasks");
|
|
342
|
+
try {
|
|
343
|
+
return fs.readdirSync(tasksTemplatesDir, { withFileTypes: true })
|
|
344
|
+
.filter((entry) => entry.isDirectory() && /^EXAMPLE-\d+/i.test(entry.name))
|
|
345
|
+
.map((entry) => entry.name)
|
|
346
|
+
.sort();
|
|
347
|
+
} catch {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function cmdUninstall(args) {
|
|
353
|
+
const projectRoot = process.cwd();
|
|
354
|
+
const dryRun = args.includes("--dry-run");
|
|
355
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
356
|
+
const removePackage = args.includes("--package") || args.includes("--all") || args.includes("--package-only");
|
|
357
|
+
const packageOnly = args.includes("--package-only");
|
|
358
|
+
const removeProject = !packageOnly;
|
|
359
|
+
const removeTasks = removeProject && (args.includes("--remove-tasks") || args.includes("--all"));
|
|
360
|
+
const local = args.includes("--local");
|
|
361
|
+
const global = args.includes("--global");
|
|
362
|
+
|
|
363
|
+
if (local && global) {
|
|
364
|
+
die("Choose either --local or --global, not both.");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log(`\n${c.bold}Taskplane Uninstall${c.reset}\n`);
|
|
368
|
+
|
|
369
|
+
const managedFiles = [
|
|
370
|
+
".pi/task-runner.yaml",
|
|
371
|
+
".pi/task-orchestrator.yaml",
|
|
372
|
+
".pi/taskplane.json",
|
|
373
|
+
".pi/agents/task-worker.md",
|
|
374
|
+
".pi/agents/task-reviewer.md",
|
|
375
|
+
".pi/agents/task-merger.md",
|
|
376
|
+
".pi/batch-state.json",
|
|
377
|
+
".pi/batch-history.json",
|
|
378
|
+
".pi/orch-abort-signal",
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
const sidecarPrefixes = [
|
|
382
|
+
"lane-state-",
|
|
383
|
+
"worker-conversation-",
|
|
384
|
+
"merge-result-",
|
|
385
|
+
"merge-request-",
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
const filesToDelete = managedFiles
|
|
389
|
+
.map(rel => ({ rel, abs: path.join(projectRoot, rel) }))
|
|
390
|
+
.filter(({ abs }) => fs.existsSync(abs));
|
|
391
|
+
|
|
392
|
+
const piDir = path.join(projectRoot, ".pi");
|
|
393
|
+
const sidecarsToDelete = fs.existsSync(piDir)
|
|
394
|
+
? fs.readdirSync(piDir)
|
|
395
|
+
.filter(name => sidecarPrefixes.some(prefix => name.startsWith(prefix)))
|
|
396
|
+
.map(name => ({ rel: path.join(".pi", name), abs: path.join(piDir, name) }))
|
|
397
|
+
: [];
|
|
398
|
+
|
|
399
|
+
let taskDirsToDelete = [];
|
|
400
|
+
if (removeTasks) {
|
|
401
|
+
const areaPaths = discoverTaskAreaPaths(projectRoot);
|
|
402
|
+
const rootPrefix = path.resolve(projectRoot) + path.sep;
|
|
403
|
+
taskDirsToDelete = areaPaths
|
|
404
|
+
.map(rel => ({ rel, abs: path.resolve(projectRoot, rel) }))
|
|
405
|
+
.filter(({ abs }) => abs.startsWith(rootPrefix) && fs.existsSync(abs));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const inferredInstallType = /[\\/]\.pi[\\/]/.test(PACKAGE_ROOT) ? "local" : "global";
|
|
409
|
+
const packageScope = local ? "local" : global ? "global" : inferredInstallType;
|
|
410
|
+
const piRemoveCmd = packageScope === "local"
|
|
411
|
+
? "pi remove -l npm:taskplane"
|
|
412
|
+
: "pi remove npm:taskplane";
|
|
413
|
+
|
|
414
|
+
if (!removeProject && !removePackage) {
|
|
415
|
+
console.log(` ${WARN} Nothing to do. Use one of:`);
|
|
416
|
+
console.log(` ${c.cyan}taskplane uninstall${c.reset} # remove project-scaffolded files`);
|
|
417
|
+
console.log(` ${c.cyan}taskplane uninstall --package${c.reset} # remove installed package via pi`);
|
|
418
|
+
console.log();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (removeProject) {
|
|
423
|
+
console.log(`${c.bold}Project cleanup:${c.reset}`);
|
|
424
|
+
if (filesToDelete.length === 0 && sidecarsToDelete.length === 0 && taskDirsToDelete.length === 0) {
|
|
425
|
+
console.log(` ${c.dim}No Taskplane-managed project files found.${c.reset}`);
|
|
426
|
+
}
|
|
427
|
+
for (const f of filesToDelete) console.log(` - remove ${f.rel}`);
|
|
428
|
+
for (const f of sidecarsToDelete) console.log(` - remove ${f.rel}`);
|
|
429
|
+
for (const d of taskDirsToDelete) console.log(` - remove dir ${d.rel}`);
|
|
430
|
+
if (removeTasks && taskDirsToDelete.length === 0) {
|
|
431
|
+
console.log(` ${c.dim}No task area directories found from .pi/task-runner.yaml.${c.reset}`);
|
|
432
|
+
}
|
|
433
|
+
if (!removeTasks) {
|
|
434
|
+
console.log(` ${c.dim}Task directories are preserved by default (use --remove-tasks to delete them).${c.reset}`);
|
|
435
|
+
}
|
|
436
|
+
console.log();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (removePackage) {
|
|
440
|
+
console.log(`${c.bold}Package cleanup:${c.reset}`);
|
|
441
|
+
console.log(` - run ${piRemoveCmd}`);
|
|
442
|
+
console.log(` ${c.dim}(removes extensions, skills, and dashboard files from this install scope)${c.reset}`);
|
|
443
|
+
console.log();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (dryRun) {
|
|
447
|
+
console.log(`${INFO} Dry run complete. No files were changed.\n`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!yes) {
|
|
452
|
+
const proceed = await confirm("Proceed with uninstall?", false);
|
|
453
|
+
if (!proceed) {
|
|
454
|
+
console.log(" Aborted.");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (removeTasks) {
|
|
458
|
+
const taskConfirm = await confirm("This will delete task area directories recursively. Continue?", false);
|
|
459
|
+
if (!taskConfirm) {
|
|
460
|
+
console.log(" Aborted.");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let removedCount = 0;
|
|
467
|
+
let failedCount = 0;
|
|
468
|
+
|
|
469
|
+
if (removeProject) {
|
|
470
|
+
for (const item of [...filesToDelete, ...sidecarsToDelete]) {
|
|
471
|
+
try {
|
|
472
|
+
fs.unlinkSync(item.abs);
|
|
473
|
+
removedCount++;
|
|
474
|
+
} catch (err) {
|
|
475
|
+
failedCount++;
|
|
476
|
+
console.log(` ${WARN} Failed to remove ${item.rel}: ${err.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const dir of taskDirsToDelete) {
|
|
481
|
+
try {
|
|
482
|
+
fs.rmSync(dir.abs, { recursive: true, force: true });
|
|
483
|
+
removedCount++;
|
|
484
|
+
} catch (err) {
|
|
485
|
+
failedCount++;
|
|
486
|
+
console.log(` ${WARN} Failed to remove directory ${dir.rel}: ${err.message}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Best-effort cleanup of empty folders
|
|
491
|
+
pruneEmptyDir(path.join(projectRoot, ".pi", "agents"));
|
|
492
|
+
pruneEmptyDir(path.join(projectRoot, ".pi"));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (removePackage) {
|
|
496
|
+
if (!commandExists("pi")) {
|
|
497
|
+
failedCount++;
|
|
498
|
+
console.log(` ${FAIL} pi is not on PATH; could not run: ${piRemoveCmd}`);
|
|
499
|
+
} else {
|
|
500
|
+
try {
|
|
501
|
+
execSync(piRemoveCmd, { cwd: projectRoot, stdio: "inherit" });
|
|
502
|
+
} catch {
|
|
503
|
+
failedCount++;
|
|
504
|
+
console.log(` ${FAIL} Package uninstall failed: ${piRemoveCmd}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
console.log();
|
|
510
|
+
if (failedCount === 0) {
|
|
511
|
+
console.log(`${OK} ${c.bold}Uninstall complete.${c.reset}`);
|
|
512
|
+
if (removeProject) {
|
|
513
|
+
console.log(` Removed ${removedCount} project artifact(s).`);
|
|
514
|
+
}
|
|
515
|
+
console.log();
|
|
516
|
+
} else {
|
|
517
|
+
console.log(`${FAIL} Uninstall completed with ${failedCount} error(s).`);
|
|
518
|
+
if (removeProject) {
|
|
519
|
+
console.log(` Removed ${removedCount} project artifact(s).`);
|
|
520
|
+
}
|
|
521
|
+
console.log();
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── init ───────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
async function cmdInit(args) {
|
|
529
|
+
const projectRoot = process.cwd();
|
|
530
|
+
const force = args.includes("--force");
|
|
531
|
+
const dryRun = args.includes("--dry-run");
|
|
532
|
+
const noExamplesFlag = args.includes("--no-examples");
|
|
533
|
+
const includeExamples = args.includes("--include-examples");
|
|
534
|
+
const presetIdx = args.indexOf("--preset");
|
|
535
|
+
const preset = presetIdx !== -1 ? args[presetIdx + 1] : null;
|
|
536
|
+
const tasksRootIdx = args.indexOf("--tasks-root");
|
|
537
|
+
const tasksRootRaw = tasksRootIdx !== -1 ? args[tasksRootIdx + 1] : null;
|
|
538
|
+
|
|
539
|
+
if (noExamplesFlag && includeExamples) {
|
|
540
|
+
die("Choose either --no-examples or --include-examples, not both.");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (tasksRootIdx !== -1 && (!tasksRootRaw || tasksRootRaw.startsWith("--"))) {
|
|
544
|
+
die("Missing value for --tasks-root <relative-path>.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let tasksRootOverride = null;
|
|
548
|
+
if (tasksRootRaw) {
|
|
549
|
+
if (path.isAbsolute(tasksRootRaw)) {
|
|
550
|
+
die("--tasks-root must be relative to the project root (absolute paths are not allowed).");
|
|
551
|
+
}
|
|
552
|
+
tasksRootOverride = tasksRootRaw.trim().replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
553
|
+
if (!tasksRootOverride || tasksRootOverride === ".") {
|
|
554
|
+
die("--tasks-root must not be empty.");
|
|
555
|
+
}
|
|
556
|
+
if (tasksRootOverride === ".." || tasksRootOverride.startsWith("../")) {
|
|
557
|
+
die("--tasks-root must stay within the project root (paths starting with .. are not allowed).");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const noExamples = noExamplesFlag || (!!tasksRootOverride && !includeExamples);
|
|
562
|
+
|
|
563
|
+
console.log(`\n${c.bold}Taskplane Init${c.reset}\n`);
|
|
564
|
+
|
|
565
|
+
if (tasksRootOverride && !noExamplesFlag && !includeExamples) {
|
|
566
|
+
console.log(` ${INFO} Using custom --tasks-root (${tasksRootOverride}); skipping example tasks by default.`);
|
|
567
|
+
console.log(` Use --include-examples to scaffold examples into that directory.\n`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check for existing config
|
|
571
|
+
const hasConfig =
|
|
572
|
+
fs.existsSync(path.join(projectRoot, ".pi", "task-runner.yaml")) ||
|
|
573
|
+
fs.existsSync(path.join(projectRoot, ".pi", "task-orchestrator.yaml"));
|
|
574
|
+
|
|
575
|
+
if (hasConfig && !force) {
|
|
576
|
+
console.log(`${WARN} Taskplane config already exists in this project.`);
|
|
577
|
+
const proceed = await confirm(" Overwrite existing files?", false);
|
|
578
|
+
if (!proceed) {
|
|
579
|
+
console.log(" Aborted.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Gather config values
|
|
585
|
+
let vars;
|
|
586
|
+
if (preset === "minimal" || preset === "full" || preset === "runner-only") {
|
|
587
|
+
vars = getPresetVars(preset, projectRoot, tasksRootOverride);
|
|
588
|
+
console.log(` Using preset: ${c.cyan}${preset}${c.reset}`);
|
|
589
|
+
if (tasksRootOverride) {
|
|
590
|
+
console.log(` Task directory: ${c.cyan}${tasksRootOverride}${c.reset}`);
|
|
591
|
+
}
|
|
592
|
+
console.log();
|
|
593
|
+
} else {
|
|
594
|
+
vars = await getInteractiveVars(projectRoot, tasksRootOverride);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const exampleTemplateDirs = noExamples ? [] : listExampleTaskTemplates();
|
|
598
|
+
|
|
599
|
+
if (dryRun) {
|
|
600
|
+
console.log(`\n${c.bold}Dry run — files that would be created:${c.reset}\n`);
|
|
601
|
+
printFileList(vars, noExamples, preset, exampleTemplateDirs);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Scaffold files
|
|
606
|
+
console.log(`\n${c.bold}Creating files...${c.reset}\n`);
|
|
607
|
+
const skipIfExists = !force;
|
|
608
|
+
|
|
609
|
+
// Agent prompts
|
|
610
|
+
for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md"]) {
|
|
611
|
+
copyTemplate(
|
|
612
|
+
path.join(TEMPLATES_DIR, "agents", agent),
|
|
613
|
+
path.join(projectRoot, ".pi", "agents", agent),
|
|
614
|
+
{ skipIfExists, label: `.pi/agents/${agent}` }
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Task runner config
|
|
619
|
+
writeFile(
|
|
620
|
+
path.join(projectRoot, ".pi", "task-runner.yaml"),
|
|
621
|
+
generateTaskRunnerYaml(vars),
|
|
622
|
+
{ skipIfExists, label: ".pi/task-runner.yaml" }
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// Orchestrator config (skip for runner-only preset)
|
|
626
|
+
if (preset !== "runner-only") {
|
|
627
|
+
writeFile(
|
|
628
|
+
path.join(projectRoot, ".pi", "task-orchestrator.yaml"),
|
|
629
|
+
generateOrchestratorYaml(vars),
|
|
630
|
+
{ skipIfExists, label: ".pi/task-orchestrator.yaml" }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Version tracker (always overwrite)
|
|
635
|
+
const versionInfo = {
|
|
636
|
+
version: getPackageVersion(),
|
|
637
|
+
installedAt: new Date().toISOString(),
|
|
638
|
+
lastUpgraded: new Date().toISOString(),
|
|
639
|
+
components: { agents: getPackageVersion(), config: getPackageVersion() },
|
|
640
|
+
};
|
|
641
|
+
writeFile(
|
|
642
|
+
path.join(projectRoot, ".pi", "taskplane.json"),
|
|
643
|
+
JSON.stringify(versionInfo, null, 2) + "\n",
|
|
644
|
+
{ label: ".pi/taskplane.json" }
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// CONTEXT.md
|
|
648
|
+
const contextSrc = fs.readFileSync(path.join(TEMPLATES_DIR, "tasks", "CONTEXT.md"), "utf-8");
|
|
649
|
+
writeFile(
|
|
650
|
+
path.join(projectRoot, vars.tasks_root, "CONTEXT.md"),
|
|
651
|
+
interpolate(contextSrc, vars),
|
|
652
|
+
{ skipIfExists, label: `${vars.tasks_root}/CONTEXT.md` }
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Example tasks
|
|
656
|
+
if (!noExamples) {
|
|
657
|
+
for (const exampleName of exampleTemplateDirs) {
|
|
658
|
+
const exampleDir = path.join(TEMPLATES_DIR, "tasks", exampleName);
|
|
659
|
+
const destDir = path.join(projectRoot, vars.tasks_root, exampleName);
|
|
660
|
+
for (const file of ["PROMPT.md", "STATUS.md"]) {
|
|
661
|
+
const srcPath = path.join(exampleDir, file);
|
|
662
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
663
|
+
const src = fs.readFileSync(srcPath, "utf-8");
|
|
664
|
+
writeFile(path.join(destDir, file), interpolate(src, vars), {
|
|
665
|
+
skipIfExists,
|
|
666
|
+
label: `${vars.tasks_root}/${exampleName}/${file}`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (exampleTemplateDirs.length === 0) {
|
|
671
|
+
console.log(` ${WARN} No example task templates found under templates/tasks/EXAMPLE-*`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Auto-commit task files to git so they're available in worktrees
|
|
676
|
+
await autoCommitTaskFiles(projectRoot, vars.tasks_root);
|
|
677
|
+
|
|
678
|
+
// Report
|
|
679
|
+
console.log(`\n${OK} ${c.bold}Taskplane initialized!${c.reset}\n`);
|
|
680
|
+
console.log(`${c.bold}Quick start:${c.reset}`);
|
|
681
|
+
console.log(` ${c.cyan}pi${c.reset} # start pi (taskplane auto-loads)`);
|
|
682
|
+
if (preset !== "runner-only") {
|
|
683
|
+
console.log(` ${c.cyan}/orch-plan all${c.reset} # preview waves/lanes/dependencies`);
|
|
684
|
+
console.log(` ${c.cyan}/orch all${c.reset} # run examples via orchestrator`);
|
|
685
|
+
}
|
|
686
|
+
if (!noExamples && exampleTemplateDirs.length > 0) {
|
|
687
|
+
const firstExample = exampleTemplateDirs[0];
|
|
688
|
+
console.log(` ${c.dim}optional single-task mode:${c.reset}`);
|
|
689
|
+
console.log(` ${c.cyan}/task ${vars.tasks_root}/${firstExample}/PROMPT.md${c.reset}`);
|
|
690
|
+
}
|
|
691
|
+
console.log();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getPresetVars(preset, projectRoot, tasksRootOverride = null) {
|
|
695
|
+
const dirName = path.basename(projectRoot);
|
|
696
|
+
const slug = slugify(dirName);
|
|
697
|
+
const { test: test_cmd, build: build_cmd } = detectStack(projectRoot);
|
|
698
|
+
return {
|
|
699
|
+
project_name: dirName,
|
|
700
|
+
integration_branch: "main",
|
|
701
|
+
max_lanes: 3,
|
|
702
|
+
worktree_prefix: `${slug}-wt`,
|
|
703
|
+
tmux_prefix: `${slug}-orch`,
|
|
704
|
+
tasks_root: tasksRootOverride || "taskplane-tasks",
|
|
705
|
+
default_area: "general",
|
|
706
|
+
default_prefix: "TP",
|
|
707
|
+
test_cmd,
|
|
708
|
+
build_cmd,
|
|
709
|
+
date: today(),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function getInteractiveVars(projectRoot, tasksRootOverride = null) {
|
|
714
|
+
const dirName = path.basename(projectRoot);
|
|
715
|
+
const detected = detectStack(projectRoot);
|
|
716
|
+
|
|
717
|
+
const project_name = await ask("Project name", dirName);
|
|
718
|
+
const integration_branch = await ask("Default branch (fallback — orchestrator uses your current branch at runtime)", "main");
|
|
719
|
+
const max_lanes = parseInt(await ask("Max parallel lanes", "3")) || 3;
|
|
720
|
+
const tasks_root = tasksRootOverride || await ask("Tasks directory", "taskplane-tasks");
|
|
721
|
+
const default_area = await ask("Default area name", "general");
|
|
722
|
+
const default_prefix = await ask("Task ID prefix", "TP");
|
|
723
|
+
const test_cmd = await ask("Test command (agents run this to verify work — blank to skip)", detected.test || "");
|
|
724
|
+
const build_cmd = await ask("Build command (agents run this after tests — blank to skip)", detected.build || "");
|
|
725
|
+
|
|
726
|
+
const slug = slugify(project_name);
|
|
727
|
+
return {
|
|
728
|
+
project_name,
|
|
729
|
+
integration_branch,
|
|
730
|
+
max_lanes,
|
|
731
|
+
worktree_prefix: `${slug}-wt`,
|
|
732
|
+
tmux_prefix: `${slug}-orch`,
|
|
733
|
+
tasks_root,
|
|
734
|
+
default_area,
|
|
735
|
+
default_prefix,
|
|
736
|
+
test_cmd,
|
|
737
|
+
build_cmd,
|
|
738
|
+
date: today(),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function printFileList(vars, noExamples, preset, exampleTemplateDirs = []) {
|
|
743
|
+
const files = [
|
|
744
|
+
".pi/agents/task-worker.md",
|
|
745
|
+
".pi/agents/task-reviewer.md",
|
|
746
|
+
".pi/agents/task-merger.md",
|
|
747
|
+
".pi/task-runner.yaml",
|
|
748
|
+
];
|
|
749
|
+
if (preset !== "runner-only") files.push(".pi/task-orchestrator.yaml");
|
|
750
|
+
files.push(".pi/taskplane.json");
|
|
751
|
+
files.push(`${vars.tasks_root}/CONTEXT.md`);
|
|
752
|
+
if (!noExamples) {
|
|
753
|
+
for (const exampleName of exampleTemplateDirs) {
|
|
754
|
+
files.push(`${vars.tasks_root}/${exampleName}/PROMPT.md`);
|
|
755
|
+
files.push(`${vars.tasks_root}/${exampleName}/STATUS.md`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const f of files) console.log(` ${c.green}create${c.reset} ${f}`);
|
|
759
|
+
console.log();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ─── doctor ─────────────────────────────────────────────────────────────────
|
|
763
|
+
|
|
764
|
+
function cmdDoctor() {
|
|
765
|
+
const projectRoot = process.cwd();
|
|
766
|
+
let issues = 0;
|
|
767
|
+
|
|
768
|
+
console.log(`\n${c.bold}Taskplane Doctor${c.reset}\n`);
|
|
769
|
+
|
|
770
|
+
// Check prerequisites
|
|
771
|
+
const checks = [
|
|
772
|
+
{ label: "pi installed", check: () => commandExists("pi"), detail: () => getVersion("pi") },
|
|
773
|
+
{
|
|
774
|
+
label: "Node.js >= 20.0.0",
|
|
775
|
+
check: () => {
|
|
776
|
+
const v = process.versions.node;
|
|
777
|
+
return parseInt(v.split(".")[0]) >= 20;
|
|
778
|
+
},
|
|
779
|
+
detail: () => `v${process.versions.node}`,
|
|
780
|
+
},
|
|
781
|
+
{ label: "git installed", check: () => commandExists("git"), detail: () => getVersion("git") },
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
for (const { label, check, detail } of checks) {
|
|
785
|
+
const ok = check();
|
|
786
|
+
const info = ok && detail ? ` ${c.dim}(${detail()})${c.reset}` : "";
|
|
787
|
+
console.log(` ${ok ? OK : FAIL} ${label}${info}`);
|
|
788
|
+
if (!ok) issues++;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check tmux (optional)
|
|
792
|
+
const hasTmux = commandExists("tmux");
|
|
793
|
+
console.log(
|
|
794
|
+
` ${hasTmux ? OK : `${WARN}`} tmux installed${hasTmux ? ` ${c.dim}(${getVersion("tmux", "-V")})${c.reset}` : ` ${c.dim}(optional — needed for spawn_mode: tmux)${c.reset}`}`
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
// Check package installation
|
|
798
|
+
const pkgJson = path.join(PACKAGE_ROOT, "package.json");
|
|
799
|
+
const pkgVersion = getPackageVersion();
|
|
800
|
+
const isProjectLocal = PACKAGE_ROOT.includes(".pi");
|
|
801
|
+
const installType = isProjectLocal ? "project-local" : "global";
|
|
802
|
+
console.log(` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`);
|
|
803
|
+
|
|
804
|
+
// Check project config
|
|
805
|
+
console.log();
|
|
806
|
+
const configFiles = [
|
|
807
|
+
{ path: ".pi/task-runner.yaml", required: true },
|
|
808
|
+
{ path: ".pi/task-orchestrator.yaml", required: true },
|
|
809
|
+
{ path: ".pi/agents/task-worker.md", required: true },
|
|
810
|
+
{ path: ".pi/agents/task-reviewer.md", required: true },
|
|
811
|
+
{ path: ".pi/agents/task-merger.md", required: true },
|
|
812
|
+
{ path: ".pi/taskplane.json", required: false },
|
|
813
|
+
];
|
|
814
|
+
|
|
815
|
+
for (const { path: relPath, required } of configFiles) {
|
|
816
|
+
const exists = fs.existsSync(path.join(projectRoot, relPath));
|
|
817
|
+
if (exists) {
|
|
818
|
+
console.log(` ${OK} ${relPath} exists`);
|
|
819
|
+
} else if (required) {
|
|
820
|
+
console.log(` ${FAIL} ${relPath} missing`);
|
|
821
|
+
issues++;
|
|
822
|
+
} else {
|
|
823
|
+
console.log(` ${WARN} ${relPath} missing ${c.dim}(optional)${c.reset}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Check task areas from config
|
|
828
|
+
const runnerYaml = readYaml(path.join(projectRoot, ".pi", "task-runner.yaml"));
|
|
829
|
+
if (runnerYaml) {
|
|
830
|
+
// Simple regex extraction of task area paths
|
|
831
|
+
const pathMatches = [...runnerYaml.matchAll(/^\s+path:\s*"?([^"\n]+)"?/gm)];
|
|
832
|
+
const contextMatches = [...runnerYaml.matchAll(/^\s+context:\s*"?([^"\n]+)"?/gm)];
|
|
833
|
+
|
|
834
|
+
if (pathMatches.length > 0) {
|
|
835
|
+
console.log();
|
|
836
|
+
for (const match of pathMatches) {
|
|
837
|
+
const areaPath = match[1].trim();
|
|
838
|
+
const exists = fs.existsSync(path.join(projectRoot, areaPath));
|
|
839
|
+
if (exists) {
|
|
840
|
+
console.log(` ${OK} task area path: ${areaPath}`);
|
|
841
|
+
} else {
|
|
842
|
+
console.log(` ${FAIL} task area path: ${areaPath} ${c.dim}(directory not found)${c.reset}`);
|
|
843
|
+
console.log(` ${c.dim}→ Run: mkdir -p ${areaPath}${c.reset}`);
|
|
844
|
+
issues++;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const match of contextMatches) {
|
|
848
|
+
const ctxPath = match[1].trim();
|
|
849
|
+
const exists = fs.existsSync(path.join(projectRoot, ctxPath));
|
|
850
|
+
if (exists) {
|
|
851
|
+
console.log(` ${OK} CONTEXT.md: ${ctxPath}`);
|
|
852
|
+
} else {
|
|
853
|
+
console.log(` ${WARN} CONTEXT.md: ${ctxPath} ${c.dim}(not found)${c.reset}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log();
|
|
860
|
+
if (issues === 0) {
|
|
861
|
+
console.log(`${OK} ${c.green}All checks passed!${c.reset}\n`);
|
|
862
|
+
} else {
|
|
863
|
+
console.log(`${FAIL} ${issues} issue(s) found. Run ${c.cyan}taskplane init${c.reset} to fix config issues.\n`);
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ─── version ────────────────────────────────────────────────────────────────
|
|
869
|
+
|
|
870
|
+
function cmdVersion() {
|
|
871
|
+
const pkgVersion = getPackageVersion();
|
|
872
|
+
const isProjectLocal = PACKAGE_ROOT.includes(".pi");
|
|
873
|
+
const installType = isProjectLocal ? `project-local: ${PACKAGE_ROOT}` : `global: ${PACKAGE_ROOT}`;
|
|
874
|
+
|
|
875
|
+
console.log(`\ntaskplane ${c.bold}v${pkgVersion}${c.reset}`);
|
|
876
|
+
console.log(` Package: ${installType}`);
|
|
877
|
+
|
|
878
|
+
// Check for project config
|
|
879
|
+
const projectRoot = process.cwd();
|
|
880
|
+
const tpJson = path.join(projectRoot, ".pi", "taskplane.json");
|
|
881
|
+
if (fs.existsSync(tpJson)) {
|
|
882
|
+
try {
|
|
883
|
+
const info = JSON.parse(fs.readFileSync(tpJson, "utf-8"));
|
|
884
|
+
console.log(` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`);
|
|
885
|
+
} catch {
|
|
886
|
+
console.log(` Config: .pi/taskplane.json (unreadable)`);
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
console.log(` Config: ${c.dim}not initialized (run taskplane init)${c.reset}`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Pi version
|
|
893
|
+
const piVersion = getVersion("pi");
|
|
894
|
+
if (piVersion) console.log(` Pi: ${piVersion}`);
|
|
895
|
+
|
|
896
|
+
// Node version
|
|
897
|
+
console.log(` Node: v${process.versions.node}`);
|
|
898
|
+
console.log();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function getPackageVersion() {
|
|
902
|
+
try {
|
|
903
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
904
|
+
return pkg.version || "unknown";
|
|
905
|
+
} catch {
|
|
906
|
+
return "unknown";
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ─── dashboard ──────────────────────────────────────────────────────────────
|
|
911
|
+
|
|
912
|
+
function cmdDashboard(args) {
|
|
913
|
+
const projectRoot = process.cwd();
|
|
914
|
+
|
|
915
|
+
if (!fs.existsSync(DASHBOARD_SERVER)) {
|
|
916
|
+
die(`Dashboard server not found at ${DASHBOARD_SERVER}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Pass through args to server.cjs, adding --root
|
|
920
|
+
const serverArgs = ["--root", projectRoot];
|
|
921
|
+
|
|
922
|
+
// Forward --port and --no-open if provided
|
|
923
|
+
for (let i = 0; i < args.length; i++) {
|
|
924
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
925
|
+
serverArgs.push("--port", args[i + 1]);
|
|
926
|
+
i++;
|
|
927
|
+
} else if (args[i] === "--no-open") {
|
|
928
|
+
serverArgs.push("--no-open");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
console.log(`\n${c.bold}Taskplane Dashboard${c.reset}`);
|
|
933
|
+
console.log(` Project: ${projectRoot}`);
|
|
934
|
+
console.log(` Server: ${DASHBOARD_SERVER}\n`);
|
|
935
|
+
|
|
936
|
+
const child = spawn("node", [DASHBOARD_SERVER, ...serverArgs], {
|
|
937
|
+
stdio: "inherit",
|
|
938
|
+
cwd: projectRoot,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
child.on("error", (err) => {
|
|
942
|
+
die(`Failed to start dashboard: ${err.message}`);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Forward signals
|
|
946
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
947
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
948
|
+
|
|
949
|
+
child.on("exit", (code) => {
|
|
950
|
+
process.exit(code ?? 0);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ─── help ───────────────────────────────────────────────────────────────────
|
|
955
|
+
|
|
956
|
+
function showHelp() {
|
|
957
|
+
const version = getPackageVersion();
|
|
958
|
+
console.log(`
|
|
959
|
+
${c.bold}taskplane${c.reset} v${version} — AI agent orchestration for pi
|
|
960
|
+
|
|
961
|
+
${c.bold}Usage:${c.reset}
|
|
962
|
+
taskplane <command> [options]
|
|
963
|
+
|
|
964
|
+
${c.bold}Commands:${c.reset}
|
|
965
|
+
${c.cyan}init${c.reset} Scaffold Taskplane config in the current project
|
|
966
|
+
${c.cyan}doctor${c.reset} Validate installation and project configuration
|
|
967
|
+
${c.cyan}version${c.reset} Show version information
|
|
968
|
+
${c.cyan}dashboard${c.reset} Launch the web-based orchestrator dashboard
|
|
969
|
+
${c.cyan}uninstall${c.reset} Remove Taskplane project files and/or package install
|
|
970
|
+
${c.cyan}help${c.reset} Show this help message
|
|
971
|
+
|
|
972
|
+
${c.bold}Init options:${c.reset}
|
|
973
|
+
--preset <name> Use a preset: minimal, full, runner-only
|
|
974
|
+
--tasks-root <path> Relative tasks directory to use (e.g. docs/task-management)
|
|
975
|
+
--no-examples Skip example tasks scaffolding
|
|
976
|
+
--include-examples With --tasks-root, include example tasks (default is skip)
|
|
977
|
+
--force Overwrite existing files without prompting
|
|
978
|
+
--dry-run Show what would be created without writing
|
|
979
|
+
|
|
980
|
+
${c.bold}Dashboard options:${c.reset}
|
|
981
|
+
--port <number> Port to listen on (default: 8099)
|
|
982
|
+
--no-open Don't auto-open browser
|
|
983
|
+
|
|
984
|
+
${c.bold}Uninstall options:${c.reset}
|
|
985
|
+
--dry-run Show what would be removed
|
|
986
|
+
--yes, -y Skip confirmation prompts
|
|
987
|
+
--package Also remove installed package via pi remove
|
|
988
|
+
--package-only Only remove installed package (skip project cleanup)
|
|
989
|
+
--local Force package uninstall from project-local scope
|
|
990
|
+
--global Force package uninstall from global scope
|
|
991
|
+
--remove-tasks Also remove task area directories from task-runner.yaml
|
|
992
|
+
--all Equivalent to --package + --remove-tasks
|
|
993
|
+
|
|
994
|
+
${c.bold}Examples:${c.reset}
|
|
995
|
+
taskplane init # Interactive project setup
|
|
996
|
+
taskplane init --preset full # Quick setup with defaults
|
|
997
|
+
taskplane init --preset full --tasks-root docs/task-management
|
|
998
|
+
# Use existing task area path
|
|
999
|
+
taskplane init --dry-run # Preview what would be created
|
|
1000
|
+
taskplane doctor # Check installation health
|
|
1001
|
+
taskplane dashboard # Launch web dashboard
|
|
1002
|
+
taskplane dashboard --port 3000 # Dashboard on custom port
|
|
1003
|
+
taskplane uninstall --dry-run # Preview uninstall actions
|
|
1004
|
+
taskplane uninstall --package --yes # Remove project files + package install
|
|
1005
|
+
|
|
1006
|
+
${c.bold}Getting started:${c.reset}
|
|
1007
|
+
1. pi install npm:taskplane # Install the pi package
|
|
1008
|
+
2. cd my-project && taskplane init # Scaffold project config
|
|
1009
|
+
3. pi # Start pi — /task and /orch are ready
|
|
1010
|
+
`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
1014
|
+
// MAIN
|
|
1015
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
1016
|
+
|
|
1017
|
+
const [command, ...args] = process.argv.slice(2);
|
|
1018
|
+
|
|
1019
|
+
switch (command) {
|
|
1020
|
+
case "init":
|
|
1021
|
+
await cmdInit(args);
|
|
1022
|
+
break;
|
|
1023
|
+
case "doctor":
|
|
1024
|
+
cmdDoctor();
|
|
1025
|
+
break;
|
|
1026
|
+
case "version":
|
|
1027
|
+
case "--version":
|
|
1028
|
+
case "-v":
|
|
1029
|
+
cmdVersion();
|
|
1030
|
+
break;
|
|
1031
|
+
case "dashboard":
|
|
1032
|
+
cmdDashboard(args);
|
|
1033
|
+
break;
|
|
1034
|
+
case "uninstall":
|
|
1035
|
+
await cmdUninstall(args);
|
|
1036
|
+
break;
|
|
1037
|
+
case "help":
|
|
1038
|
+
case "--help":
|
|
1039
|
+
case "-h":
|
|
1040
|
+
case undefined:
|
|
1041
|
+
showHelp();
|
|
1042
|
+
break;
|
|
1043
|
+
default:
|
|
1044
|
+
console.error(`${FAIL} Unknown command: ${command}`);
|
|
1045
|
+
showHelp();
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|