baton-issue-tracker 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/drizzle/0002_breezy_wiccan.sql +10 -0
- package/drizzle/meta/0002_snapshot.json +229 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +2 -1
- package/source/cli.js +31 -5
- package/source/commands/create.js +217 -32
- package/source/commands/log.js +85 -0
- package/source/commands/next.js +3 -3
- package/source/commands/priority.js +102 -0
- package/source/commands/register.js +68 -0
- package/source/commands/update.js +233 -50
- package/source/models/activityLog.js +2 -0
- package/source/models/agents.js +37 -0
- package/source/models/issue.js +2 -4
- package/source/models/schema.js +10 -3
- package/source/services/agentsService.js +42 -0
- package/source/services/authService.js +44 -0
- package/source/services/issuesService.js +33 -12
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// AI was consulted for some portions of this file.
|
|
2
|
+
// log.js
|
|
3
|
+
// log command which displays the full activity history for an issue
|
|
4
|
+
// Usage: baton log <id>
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// -h, --help Show this help
|
|
8
|
+
// --json Output as JSON (for AI agents)
|
|
9
|
+
//
|
|
10
|
+
// Examples:
|
|
11
|
+
// baton log 5
|
|
12
|
+
|
|
13
|
+
import { getActivityLog } from '../services/issuesService.js';
|
|
14
|
+
import { formatTimestamp, hasFlag } from '../util.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Serializes an activity log entry for JSON output.
|
|
18
|
+
* @param {object} entry
|
|
19
|
+
* @returns {object}
|
|
20
|
+
*/
|
|
21
|
+
function serializeLogEntry(entry) {
|
|
22
|
+
return {
|
|
23
|
+
log_id: entry.logId,
|
|
24
|
+
issue_id: entry.issueId,
|
|
25
|
+
action: entry.action,
|
|
26
|
+
details: entry.details ?? null,
|
|
27
|
+
created_at: entry.createdAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Displays the full activity history for a given issue ID.
|
|
33
|
+
* @param {string[]} args - The issue ID
|
|
34
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
35
|
+
*/
|
|
36
|
+
export async function run(args) {
|
|
37
|
+
const isJson = hasFlag(args, '--json');
|
|
38
|
+
const idArgs = args.filter((arg) => arg !== '--json');
|
|
39
|
+
|
|
40
|
+
if (idArgs.length === 0) {
|
|
41
|
+
throw new Error('Invalid input: Missing Issue ID.\nUsage: baton log <id>');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const id = idArgs.join(' ');
|
|
45
|
+
|
|
46
|
+
if (isNaN(id)) {
|
|
47
|
+
throw new Error('Invalid input: ID must be a number.\nUsage: baton log <id>');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const logs = getActivityLog(Number(id));
|
|
52
|
+
const entries = logs.map(serializeLogEntry);
|
|
53
|
+
const envelope = {
|
|
54
|
+
status: 'success',
|
|
55
|
+
issue_id: Number(id),
|
|
56
|
+
count: entries.length,
|
|
57
|
+
entries,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (isJson) {
|
|
61
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
console.log(`No activity history for issue #${id}.`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Activity log for issue #${id}`);
|
|
71
|
+
console.log('──────────────────────────────────────────');
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const timestamp = formatTimestamp(entry.created_at);
|
|
74
|
+
const details = entry.details ?? '';
|
|
75
|
+
console.log(`${timestamp} ${entry.action} ${details}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
return 0;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error: Failed to retrieve activity log.');
|
|
82
|
+
console.error(error.message);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
package/source/commands/next.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
isTrackerReady,
|
|
8
8
|
selectNextIssue,
|
|
9
|
-
|
|
9
|
+
claimIssue,
|
|
10
10
|
} from '../services/issuesService.js';
|
|
11
11
|
import { formatTimestamp, hasFlag, renderOutput, reportTrackerNotReady, serializeIssue } from '../util.js';
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ import { formatTimestamp, hasFlag, renderOutput, reportTrackerNotReady, serializ
|
|
|
14
14
|
* Moves the AI agent to work on the next issue.
|
|
15
15
|
* Checks if the tracker is ready and if there are any open issues.
|
|
16
16
|
* Prompts user to initialize the tracker if it is not ready.
|
|
17
|
-
* Stats are updated on the issue through
|
|
17
|
+
* Stats are updated on the issue through claimIssue function from init.js.
|
|
18
18
|
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
19
19
|
*/
|
|
20
20
|
export async function run(args = []) {
|
|
@@ -33,7 +33,7 @@ export async function run(args = []) {
|
|
|
33
33
|
return 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const updated =
|
|
36
|
+
const updated = claimIssue(issue.id);
|
|
37
37
|
const envelope = { status: 'success', issue: serializeIssue(updated) };
|
|
38
38
|
|
|
39
39
|
renderOutput(isJson, envelope, () => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// AI was consulted for some portions of this file.
|
|
2
|
+
// priority.js
|
|
3
|
+
// Sets the priority of an issue.
|
|
4
|
+
// Usage: baton priority <id> <priority>
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// -h, --help Show this help
|
|
8
|
+
// --json Output as JSON (for AI agents)
|
|
9
|
+
//
|
|
10
|
+
// Examples:
|
|
11
|
+
// baton priority 5 high
|
|
12
|
+
// baton priority 3 low
|
|
13
|
+
|
|
14
|
+
import { Priority } from '../models/issue.js';
|
|
15
|
+
import { setPriority } from '../services/issuesService.js';
|
|
16
|
+
import { hasFlag, renderOutput, serializeIssue, wantsHelp } from '../util.js';
|
|
17
|
+
|
|
18
|
+
const HELP = `Usage:
|
|
19
|
+
baton priority <id> <priority>
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
--json Output as JSON (for AI agents)
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
baton priority 5 high
|
|
27
|
+
baton priority 3 low
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize CLI priority input to a canonical Priority enum value.
|
|
32
|
+
* @param {string} input
|
|
33
|
+
* @returns {string | null}
|
|
34
|
+
*/
|
|
35
|
+
function normalizePriority(input) {
|
|
36
|
+
const values = Object.values(Priority);
|
|
37
|
+
return values.find((v) => v.toLowerCase() === input.trim().toLowerCase()) ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sets the priority of an issue.
|
|
42
|
+
* @param {string[]} args - The command line arguments.
|
|
43
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
44
|
+
*/
|
|
45
|
+
export async function run(args) {
|
|
46
|
+
if (wantsHelp(args)) {
|
|
47
|
+
console.log(HELP);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isJson = hasFlag(args, '--json');
|
|
52
|
+
const cmdArgs = args.filter((arg) => arg !== '--json');
|
|
53
|
+
|
|
54
|
+
if (cmdArgs.length < 2) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Invalid input: Missing issue ID or priority.\nUsage: baton priority <id> <priority>'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (cmdArgs.length > 2) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'Invalid input: Too many arguments.\nUsage: baton priority <id> <priority>'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const arg of cmdArgs) {
|
|
67
|
+
if (arg.startsWith('--')) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Unknown flag provided.\nUsage: baton priority <id> <priority>\nPriority: low | medium | high'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const id = parseInt(cmdArgs[0], 10);
|
|
75
|
+
if (Number.isNaN(id)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Invalid input: ID must be a number.\nUsage: baton priority <id> <priority>'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const priority = normalizePriority(cmdArgs[1]);
|
|
82
|
+
if (!priority) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid priority "${cmdArgs[1]}". Must be one of: low, medium, high.\nUsage: baton priority <id> <priority>`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const issue = setPriority(id, priority);
|
|
90
|
+
const envelope = { status: 'success', issue: serializeIssue(issue) };
|
|
91
|
+
|
|
92
|
+
renderOutput(isJson, envelope, () => {
|
|
93
|
+
console.log(`Issue #${issue.id} priority set to ${issue.priority}.`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return 0;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error: Failed to set issue priority.');
|
|
99
|
+
console.error(error.message);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// register.js
|
|
2
|
+
// AI was consulted for some portions of this file.
|
|
3
|
+
// register command which allows the user to register a new AI agent or human
|
|
4
|
+
// Usage: baton register --name <name> [--type <type>] [--json]
|
|
5
|
+
//
|
|
6
|
+
// Options:
|
|
7
|
+
// --name <n> Name of the agent or human (required)
|
|
8
|
+
// --type <t> Type: agent | human (default: agent)
|
|
9
|
+
// --json Output as JSON (for AI agents)
|
|
10
|
+
// -h, --help Show this help
|
|
11
|
+
|
|
12
|
+
import { registerAgent } from '../services/agentsService.js';
|
|
13
|
+
import {
|
|
14
|
+
getFlagValue,
|
|
15
|
+
hasFlag,
|
|
16
|
+
renderOutput,
|
|
17
|
+
} from '../util.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers a new agent or human user
|
|
21
|
+
* @param {string[]} args - The command line arguments
|
|
22
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
23
|
+
*/
|
|
24
|
+
export async function run(args) {
|
|
25
|
+
const isJson = hasFlag(args, '--json');
|
|
26
|
+
const validFlags = ['--name', '--type', '--json'];
|
|
27
|
+
|
|
28
|
+
// Check if user misspelled a flag
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
if (arg.startsWith('--')) {
|
|
31
|
+
if (!validFlags.includes(arg)) {
|
|
32
|
+
throw new Error(`Unknown flag provided: ${arg}. \nFlags: --name <n>, --type <t>, --json`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const name = getFlagValue(args, '--name');
|
|
39
|
+
const type = getFlagValue(args, '--type') || 'agent';
|
|
40
|
+
|
|
41
|
+
if (!name) {
|
|
42
|
+
throw new Error("Missing required flag: --name <name>");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (type !== 'agent' && type !== 'human') {
|
|
46
|
+
throw new Error(`Invalid type "${type}". Must be 'agent' or 'human'.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const agent = registerAgent(name, type);
|
|
50
|
+
const envelope = { status: 'success', agent };
|
|
51
|
+
|
|
52
|
+
renderOutput(isJson, envelope, (data) => {
|
|
53
|
+
console.log(`\nSuccessfully registered ${data.agent.type}: "${data.agent.name}" (ID: ${data.agent.id})\n`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return 0;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.message.includes('UNIQUE constraint failed')) {
|
|
59
|
+
console.error(`Error: An agent or user with the name "${getFlagValue(args, '--name')}" is already registered.`);
|
|
60
|
+
} else if (error.message.includes('Missing required') || error.message.includes('Invalid type')) {
|
|
61
|
+
console.error(`Usage Error: ${error.message}`);
|
|
62
|
+
} else {
|
|
63
|
+
console.error("Error: Failed to register agent.");
|
|
64
|
+
console.error(error.message);
|
|
65
|
+
}
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// update.js
|
|
2
2
|
// AI was consulted for some portions of this file.
|
|
3
|
-
// update command allows the user to update data fields for an issue
|
|
3
|
+
// update command allows the user to update data fields for an issue
|
|
4
4
|
// Usage: baton update <id> [options]
|
|
5
|
+
// baton update <id> (launches interactive mode pre-filled with current values)
|
|
5
6
|
//
|
|
6
7
|
// Options:
|
|
7
8
|
// --title <text> New title
|
|
@@ -12,70 +13,252 @@
|
|
|
12
13
|
// -h, --help Show this help
|
|
13
14
|
//
|
|
14
15
|
// Examples:
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// baton update 3 # interactive mode
|
|
17
|
+
// baton update 3 --title "Revised title"
|
|
18
|
+
// baton update 7 --status closed --priority medium
|
|
17
19
|
|
|
18
20
|
import { updateIssue, getIssue } from "../services/issuesService.js";
|
|
19
|
-
import { hasFlag, parseArgs, renderOutput, serializeIssue } from
|
|
21
|
+
import { hasFlag, parseArgs, renderOutput, serializeIssue } from "../util.js";
|
|
22
|
+
import { issueSchema } from "../models/schema.js";
|
|
23
|
+
import { input, select, confirm, editor } from "@inquirer/prompts";
|
|
24
|
+
import { spawnSync } from "child_process";
|
|
25
|
+
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
26
|
+
import { tmpdir } from "os";
|
|
27
|
+
import { join } from "path";
|
|
20
28
|
|
|
29
|
+
const ALLOWED_UPDATE_FIELDS = ['title', 'status', 'priority', 'tokenLimit', 'description'];
|
|
30
|
+
|
|
31
|
+
const VALID_FLAGS = new Set([
|
|
32
|
+
...ALLOWED_UPDATE_FIELDS.map(key => issueSchema[key].flag),
|
|
33
|
+
"--json",
|
|
34
|
+
]);
|
|
35
|
+
const FLAGS_HINT = [...VALID_FLAGS].join(", ");
|
|
36
|
+
const USAGE = "Usage: baton update <id> [options]";
|
|
37
|
+
|
|
38
|
+
// Build select choices from issueSchema enums so they stay in sync automatically.
|
|
39
|
+
const PRIORITY_HINTS = {
|
|
40
|
+
Low: "routine work, no urgency",
|
|
41
|
+
Medium: "should be resolved this week",
|
|
42
|
+
High: "blocking or time-sensitive",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PRIORITY_CHOICES = issueSchema.priority.values.map((v) => ({
|
|
46
|
+
name: `${v.padEnd(6)} -- ${PRIORITY_HINTS[v] ?? v}`,
|
|
47
|
+
value: v,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const STATUS_CHOICES = issueSchema.status.values.map((v) => ({
|
|
51
|
+
name: v,
|
|
52
|
+
value: v,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opens the user's $EDITOR pre-filled with existing content.
|
|
57
|
+
* Falls back to the inquirer built-in editor widget if $EDITOR is not set.
|
|
58
|
+
* Returns null if the user saves without making any changes.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} existing Current description to pre-fill.
|
|
61
|
+
* @returns {Promise<string|null>}
|
|
62
|
+
*/
|
|
63
|
+
async function openEditorForDescription(existing = "") {
|
|
64
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
65
|
+
|
|
66
|
+
if (!editorBin) {
|
|
67
|
+
const result = await editor({
|
|
68
|
+
message: "Description (save & quit when done):",
|
|
69
|
+
default: existing,
|
|
70
|
+
waitForUseInput: false,
|
|
71
|
+
});
|
|
72
|
+
const cleaned = result.trim();
|
|
73
|
+
return cleaned !== existing.trim() ? cleaned : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tmpPath = join(tmpdir(), `baton-issue-${Date.now()}.md`);
|
|
77
|
+
writeFileSync(tmpPath, existing, "utf8");
|
|
78
|
+
|
|
79
|
+
const result = spawnSync(editorBin, [tmpPath], { stdio: "inherit" });
|
|
80
|
+
if (result.error) {
|
|
81
|
+
unlinkSync(tmpPath);
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Could not open $EDITOR (${editorBin}): ${result.error.message}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const saved = readFileSync(tmpPath, "utf8");
|
|
88
|
+
unlinkSync(tmpPath);
|
|
89
|
+
|
|
90
|
+
const cleaned = saved.trim();
|
|
91
|
+
return cleaned !== existing.trim() ? cleaned : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Prompts the user to edit each field of an existing issue.
|
|
96
|
+
* Every prompt is pre-filled with the issue's current value so the user
|
|
97
|
+
* only needs to change what they care about.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} issue The current issue object from the database.
|
|
100
|
+
* @returns {Promise<object>} Partial options object containing only changed fields.
|
|
101
|
+
*/
|
|
102
|
+
async function runInteractiveMode(issue) {
|
|
103
|
+
console.log(`\n Baton -- editing issue #${issue.id}: "${issue.title}"\n`);
|
|
104
|
+
|
|
105
|
+
// Collect all prompt results into one object keyed by schema field name.
|
|
106
|
+
// The diff at the end loops over this -- no field names hardcoded there.
|
|
107
|
+
const results = {};
|
|
108
|
+
|
|
109
|
+
// Title
|
|
110
|
+
results.title = await input({
|
|
111
|
+
message: "Title:",
|
|
112
|
+
default: issue.title,
|
|
113
|
+
validate: (val) => val.trim().length > 0 || "Title cannot be empty.",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Status
|
|
117
|
+
results.status = await select({
|
|
118
|
+
message: "Status:",
|
|
119
|
+
choices: STATUS_CHOICES,
|
|
120
|
+
default: issue.status,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Priority
|
|
124
|
+
results.priority = await select({
|
|
125
|
+
message: "Priority:",
|
|
126
|
+
choices: PRIORITY_CHOICES,
|
|
127
|
+
default: issue.priority,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Token limit -- confirm-gated since it is optional
|
|
131
|
+
const currentLimit = issue.tokenLimit ?? null;
|
|
132
|
+
const wantsTokenLimit = await confirm({
|
|
133
|
+
message: `Set a token limit?${currentLimit ? ` (currently ${currentLimit})` : ""}`,
|
|
134
|
+
default: currentLimit !== null,
|
|
135
|
+
});
|
|
136
|
+
if (wantsTokenLimit) {
|
|
137
|
+
const raw = await input({
|
|
138
|
+
message: "Token limit (positive integer):",
|
|
139
|
+
default: currentLimit ? String(currentLimit) : undefined,
|
|
140
|
+
validate: (val) => {
|
|
141
|
+
const n = Number(val);
|
|
142
|
+
return (Number.isInteger(n) && n > 0) || "Must be a positive integer.";
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
results.tokenLimit = Number(raw);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Description - $EDITOR flow, keep original if the user makes no changes
|
|
149
|
+
const wantsDescription = await confirm({
|
|
150
|
+
message: "Edit description?",
|
|
151
|
+
default: true,
|
|
152
|
+
});
|
|
153
|
+
if (wantsDescription) {
|
|
154
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
155
|
+
const hint = editorBin ? `opens ${editorBin}` : "in-terminal editor";
|
|
156
|
+
console.log(
|
|
157
|
+
` -> ${hint} -- edit the description, save and quit when done.\n`,
|
|
158
|
+
);
|
|
159
|
+
const edited = await openEditorForDescription(issue.description ?? "");
|
|
160
|
+
// Only write to results if the user actually changed something
|
|
161
|
+
if (edited !== null) results.description = edited;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Diff: loop over results and collect only what changed.
|
|
165
|
+
// Adding a new prompt above is all that is needed -- nothing to update here.
|
|
166
|
+
const pending = Object.fromEntries(
|
|
167
|
+
Object.entries(results).filter(([key, val]) => val !== issue[key]),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (Object.keys(pending).length === 0) {
|
|
171
|
+
console.log("\nNo changes made.");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log("\n" + "-".repeat(48));
|
|
176
|
+
for (const [key, val] of Object.entries(pending)) {
|
|
177
|
+
const old = issue[key] ?? "(none)";
|
|
178
|
+
const preview = String(val).split("\n").slice(0, 2).join(" ").slice(0, 60);
|
|
179
|
+
console.log(
|
|
180
|
+
` ${key}: "${old}" -> "${preview}${String(val).length > 60 ? "..." : ""}"`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
console.log("-".repeat(48) + "\n");
|
|
184
|
+
|
|
185
|
+
const confirmed = await confirm({ message: "Save changes?", default: true });
|
|
186
|
+
if (!confirmed) {
|
|
187
|
+
console.log("Aborted -- no changes saved.");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return pending;
|
|
192
|
+
}
|
|
21
193
|
|
|
22
194
|
/**
|
|
23
|
-
* Updates the specified fields for a given issue ID
|
|
195
|
+
* Updates the specified fields for a given issue ID.
|
|
196
|
+
* Drops into interactive mode when only an ID is provided and no field flags follow.
|
|
197
|
+
*
|
|
24
198
|
* @param {string[]} args - The command line arguments
|
|
25
199
|
* @returns {Promise<number>} The exit code: 0 is success, 1 is error.
|
|
26
200
|
*/
|
|
27
201
|
export async function run(args) {
|
|
28
|
-
|
|
29
|
-
|
|
202
|
+
const isJson = hasFlag(args, "--json");
|
|
203
|
+
const cmdArgs = args.filter((arg) => arg !== "--json");
|
|
30
204
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
205
|
+
if (cmdArgs.length === 0 || cmdArgs === "") {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Invalid input: No arguments entered.\n${USAGE}\nOptions: ${FLAGS_HINT}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
34
210
|
|
|
35
|
-
|
|
36
|
-
|
|
211
|
+
// Convert id argument from string to base-10 integer
|
|
212
|
+
const id = parseInt(cmdArgs[0], 10);
|
|
37
213
|
|
|
38
|
-
|
|
39
|
-
|
|
214
|
+
if (isNaN(id)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Invalid input: No ID entered.\n${USAGE}\nOptions: ${FLAGS_HINT}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if user misspelled a flag
|
|
221
|
+
for (const arg of cmdArgs) {
|
|
222
|
+
if (arg.startsWith("--") && !VALID_FLAGS.has(arg)) {
|
|
223
|
+
throw new Error(`Unknown flag: ${arg}\nValid flags: ${FLAGS_HINT}`);
|
|
40
224
|
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const oldIssue = await getIssue(id);
|
|
41
229
|
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
230
|
+
// Interactive mode: only the ID was passed, no field flags.
|
|
231
|
+
// Flag mode: at least one flag follows the ID.
|
|
232
|
+
const providedFlags = cmdArgs.slice(1).filter((a) => a.startsWith("--"));
|
|
233
|
+
const isInteractive = providedFlags.length === 0;
|
|
234
|
+
|
|
235
|
+
const options = isInteractive
|
|
236
|
+
? await runInteractiveMode(oldIssue)
|
|
237
|
+
: parseArgs(cmdArgs.slice(1));
|
|
238
|
+
|
|
239
|
+
const newIssue = await updateIssue(id, oldIssue, options);
|
|
240
|
+
const envelope = { status: "success", issue: serializeIssue(newIssue) };
|
|
241
|
+
|
|
242
|
+
renderOutput(isJson, envelope, () => {
|
|
243
|
+
console.log("");
|
|
244
|
+
console.log(`Successfully updated issue #${id}:`);
|
|
245
|
+
for (const key in options) {
|
|
246
|
+
if (oldIssue[key] !== newIssue[key]) {
|
|
247
|
+
console.log(` ${key}: "${oldIssue[key]}" -> "${newIssue[key]}"`);
|
|
248
|
+
} else {
|
|
249
|
+
console.log(` ${key}: No change (already set to "${newIssue[key]}")`);
|
|
49
250
|
}
|
|
50
|
-
|
|
251
|
+
}
|
|
252
|
+
console.log("");
|
|
253
|
+
});
|
|
51
254
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const newIssue = await updateIssue(id, oldIssue, options);
|
|
59
|
-
const envelope = { status: 'success', issue: serializeIssue(newIssue) };
|
|
60
|
-
|
|
61
|
-
renderOutput(isJson, envelope, () => {
|
|
62
|
-
console.log('');
|
|
63
|
-
// Compare and print the changes:
|
|
64
|
-
console.log(`Successfully updated issue #${id}:`);
|
|
65
|
-
for (const key in options) {
|
|
66
|
-
if (oldIssue[key] !== newIssue[key]) {
|
|
67
|
-
console.log(` ${key}: "${oldIssue[key]}" -> "${newIssue[key]}"`);
|
|
68
|
-
} else {
|
|
69
|
-
// If the entered argument matches the old data
|
|
70
|
-
console.log(` ${key}: No change (already set to "${newIssue[key]}")`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
console.log('');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return 0;
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error(`Failed to update issue: ${error.message}`);
|
|
79
|
-
return 1;
|
|
255
|
+
return 0;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error.name === "ExitPromptError") {
|
|
258
|
+
console.log("\nAborted.");
|
|
259
|
+
return 0;
|
|
80
260
|
}
|
|
261
|
+
console.error(`Failed to update issue: ${error.message}`);
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
81
264
|
}
|
|
@@ -12,11 +12,13 @@ export class ActivityLog {
|
|
|
12
12
|
constructor({
|
|
13
13
|
logId = null,
|
|
14
14
|
issueId,
|
|
15
|
+
actorId = null,
|
|
15
16
|
action,
|
|
16
17
|
createdAt = new Date().toISOString(),
|
|
17
18
|
} = {}) {
|
|
18
19
|
this.logId = logId;
|
|
19
20
|
this.issueId = issueId;
|
|
21
|
+
this.actorId = actorId;
|
|
20
22
|
this.action = action;
|
|
21
23
|
this.createdAt = createdAt;
|
|
22
24
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const AgentType = Object.freeze({
|
|
2
|
+
AGENT: "agent",
|
|
3
|
+
HUMAN: "human"
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export class Agent {
|
|
7
|
+
constructor({
|
|
8
|
+
// User fields
|
|
9
|
+
name = "Unnamed",
|
|
10
|
+
type = AgentType.AGENT,
|
|
11
|
+
// Auto-generated fields
|
|
12
|
+
id = 0,
|
|
13
|
+
createdAt = new Date().toISOString(),
|
|
14
|
+
} = {}) {
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.type = type;
|
|
17
|
+
this.id = id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates the agent fields based on project business rules.
|
|
22
|
+
* @returns {{isValid: boolean, errors: string[]}} boolean and an array of errors
|
|
23
|
+
*/
|
|
24
|
+
validate() {
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
if (!this.name || typeof this.name !== "string" || this.name.trim() === "") {
|
|
28
|
+
errors.push("Name cannot be empty");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Object.values(AgentType).includes(this.type)) {
|
|
32
|
+
errors.push(`Invalid type "${this.type}". Must be one of: ${Object.values(AgentType).join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { isValid: errors.length === 0, errors: errors };
|
|
36
|
+
}
|
|
37
|
+
}
|
package/source/models/issue.js
CHANGED
|
@@ -20,7 +20,7 @@ export class Issue {
|
|
|
20
20
|
tokenLimit = null,
|
|
21
21
|
description = null,
|
|
22
22
|
lastUpdated = new Date().toISOString(),
|
|
23
|
-
|
|
23
|
+
assigneeId = null,
|
|
24
24
|
// Auto-generated fields
|
|
25
25
|
id = 0,
|
|
26
26
|
createdAt = new Date().toISOString(),
|
|
@@ -32,7 +32,7 @@ export class Issue {
|
|
|
32
32
|
this.tokenLimit = tokenLimit;
|
|
33
33
|
this.description = description;
|
|
34
34
|
this.lastUpdated = lastUpdated;
|
|
35
|
-
this.
|
|
35
|
+
this.assigneeId = assigneeId;
|
|
36
36
|
this.id = id;
|
|
37
37
|
this.createdAt = createdAt;
|
|
38
38
|
this.attemptNum = attemptNum;
|
|
@@ -68,5 +68,3 @@ export class Issue {
|
|
|
68
68
|
return {isValid: errors.length == 0, errors: errors}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|