@dunnewold-labs/mr-manager 0.4.36 → 0.4.38
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/dist/index.mjs +152 -85
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import { existsSync as
|
|
4
|
+
import { Command as Command31 } from "commander";
|
|
5
|
+
import { existsSync as existsSync18 } from "fs";
|
|
6
6
|
import { homedir as homedir3 } from "os";
|
|
7
7
|
import { join as join12 } from "path";
|
|
8
8
|
|
|
@@ -185,7 +185,7 @@ import { fileURLToPath } from "url";
|
|
|
185
185
|
// cli/package.json
|
|
186
186
|
var package_default = {
|
|
187
187
|
name: "@dunnewold-labs/mr-manager",
|
|
188
|
-
version: "0.4.
|
|
188
|
+
version: "0.4.38",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -354,8 +354,28 @@ var tasksCommand = new Command5("tasks").description("List tasks for the linked
|
|
|
354
354
|
console.log(JSON.stringify(tasks, null, 2));
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
-
// cli/commands/
|
|
357
|
+
// cli/commands/task-group.ts
|
|
358
358
|
import { Command as Command6 } from "commander";
|
|
359
|
+
var taskGroupCommand = new Command6("task-group").description("Group existing tasks under a new parent task with linked subtasks").requiredOption("--title <title>", "Parent task title").argument("<taskIds...>", "IDs of tasks to link as subtasks").action(async (taskIds, opts) => {
|
|
360
|
+
if (!Array.isArray(taskIds) || taskIds.length === 0) {
|
|
361
|
+
console.error("At least one task ID is required");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const parent = await api.post("/api/tasks/group", {
|
|
366
|
+
title: opts.title,
|
|
367
|
+
taskIds
|
|
368
|
+
});
|
|
369
|
+
console.log(parent.id);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
372
|
+
console.error(`Failed to create parent task: ${message}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// cli/commands/link.ts
|
|
378
|
+
import { Command as Command7 } from "commander";
|
|
359
379
|
import { createInterface } from "readline";
|
|
360
380
|
|
|
361
381
|
// cli/vcs.ts
|
|
@@ -402,7 +422,7 @@ function prompt(question) {
|
|
|
402
422
|
});
|
|
403
423
|
});
|
|
404
424
|
}
|
|
405
|
-
var linkCommand = new
|
|
425
|
+
var linkCommand = new Command7("link").description("Associate current directory with a project").argument("[project-id]", "Project ID to link").action(async (projectId) => {
|
|
406
426
|
if (!projectId) {
|
|
407
427
|
const current = getLinkedProjectId();
|
|
408
428
|
if (current) {
|
|
@@ -441,7 +461,7 @@ Created project: ${project.name} (${project.id})`);
|
|
|
441
461
|
);
|
|
442
462
|
}
|
|
443
463
|
});
|
|
444
|
-
var unlinkCommand = new
|
|
464
|
+
var unlinkCommand = new Command7("unlink").description("Remove current directory's project association").action(async () => {
|
|
445
465
|
const config = loadConfig();
|
|
446
466
|
const cwd = process.cwd();
|
|
447
467
|
if (!config.directories[cwd]) {
|
|
@@ -454,7 +474,7 @@ var unlinkCommand = new Command6("unlink").description("Remove current directory
|
|
|
454
474
|
});
|
|
455
475
|
|
|
456
476
|
// cli/commands/context.ts
|
|
457
|
-
import { Command as
|
|
477
|
+
import { Command as Command8 } from "commander";
|
|
458
478
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
459
479
|
import { join as join3, sep } from "path";
|
|
460
480
|
var FEATURES_FILE = ".mr-features.md";
|
|
@@ -472,7 +492,7 @@ function readFeatures() {
|
|
|
472
492
|
if (!existsSync3(path)) return null;
|
|
473
493
|
return readFileSync3(path, "utf-8");
|
|
474
494
|
}
|
|
475
|
-
var contextCommand = new
|
|
495
|
+
var contextCommand = new Command8("context").description("Output project context JSON for Claude Code").option("--install", "Install Claude Code command to .claude/commands/").action(async (opts) => {
|
|
476
496
|
if (opts.install) {
|
|
477
497
|
const dir = join3(process.cwd(), ".claude", "commands");
|
|
478
498
|
if (!existsSync3(dir)) {
|
|
@@ -527,7 +547,7 @@ var contextCommand = new Command7("context").description("Output project context
|
|
|
527
547
|
});
|
|
528
548
|
|
|
529
549
|
// cli/commands/watch.ts
|
|
530
|
-
import { Command as
|
|
550
|
+
import { Command as Command9 } from "commander";
|
|
531
551
|
import { spawn as spawn4, exec } from "child_process";
|
|
532
552
|
import { randomUUID } from "crypto";
|
|
533
553
|
import { resolve as resolve2 } from "path";
|
|
@@ -1950,11 +1970,18 @@ function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs
|
|
|
1950
1970
|
const workingDir = executionDir ?? repoDir;
|
|
1951
1971
|
const prBodyPath = "/tmp/mr-pr-body.md";
|
|
1952
1972
|
const hasAttachedBranch = !!task.attachedBranch?.trim();
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1973
|
+
const hasPrd = !!task.prdContent?.trim();
|
|
1974
|
+
const notes = hasPrd ? [
|
|
1975
|
+
``,
|
|
1976
|
+
``,
|
|
1977
|
+
`## Approved PRD (Product Requirements Document)`,
|
|
1978
|
+
``,
|
|
1979
|
+
`A PRD for this task has been generated and approved by the user. Your job now is to IMPLEMENT the PRD below in code. The planning/proposal phase is already complete \u2014 do not produce another plan or proposal.`,
|
|
1980
|
+
``,
|
|
1981
|
+
`Even if the task title includes words like "proposal", "plan", "design", "spec", or "PRD", treat the PRD below as the finalized spec and deliver working code that fulfills it. Only signal "no MR/PR needed" if the PRD itself explicitly calls out that no code changes are required.`,
|
|
1982
|
+
``,
|
|
1983
|
+
task.prdContent
|
|
1984
|
+
].join("\n") : task.notes ? `
|
|
1958
1985
|
|
|
1959
1986
|
Task notes:
|
|
1960
1987
|
${task.notes}` : "";
|
|
@@ -2041,6 +2068,10 @@ ${task.notes}` : "";
|
|
|
2041
2068
|
`\`mr no-mr ${task.id} "Brief description of what was done instead"\``,
|
|
2042
2069
|
``,
|
|
2043
2070
|
`This tells the watch system to skip looking for a ${vcs === "gitlab" ? "MR" : "PR"} and records what action was taken. You should still clean up any worktrees and exit normally.`,
|
|
2071
|
+
...hasPrd ? [
|
|
2072
|
+
``,
|
|
2073
|
+
`**Important:** This task has an approved PRD, which means the user has already committed to implementation. Do NOT use \`mr no-mr\` just because the task title mentions "proposal", "plan", or because a PRD already exists \u2014 the PRD is the starting point for your implementation, not the deliverable. Only use \`mr no-mr\` if the PRD itself explicitly states that no code changes are required.`
|
|
2074
|
+
] : [],
|
|
2044
2075
|
``,
|
|
2045
2076
|
...hasFeedback || hasAttachedBranch && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink) ? [] : [
|
|
2046
2077
|
`## PR Description Template`,
|
|
@@ -2560,7 +2591,7 @@ function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name
|
|
|
2560
2591
|
});
|
|
2561
2592
|
return child;
|
|
2562
2593
|
}
|
|
2563
|
-
var watchCommand = new
|
|
2594
|
+
var watchCommand = new Command9("watch").description(
|
|
2564
2595
|
"Watch for in-progress tasks and autonomously dispatch an AI coding agent to work on them"
|
|
2565
2596
|
).option("--interval <seconds>", "Polling interval in seconds", "15").option("--dry-run", "Show what would be dispatched without spawning the agent", false).option("--plan-approval", "Show the agent's plan and ask for approval before executing", false).option("--root <dir>", "Root directory filter for linked repos (default: cwd)").option("--agent <agent>", "AI agent to use: claude, codex, or gemini", "claude").option("--scan-at <HH:MM>", "Run a product scan daily at this time (e.g., 02:00)").action(async (opts) => {
|
|
2566
2597
|
const intervalMs = parseInt(opts.interval, 10) * 1e3;
|
|
@@ -4066,7 +4097,7 @@ ${timestamp()} ${watchTag()} Shutting down\u2026`);
|
|
|
4066
4097
|
});
|
|
4067
4098
|
|
|
4068
4099
|
// cli/commands/start.ts
|
|
4069
|
-
import { Command as
|
|
4100
|
+
import { Command as Command10 } from "commander";
|
|
4070
4101
|
var c2 = {
|
|
4071
4102
|
reset: "\x1B[0m",
|
|
4072
4103
|
bold: "\x1B[1m",
|
|
@@ -4092,19 +4123,19 @@ function printTaskBanner(action, title, id) {
|
|
|
4092
4123
|
``
|
|
4093
4124
|
].join("\n"));
|
|
4094
4125
|
}
|
|
4095
|
-
var startCommand = new
|
|
4126
|
+
var startCommand = new Command10("start").description("Mark a task as in-progress (you are working on it)").argument("<task-id>", "Task ID to start").action(async (taskId) => {
|
|
4096
4127
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
4097
4128
|
status: "in_progress"
|
|
4098
4129
|
});
|
|
4099
4130
|
printTaskBanner(paint2("green", "start"), task.title, task.id);
|
|
4100
4131
|
});
|
|
4101
|
-
var delegateCommand = new
|
|
4132
|
+
var delegateCommand = new Command10("delegate").description("Queue a task for the watch agent to pick up").argument("<task-id>", "Task ID to delegate").action(async (taskId) => {
|
|
4102
4133
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
4103
4134
|
status: "queued"
|
|
4104
4135
|
});
|
|
4105
4136
|
printTaskBanner(paint2("cyan", "delegate"), task.title, task.id);
|
|
4106
4137
|
});
|
|
4107
|
-
var undelegateCommand = new
|
|
4138
|
+
var undelegateCommand = new Command10("undelegate").description("Remove delegation from a task and return to todo").argument("<task-id>", "Task ID to undelegate").action(async (taskId) => {
|
|
4108
4139
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
4109
4140
|
status: "todo",
|
|
4110
4141
|
mode: "development"
|
|
@@ -4113,8 +4144,8 @@ var undelegateCommand = new Command9("undelegate").description("Remove delegatio
|
|
|
4113
4144
|
});
|
|
4114
4145
|
|
|
4115
4146
|
// cli/commands/create.ts
|
|
4116
|
-
import { Command as
|
|
4117
|
-
var createCommand = new
|
|
4147
|
+
import { Command as Command11 } from "commander";
|
|
4148
|
+
var createCommand = new Command11("create").description("Create a new task in the linked project").argument("<title>", "Task title").option("--notes <notes>", "Task notes").option("--in-progress", "Mark the task as in-progress immediately").action(async (title, opts) => {
|
|
4118
4149
|
const projectId = getLinkedProjectId();
|
|
4119
4150
|
if (!projectId) {
|
|
4120
4151
|
console.error(
|
|
@@ -4133,7 +4164,7 @@ var createCommand = new Command10("create").description("Create a new task in th
|
|
|
4133
4164
|
});
|
|
4134
4165
|
|
|
4135
4166
|
// cli/commands/complete.ts
|
|
4136
|
-
import { Command as
|
|
4167
|
+
import { Command as Command12 } from "commander";
|
|
4137
4168
|
var c3 = {
|
|
4138
4169
|
reset: "\x1B[0m",
|
|
4139
4170
|
bold: "\x1B[1m",
|
|
@@ -4159,7 +4190,7 @@ function printTaskBanner2(action, title, id) {
|
|
|
4159
4190
|
``
|
|
4160
4191
|
].join("\n"));
|
|
4161
4192
|
}
|
|
4162
|
-
var completeCommand = new
|
|
4193
|
+
var completeCommand = new Command12("complete").description("Mark a task as completed").argument("<task-id>", "Task ID to complete").action(async (taskId) => {
|
|
4163
4194
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
4164
4195
|
status: "completed"
|
|
4165
4196
|
});
|
|
@@ -4167,8 +4198,8 @@ var completeCommand = new Command11("complete").description("Mark a task as comp
|
|
|
4167
4198
|
});
|
|
4168
4199
|
|
|
4169
4200
|
// cli/commands/subtask-complete.ts
|
|
4170
|
-
import { Command as
|
|
4171
|
-
var subtaskCompleteCommand = new
|
|
4201
|
+
import { Command as Command13 } from "commander";
|
|
4202
|
+
var subtaskCompleteCommand = new Command13("subtask-complete").description("Mark a subtask as completed").argument("<task-id>", "Parent task ID").argument("<subtask-id>", "Subtask ID to complete").action(async (taskId, subtaskId) => {
|
|
4172
4203
|
const subtask = await api.patch(
|
|
4173
4204
|
`/api/tasks/${taskId}/subtasks/${subtaskId}`,
|
|
4174
4205
|
{ completed: true }
|
|
@@ -4177,7 +4208,7 @@ var subtaskCompleteCommand = new Command12("subtask-complete").description("Mark
|
|
|
4177
4208
|
});
|
|
4178
4209
|
|
|
4179
4210
|
// cli/commands/prototype.ts
|
|
4180
|
-
import { Command as
|
|
4211
|
+
import { Command as Command14 } from "commander";
|
|
4181
4212
|
var c4 = {
|
|
4182
4213
|
reset: "\x1B[0m",
|
|
4183
4214
|
bold: "\x1B[1m",
|
|
@@ -4206,8 +4237,8 @@ function statusBadge(status) {
|
|
|
4206
4237
|
return paint4("gray", status);
|
|
4207
4238
|
}
|
|
4208
4239
|
}
|
|
4209
|
-
var prototypeCommand = new
|
|
4210
|
-
new
|
|
4240
|
+
var prototypeCommand = new Command14("prototype").description("Manage prototypes").addCommand(
|
|
4241
|
+
new Command14("list").description("List prototypes for the linked project").option("--all", "Show prototypes for all projects").action(async (opts) => {
|
|
4211
4242
|
const params = new URLSearchParams();
|
|
4212
4243
|
if (!opts.all) {
|
|
4213
4244
|
const projectId = getLinkedProjectId();
|
|
@@ -4240,7 +4271,7 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4240
4271
|
}
|
|
4241
4272
|
})
|
|
4242
4273
|
).addCommand(
|
|
4243
|
-
new
|
|
4274
|
+
new Command14("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project, when available)").option("--variants <count>", "Number of variants to generate (1-50)", "5").option("--type <type>", "Prototype type: web_app, mobile_app, desktop_app, logo (default: web_app)", "web_app").action(async (title, opts) => {
|
|
4244
4275
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
4245
4276
|
const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
|
|
4246
4277
|
const validTypes = ["web_app", "mobile_app", "desktop_app", "logo"];
|
|
@@ -4269,7 +4300,7 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4269
4300
|
console.log();
|
|
4270
4301
|
})
|
|
4271
4302
|
).addCommand(
|
|
4272
|
-
new
|
|
4303
|
+
new Command14("start").description("Start prototype generation (sets status to in_progress)").argument("<id>", "Prototype ID").action(async (id) => {
|
|
4273
4304
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
4274
4305
|
status: "in_progress"
|
|
4275
4306
|
});
|
|
@@ -4279,7 +4310,7 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4279
4310
|
console.log();
|
|
4280
4311
|
})
|
|
4281
4312
|
).addCommand(
|
|
4282
|
-
new
|
|
4313
|
+
new Command14("retry").description("Retry a failed prototype").argument("<id>", "Prototype ID").action(async (id) => {
|
|
4283
4314
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
4284
4315
|
status: "in_progress",
|
|
4285
4316
|
files: null
|
|
@@ -4291,7 +4322,7 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4291
4322
|
);
|
|
4292
4323
|
|
|
4293
4324
|
// cli/commands/setup.ts
|
|
4294
|
-
import { Command as
|
|
4325
|
+
import { Command as Command15 } from "commander";
|
|
4295
4326
|
import { exec as exec2 } from "child_process";
|
|
4296
4327
|
var c5 = {
|
|
4297
4328
|
reset: "\x1B[0m",
|
|
@@ -4594,7 +4625,7 @@ async function autoFix(checks, agent) {
|
|
|
4594
4625
|
console.log("");
|
|
4595
4626
|
}
|
|
4596
4627
|
}
|
|
4597
|
-
var setupCommand = new
|
|
4628
|
+
var setupCommand = new Command15("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or gemini (default: claude)", "claude").action(async (opts) => {
|
|
4598
4629
|
const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
|
|
4599
4630
|
const banner = [
|
|
4600
4631
|
``,
|
|
@@ -4644,8 +4675,8 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
|
|
|
4644
4675
|
});
|
|
4645
4676
|
|
|
4646
4677
|
// cli/commands/update.ts
|
|
4647
|
-
import { Command as
|
|
4648
|
-
var updateCommand = new
|
|
4678
|
+
import { Command as Command16 } from "commander";
|
|
4679
|
+
var updateCommand = new Command16("update").description("Post a status update to a task, or attach a resource").argument("<task-id>", "Task ID").argument("[message-or-title]", "Status update message, or resource title when using --resource").argument("[content]", "Resource content (only used with --resource)").option("--source <source>", "Update source: agent, system, or user", "agent").option("--resource <type>", "Create a task resource (e.g. test-plan, note, plan, research)").action(async (taskId, messageOrTitle, content, opts) => {
|
|
4649
4680
|
if (opts.resource) {
|
|
4650
4681
|
if (!messageOrTitle || !content) {
|
|
4651
4682
|
console.error(`Usage: mr update <task-id> --resource <type> "<title>" '<content>'`);
|
|
@@ -4671,11 +4702,11 @@ var updateCommand = new Command15("update").description("Post a status update to
|
|
|
4671
4702
|
});
|
|
4672
4703
|
|
|
4673
4704
|
// cli/commands/screenshot.ts
|
|
4674
|
-
import { Command as
|
|
4705
|
+
import { Command as Command17 } from "commander";
|
|
4675
4706
|
import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
4676
4707
|
import { join as join7 } from "path";
|
|
4677
4708
|
import { tmpdir } from "os";
|
|
4678
|
-
var screenshotCommand = new
|
|
4709
|
+
var screenshotCommand = new Command17("screenshot").description(
|
|
4679
4710
|
"Take or attach a screenshot to a task update (agents use this to show their work)"
|
|
4680
4711
|
).argument("<task-id>", "Task ID").argument("[file]", "Path to an image file (if omitted, uses headless browser to screenshot the app)").option("-m, --message <message>", "Optional message to include with the screenshot").option("-u, --url <url>", "Custom URL to screenshot (defaults to the task's project page)").action(async (taskId, file, opts) => {
|
|
4681
4712
|
let filePath = file;
|
|
@@ -4766,7 +4797,7 @@ var screenshotCommand = new Command16("screenshot").description(
|
|
|
4766
4797
|
});
|
|
4767
4798
|
|
|
4768
4799
|
// cli/commands/resume.ts
|
|
4769
|
-
import { Command as
|
|
4800
|
+
import { Command as Command18 } from "commander";
|
|
4770
4801
|
import { spawn as spawn5 } from "child_process";
|
|
4771
4802
|
import { resolve as resolve3 } from "path";
|
|
4772
4803
|
var c6 = {
|
|
@@ -4783,7 +4814,7 @@ var c6 = {
|
|
|
4783
4814
|
function paint6(color, text) {
|
|
4784
4815
|
return `${c6[color]}${text}${c6.reset}`;
|
|
4785
4816
|
}
|
|
4786
|
-
var resumeCommand = new
|
|
4817
|
+
var resumeCommand = new Command18("resume").description("Resume an interactive Claude session for a task (non-headless)").argument("<task-id>", "Task ID whose Claude session to resume").option("--dir <directory>", "Override the working directory for the session").action(async (taskId, opts) => {
|
|
4787
4818
|
const task = await api.get(`/api/tasks/${taskId}`);
|
|
4788
4819
|
if (!task.claudeSessionId) {
|
|
4789
4820
|
console.error(
|
|
@@ -4852,7 +4883,7 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
|
|
|
4852
4883
|
});
|
|
4853
4884
|
|
|
4854
4885
|
// cli/commands/browse.ts
|
|
4855
|
-
import { Command as
|
|
4886
|
+
import { Command as Command19 } from "commander";
|
|
4856
4887
|
import { execSync as execSync4, spawn as spawn6 } from "child_process";
|
|
4857
4888
|
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
4858
4889
|
import { createHash } from "crypto";
|
|
@@ -4967,7 +4998,7 @@ async function ensureDevServer(options = {}) {
|
|
|
4967
4998
|
}
|
|
4968
4999
|
throw new Error(`Dev server failed to start within 60s. Command: ${devCmd} in ${projectCwd}`);
|
|
4969
5000
|
}
|
|
4970
|
-
var browseCommand = new
|
|
5001
|
+
var browseCommand = new Command19("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
|
|
4971
5002
|
"--task-id <id>",
|
|
4972
5003
|
"Attach output to a task update (for screenshot and recording-stop commands)"
|
|
4973
5004
|
).option("--dev", "Auto-start local dev server before browsing").option("--dev-cwd <path>", "Working directory for the dev server (defaults to mr-manager root)").option("--dev-cmd <command>", "Dev server command to run (auto-detected from package.json if omitted)").option("--dev-port-flag <flag>", "CLI flag name used to set port (e.g. --port). Omit to use PORT env var.").allowUnknownOption(true).action(
|
|
@@ -5110,10 +5141,10 @@ var browseCommand = new Command18("browse").description("Control a headless brow
|
|
|
5110
5141
|
);
|
|
5111
5142
|
|
|
5112
5143
|
// cli/commands/set-path.ts
|
|
5113
|
-
import { Command as
|
|
5144
|
+
import { Command as Command20 } from "commander";
|
|
5114
5145
|
import { resolve as resolve5 } from "path";
|
|
5115
5146
|
import { existsSync as existsSync10 } from "fs";
|
|
5116
|
-
var setPathCommand = new
|
|
5147
|
+
var setPathCommand = new Command20("set-path").description("Set or update the local repo path for a project").argument("<project-id>", "Project ID").argument("<path>", "Absolute or relative path to the local repo").action(async (projectId, pathArg) => {
|
|
5117
5148
|
const absolutePath = resolve5(pathArg);
|
|
5118
5149
|
if (!existsSync10(absolutePath)) {
|
|
5119
5150
|
console.error(`Error: Path does not exist: ${absolutePath}`);
|
|
@@ -5131,9 +5162,9 @@ var setPathCommand = new Command19("set-path").description("Set or update the lo
|
|
|
5131
5162
|
});
|
|
5132
5163
|
|
|
5133
5164
|
// cli/commands/test.ts
|
|
5134
|
-
import { Command as
|
|
5165
|
+
import { Command as Command21 } from "commander";
|
|
5135
5166
|
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
5136
|
-
var testCommand = new
|
|
5167
|
+
var testCommand = new Command21("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").option("--no-recording", "Disable proof recording for this run").action(async (taskId, opts) => {
|
|
5137
5168
|
const config = loadConfig();
|
|
5138
5169
|
console.log("[test] Fetching task...");
|
|
5139
5170
|
let task;
|
|
@@ -5295,7 +5326,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
5295
5326
|
});
|
|
5296
5327
|
|
|
5297
5328
|
// cli/commands/features.ts
|
|
5298
|
-
import { Command as
|
|
5329
|
+
import { Command as Command22 } from "commander";
|
|
5299
5330
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
|
|
5300
5331
|
import { resolve as resolve6, sep as sep2 } from "path";
|
|
5301
5332
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
@@ -5329,7 +5360,7 @@ function readFeatures2() {
|
|
|
5329
5360
|
if (!existsSync12(path)) return null;
|
|
5330
5361
|
return readFileSync9(path, "utf-8");
|
|
5331
5362
|
}
|
|
5332
|
-
var featuresCommand = new
|
|
5363
|
+
var featuresCommand = new Command22("features").description("View or update the project features & goals document (.mr-features.md)").option("--update <content>", "Replace the features document with the given content").option("--file <path>", "Read content from a file and use it to update the features document").option("--path", "Print the path to the features file").action(async (opts) => {
|
|
5333
5364
|
if (opts.path) {
|
|
5334
5365
|
console.log(getFeaturesPath());
|
|
5335
5366
|
return;
|
|
@@ -5357,11 +5388,11 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
5357
5388
|
});
|
|
5358
5389
|
|
|
5359
5390
|
// cli/commands/no-mr.ts
|
|
5360
|
-
import { Command as
|
|
5391
|
+
import { Command as Command23 } from "commander";
|
|
5361
5392
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
5362
5393
|
import { resolve as resolve7 } from "path";
|
|
5363
5394
|
var NO_MR_FILE = ".mr-no-mr";
|
|
5364
|
-
var noMrCommand = new
|
|
5395
|
+
var noMrCommand = new Command23("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
|
|
5365
5396
|
const filePath = resolve7(process.cwd(), NO_MR_FILE);
|
|
5366
5397
|
writeFileSync6(filePath, description, "utf-8");
|
|
5367
5398
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
@@ -5373,8 +5404,9 @@ var noMrCommand = new Command22("no-mr").description("Signal that a task does no
|
|
|
5373
5404
|
});
|
|
5374
5405
|
|
|
5375
5406
|
// cli/commands/review.ts
|
|
5376
|
-
import { Command as
|
|
5407
|
+
import { Command as Command24 } from "commander";
|
|
5377
5408
|
import { spawn as spawn7, execSync as execSync5 } from "child_process";
|
|
5409
|
+
import { existsSync as existsSync13, statSync as statSync2 } from "fs";
|
|
5378
5410
|
var c8 = {
|
|
5379
5411
|
reset: "\x1B[0m",
|
|
5380
5412
|
bold: "\x1B[1m",
|
|
@@ -5405,7 +5437,7 @@ function logOk(msg) {
|
|
|
5405
5437
|
function logErr(msg) {
|
|
5406
5438
|
console.error(`${timestamp2()} ${tag()} ${paint8("red", "\u2717")} ${msg}`);
|
|
5407
5439
|
}
|
|
5408
|
-
var reviewCommand = new
|
|
5440
|
+
var reviewCommand = new Command24("review").description("Run an automated code review on a branch").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing review report ID (created by UI trigger)").option("--branch <name>", "Branch to review (defaults to current branch)").option("--base <name>", "Base branch to diff against (defaults to main)").action(async (opts) => {
|
|
5409
5441
|
const config = loadConfig();
|
|
5410
5442
|
if (!config.apiKey) {
|
|
5411
5443
|
logErr('Not authenticated. Run "mr login" first.');
|
|
@@ -5433,18 +5465,48 @@ var reviewCommand = new Command23("review").description("Run an automated code r
|
|
|
5433
5465
|
logErr(`Failed to fetch project ${projectId}`);
|
|
5434
5466
|
process.exit(1);
|
|
5435
5467
|
}
|
|
5436
|
-
|
|
5437
|
-
if (
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5468
|
+
const candidates = [];
|
|
5469
|
+
if (project.localPath) candidates.push({ path: project.localPath, source: "project.localPath" });
|
|
5470
|
+
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
5471
|
+
if (pid === projectId) candidates.push({ path: dir, source: "linked directory" });
|
|
5472
|
+
}
|
|
5473
|
+
candidates.push({ path: process.cwd(), source: "cwd" });
|
|
5474
|
+
let projectPath;
|
|
5475
|
+
let pathSource = "";
|
|
5476
|
+
const triedPaths = [];
|
|
5477
|
+
for (const { path, source } of candidates) {
|
|
5478
|
+
if (existsSync13(path)) {
|
|
5479
|
+
projectPath = path;
|
|
5480
|
+
pathSource = source;
|
|
5481
|
+
break;
|
|
5443
5482
|
}
|
|
5483
|
+
triedPaths.push(`${path} (${source})`);
|
|
5444
5484
|
}
|
|
5445
5485
|
if (!projectPath) {
|
|
5446
|
-
|
|
5486
|
+
logErr(`No valid project path found. Tried:`);
|
|
5487
|
+
for (const p of triedPaths) logErr(` - ${p}`);
|
|
5488
|
+
logErr(`Run "mr link" from the correct directory, or update the project's localPath.`);
|
|
5489
|
+
process.exit(1);
|
|
5490
|
+
}
|
|
5491
|
+
if (triedPaths.length > 0) {
|
|
5492
|
+
log(paint8("yellow", `Skipped stale path(s): ${triedPaths.join(", ")}`));
|
|
5493
|
+
log(`Falling back to ${pathSource}`);
|
|
5447
5494
|
}
|
|
5495
|
+
try {
|
|
5496
|
+
if (!statSync2(projectPath).isDirectory()) {
|
|
5497
|
+
logErr(`Project path is not a directory: ${projectPath}`);
|
|
5498
|
+
process.exit(1);
|
|
5499
|
+
}
|
|
5500
|
+
} catch (err) {
|
|
5501
|
+
logErr(`Cannot stat project path ${projectPath}: ${err.message}`);
|
|
5502
|
+
process.exit(1);
|
|
5503
|
+
}
|
|
5504
|
+
if (!existsSync13(`${projectPath}/.git`)) {
|
|
5505
|
+
logErr(`Project path is not a git repository: ${projectPath}`);
|
|
5506
|
+
logErr(`Update the project's localPath to point to the local checkout.`);
|
|
5507
|
+
process.exit(1);
|
|
5508
|
+
}
|
|
5509
|
+
log(`Using project path: ${paint8("dim", projectPath)}`);
|
|
5448
5510
|
let branch = opts.branch;
|
|
5449
5511
|
if (!branch) {
|
|
5450
5512
|
try {
|
|
@@ -5691,13 +5753,13 @@ function parseReviewOutput(output) {
|
|
|
5691
5753
|
}
|
|
5692
5754
|
|
|
5693
5755
|
// cli/commands/scan.ts
|
|
5694
|
-
import { Command as
|
|
5756
|
+
import { Command as Command25 } from "commander";
|
|
5695
5757
|
|
|
5696
5758
|
// lib/scanner/index.ts
|
|
5697
5759
|
import { spawn as spawn8 } from "child_process";
|
|
5698
5760
|
|
|
5699
5761
|
// lib/scanner/config.ts
|
|
5700
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
5762
|
+
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
5701
5763
|
import { join as join9 } from "path";
|
|
5702
5764
|
var ALL_FINDING_TYPES = [
|
|
5703
5765
|
"idea",
|
|
@@ -5715,7 +5777,7 @@ var DEFAULTS = {
|
|
|
5715
5777
|
};
|
|
5716
5778
|
function loadScanConfig(projectPath) {
|
|
5717
5779
|
const configPath2 = join9(projectPath, ".mr-scan.json");
|
|
5718
|
-
if (!
|
|
5780
|
+
if (!existsSync14(configPath2)) {
|
|
5719
5781
|
return { ...DEFAULTS };
|
|
5720
5782
|
}
|
|
5721
5783
|
try {
|
|
@@ -5761,13 +5823,13 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
5761
5823
|
}
|
|
5762
5824
|
|
|
5763
5825
|
// lib/scanner/codebase-analysis.ts
|
|
5764
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as
|
|
5826
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync15 } from "fs";
|
|
5765
5827
|
import { join as join10, relative } from "path";
|
|
5766
5828
|
import { execSync as execSync6 } from "child_process";
|
|
5767
5829
|
function resolveDir(projectPath, candidates) {
|
|
5768
5830
|
for (const candidate of candidates) {
|
|
5769
5831
|
const dir = join10(projectPath, candidate);
|
|
5770
|
-
if (
|
|
5832
|
+
if (existsSync15(dir)) return dir;
|
|
5771
5833
|
}
|
|
5772
5834
|
return null;
|
|
5773
5835
|
}
|
|
@@ -5801,7 +5863,7 @@ function discoverRoutes(projectPath) {
|
|
|
5801
5863
|
}
|
|
5802
5864
|
function extractModels(projectPath) {
|
|
5803
5865
|
const schemaPath = join10(projectPath, "prisma", "schema.prisma");
|
|
5804
|
-
if (
|
|
5866
|
+
if (existsSync15(schemaPath)) {
|
|
5805
5867
|
const content = readFileSync11(schemaPath, "utf-8");
|
|
5806
5868
|
const models2 = [];
|
|
5807
5869
|
const modelRegex = /^model\s+(\w+)\s*\{/gm;
|
|
@@ -5815,7 +5877,7 @@ function extractModels(projectPath) {
|
|
|
5815
5877
|
const drizzleDirs = ["src/db", "src/schema", "db", "drizzle"];
|
|
5816
5878
|
for (const dir of drizzleDirs) {
|
|
5817
5879
|
const fullDir = join10(projectPath, dir);
|
|
5818
|
-
if (!
|
|
5880
|
+
if (!existsSync15(fullDir)) continue;
|
|
5819
5881
|
try {
|
|
5820
5882
|
const entries = readdirSync2(fullDir, { withFileTypes: true });
|
|
5821
5883
|
for (const entry of entries) {
|
|
@@ -5856,7 +5918,7 @@ function discoverComponents(projectPath) {
|
|
|
5856
5918
|
function extractInternalLinks(projectPath) {
|
|
5857
5919
|
const links = /* @__PURE__ */ new Set();
|
|
5858
5920
|
function searchDir(dir) {
|
|
5859
|
-
if (!
|
|
5921
|
+
if (!existsSync15(dir)) return;
|
|
5860
5922
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
5861
5923
|
for (const entry of entries) {
|
|
5862
5924
|
if (entry.isDirectory()) {
|
|
@@ -6453,7 +6515,7 @@ function logOk2(msg) {
|
|
|
6453
6515
|
function logErr2(msg) {
|
|
6454
6516
|
console.error(`${timestamp3()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
|
|
6455
6517
|
}
|
|
6456
|
-
var scanCommand = new
|
|
6518
|
+
var scanCommand = new Command25("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
|
|
6457
6519
|
const config = loadConfig();
|
|
6458
6520
|
if (!config.apiKey) {
|
|
6459
6521
|
logErr2('Not authenticated. Run "mr login" first.');
|
|
@@ -6608,13 +6670,13 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
6608
6670
|
});
|
|
6609
6671
|
|
|
6610
6672
|
// cli/commands/doctor.ts
|
|
6611
|
-
import { Command as
|
|
6612
|
-
import { existsSync as
|
|
6673
|
+
import { Command as Command26 } from "commander";
|
|
6674
|
+
import { existsSync as existsSync16 } from "fs";
|
|
6613
6675
|
import { homedir as homedir2 } from "os";
|
|
6614
6676
|
import { join as join11 } from "path";
|
|
6615
6677
|
async function checkConfigExists() {
|
|
6616
6678
|
const configPath2 = join11(homedir2(), ".mr-manager", "config.json");
|
|
6617
|
-
const exists =
|
|
6679
|
+
const exists = existsSync16(configPath2);
|
|
6618
6680
|
if (!exists) {
|
|
6619
6681
|
return {
|
|
6620
6682
|
name: "Config file",
|
|
@@ -6656,7 +6718,7 @@ async function checkProjectLink() {
|
|
|
6656
6718
|
optional: true
|
|
6657
6719
|
};
|
|
6658
6720
|
}
|
|
6659
|
-
var doctorCommand = new
|
|
6721
|
+
var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
6660
6722
|
const banner = [
|
|
6661
6723
|
``,
|
|
6662
6724
|
paint5("cyan", ` MR DOCTOR`),
|
|
@@ -6699,14 +6761,14 @@ var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CL
|
|
|
6699
6761
|
});
|
|
6700
6762
|
|
|
6701
6763
|
// cli/commands/prompt-audit.ts
|
|
6702
|
-
import { Command as
|
|
6764
|
+
import { Command as Command27 } from "commander";
|
|
6703
6765
|
import { resolve as resolve8 } from "path";
|
|
6704
|
-
import { existsSync as
|
|
6766
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
|
|
6705
6767
|
function auditLine(label, tokens) {
|
|
6706
6768
|
const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
|
|
6707
6769
|
return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
|
|
6708
6770
|
}
|
|
6709
|
-
var promptAuditCommand = new
|
|
6771
|
+
var promptAuditCommand = new Command27("prompt-audit").description("Dry-run prompt construction and report estimated token counts by job type").option("--task <id>", "Audit prompts for a specific task ID").option("--all", "Audit all supported job types with representative data", false).option("--json", "Output as JSON instead of plain text", false).action(async (opts) => {
|
|
6710
6772
|
const results = [];
|
|
6711
6773
|
if (opts.task) {
|
|
6712
6774
|
try {
|
|
@@ -6722,7 +6784,11 @@ var promptAuditCommand = new Command26("prompt-audit").description("Dry-run prom
|
|
|
6722
6784
|
const sections = [];
|
|
6723
6785
|
const notesContent = task.prdContent ? `
|
|
6724
6786
|
|
|
6725
|
-
## PRD (Product Requirements Document)
|
|
6787
|
+
## Approved PRD (Product Requirements Document)
|
|
6788
|
+
|
|
6789
|
+
A PRD for this task has been generated and approved by the user. Your job now is to IMPLEMENT the PRD below in code. The planning/proposal phase is already complete \u2014 do not produce another plan or proposal.
|
|
6790
|
+
|
|
6791
|
+
Even if the task title includes words like "proposal", "plan", "design", "spec", or "PRD", treat the PRD below as the finalized spec and deliver working code that fulfills it. Only signal "no MR/PR needed" if the PRD itself explicitly calls out that no code changes are required.
|
|
6726
6792
|
|
|
6727
6793
|
${task.prdContent}` : task.notes ? `
|
|
6728
6794
|
|
|
@@ -6756,7 +6822,7 @@ ${task.notes}` : "";
|
|
|
6756
6822
|
const repoDir = Object.entries(config.directories).find(([, pid]) => pid === task.projectId)?.[0];
|
|
6757
6823
|
if (repoDir) {
|
|
6758
6824
|
const featuresPath = resolve8(repoDir, ".mr-features.md");
|
|
6759
|
-
if (
|
|
6825
|
+
if (existsSync17(featuresPath)) {
|
|
6760
6826
|
const featuresContent = readFileSync12(featuresPath, "utf-8");
|
|
6761
6827
|
sections.push({ name: "features-doc", tokens: estimateTokens(featuresContent) });
|
|
6762
6828
|
}
|
|
@@ -6911,7 +6977,7 @@ ${r.jobType} [${r.identifier}]`);
|
|
|
6911
6977
|
});
|
|
6912
6978
|
|
|
6913
6979
|
// cli/commands/skill.ts
|
|
6914
|
-
import { Command as
|
|
6980
|
+
import { Command as Command28 } from "commander";
|
|
6915
6981
|
var c10 = {
|
|
6916
6982
|
reset: "\x1B[0m",
|
|
6917
6983
|
bold: "\x1B[1m",
|
|
@@ -6920,7 +6986,7 @@ var c10 = {
|
|
|
6920
6986
|
green: "\x1B[32m",
|
|
6921
6987
|
yellow: "\x1B[33m"
|
|
6922
6988
|
};
|
|
6923
|
-
var skillCommand = new
|
|
6989
|
+
var skillCommand = new Command28("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
|
|
6924
6990
|
skillCommand.command("list").alias("ls").description("List all skills").option("--category <category>", "Filter by category").action(async (opts) => {
|
|
6925
6991
|
const params = new URLSearchParams();
|
|
6926
6992
|
if (opts.category) params.set("category", opts.category);
|
|
@@ -7011,7 +7077,7 @@ skillCommand.command("generate").alias("gen").description("Generate a new skill
|
|
|
7011
7077
|
});
|
|
7012
7078
|
|
|
7013
7079
|
// cli/commands/resource.ts
|
|
7014
|
-
import { Command as
|
|
7080
|
+
import { Command as Command29 } from "commander";
|
|
7015
7081
|
var c11 = {
|
|
7016
7082
|
reset: "\x1B[0m",
|
|
7017
7083
|
bold: "\x1B[1m",
|
|
@@ -7031,7 +7097,7 @@ function typeLabel(type) {
|
|
|
7031
7097
|
const color = TYPE_COLORS[type] ?? c11.dim;
|
|
7032
7098
|
return `${color}${type}${c11.reset}`;
|
|
7033
7099
|
}
|
|
7034
|
-
var resourceCommand = new
|
|
7100
|
+
var resourceCommand = new Command29("resource").description("Manage resources \u2014 documents, plans, research, and notes");
|
|
7035
7101
|
resourceCommand.command("list").alias("ls").description("List resources for the linked project (or all)").option("--all", "List all resources across projects").action(async (opts) => {
|
|
7036
7102
|
const params = new URLSearchParams();
|
|
7037
7103
|
if (opts.all) {
|
|
@@ -7129,13 +7195,13 @@ resourceCommand.command("generate").alias("gen").description("Generate a resourc
|
|
|
7129
7195
|
});
|
|
7130
7196
|
|
|
7131
7197
|
// cli/commands/tests.ts
|
|
7132
|
-
import { Command as
|
|
7198
|
+
import { Command as Command30 } from "commander";
|
|
7133
7199
|
var c12 = {
|
|
7134
7200
|
reset: "\x1B[0m",
|
|
7135
7201
|
dim: "\x1B[2m",
|
|
7136
7202
|
yellow: "\x1B[33m"
|
|
7137
7203
|
};
|
|
7138
|
-
var testsCommand = new
|
|
7204
|
+
var testsCommand = new Command30("tests").description("List MR Test scenarios for the linked project").action(async () => {
|
|
7139
7205
|
const projectId = getLinkedProjectId();
|
|
7140
7206
|
if (!projectId) {
|
|
7141
7207
|
console.error(
|
|
@@ -7168,7 +7234,7 @@ var testsCommand = new Command29("tests").description("List MR Test scenarios fo
|
|
|
7168
7234
|
|
|
7169
7235
|
// cli/index.ts
|
|
7170
7236
|
var configPath = join12(homedir3(), ".mr-manager", "config.json");
|
|
7171
|
-
var isFirstRun = !
|
|
7237
|
+
var isFirstRun = !existsSync18(configPath);
|
|
7172
7238
|
var userArgs = process.argv.slice(2);
|
|
7173
7239
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
7174
7240
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
@@ -7204,13 +7270,14 @@ if (isFirstRun && !shouldBypass) {
|
|
|
7204
7270
|
console.log("");
|
|
7205
7271
|
process.exit(0);
|
|
7206
7272
|
}
|
|
7207
|
-
var program = new
|
|
7273
|
+
var program = new Command31();
|
|
7208
7274
|
program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
|
|
7209
7275
|
program.addCommand(initCommand);
|
|
7210
7276
|
program.addCommand(authCommand);
|
|
7211
7277
|
program.addCommand(loginCommand);
|
|
7212
7278
|
program.addCommand(projectsCommand);
|
|
7213
7279
|
program.addCommand(tasksCommand);
|
|
7280
|
+
program.addCommand(taskGroupCommand);
|
|
7214
7281
|
program.addCommand(linkCommand);
|
|
7215
7282
|
program.addCommand(unlinkCommand);
|
|
7216
7283
|
program.addCommand(contextCommand);
|