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,10 @@
|
|
|
1
|
+
CREATE TABLE `agents` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`name` text NOT NULL,
|
|
4
|
+
`type` text NOT NULL
|
|
5
|
+
);
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
CREATE UNIQUE INDEX `agents_name_unique` ON `agents` (`name`);--> statement-breakpoint
|
|
8
|
+
ALTER TABLE `activity` ADD `actor_id` integer REFERENCES agents(id);--> statement-breakpoint
|
|
9
|
+
ALTER TABLE `issues` ADD `assignee_id` integer REFERENCES agents(id);--> statement-breakpoint
|
|
10
|
+
ALTER TABLE `issues` DROP COLUMN `assignees`;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "d89cefa6-9c90-4882-8fab-f6322fd1f45e",
|
|
5
|
+
"prevId": "af5467aa-0757-4c91-8afb-c6f0202dd90c",
|
|
6
|
+
"tables": {
|
|
7
|
+
"activity": {
|
|
8
|
+
"name": "activity",
|
|
9
|
+
"columns": {
|
|
10
|
+
"log_id": {
|
|
11
|
+
"name": "log_id",
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"primaryKey": true,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": true
|
|
16
|
+
},
|
|
17
|
+
"issue_id": {
|
|
18
|
+
"name": "issue_id",
|
|
19
|
+
"type": "integer",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": false,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"actor_id": {
|
|
25
|
+
"name": "actor_id",
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": false,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"created_at": {
|
|
32
|
+
"name": "created_at",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"autoincrement": false,
|
|
37
|
+
"default": "CURRENT_TIMESTAMP"
|
|
38
|
+
},
|
|
39
|
+
"action": {
|
|
40
|
+
"name": "action",
|
|
41
|
+
"type": "text",
|
|
42
|
+
"primaryKey": false,
|
|
43
|
+
"notNull": true,
|
|
44
|
+
"autoincrement": false
|
|
45
|
+
},
|
|
46
|
+
"details": {
|
|
47
|
+
"name": "details",
|
|
48
|
+
"type": "text",
|
|
49
|
+
"primaryKey": false,
|
|
50
|
+
"notNull": false,
|
|
51
|
+
"autoincrement": false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"indexes": {},
|
|
55
|
+
"foreignKeys": {
|
|
56
|
+
"activity_actor_id_agents_id_fk": {
|
|
57
|
+
"name": "activity_actor_id_agents_id_fk",
|
|
58
|
+
"tableFrom": "activity",
|
|
59
|
+
"tableTo": "agents",
|
|
60
|
+
"columnsFrom": [
|
|
61
|
+
"actor_id"
|
|
62
|
+
],
|
|
63
|
+
"columnsTo": [
|
|
64
|
+
"id"
|
|
65
|
+
],
|
|
66
|
+
"onDelete": "no action",
|
|
67
|
+
"onUpdate": "no action"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"compositePrimaryKeys": {},
|
|
71
|
+
"uniqueConstraints": {},
|
|
72
|
+
"checkConstraints": {
|
|
73
|
+
"activity_action_check": {
|
|
74
|
+
"name": "activity_action_check",
|
|
75
|
+
"value": "\"activity\".\"action\" IN ('state_change', 'priority_change', 'edit', 'read', 'creation', 'deletion', 'rejection')"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"agents": {
|
|
80
|
+
"name": "agents",
|
|
81
|
+
"columns": {
|
|
82
|
+
"id": {
|
|
83
|
+
"name": "id",
|
|
84
|
+
"type": "integer",
|
|
85
|
+
"primaryKey": true,
|
|
86
|
+
"notNull": true,
|
|
87
|
+
"autoincrement": true
|
|
88
|
+
},
|
|
89
|
+
"name": {
|
|
90
|
+
"name": "name",
|
|
91
|
+
"type": "text",
|
|
92
|
+
"primaryKey": false,
|
|
93
|
+
"notNull": true,
|
|
94
|
+
"autoincrement": false
|
|
95
|
+
},
|
|
96
|
+
"type": {
|
|
97
|
+
"name": "type",
|
|
98
|
+
"type": "text",
|
|
99
|
+
"primaryKey": false,
|
|
100
|
+
"notNull": true,
|
|
101
|
+
"autoincrement": false
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"indexes": {
|
|
105
|
+
"agents_name_unique": {
|
|
106
|
+
"name": "agents_name_unique",
|
|
107
|
+
"columns": [
|
|
108
|
+
"name"
|
|
109
|
+
],
|
|
110
|
+
"isUnique": true
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"foreignKeys": {},
|
|
114
|
+
"compositePrimaryKeys": {},
|
|
115
|
+
"uniqueConstraints": {},
|
|
116
|
+
"checkConstraints": {}
|
|
117
|
+
},
|
|
118
|
+
"issues": {
|
|
119
|
+
"name": "issues",
|
|
120
|
+
"columns": {
|
|
121
|
+
"id": {
|
|
122
|
+
"name": "id",
|
|
123
|
+
"type": "integer",
|
|
124
|
+
"primaryKey": true,
|
|
125
|
+
"notNull": true,
|
|
126
|
+
"autoincrement": true
|
|
127
|
+
},
|
|
128
|
+
"created_at": {
|
|
129
|
+
"name": "created_at",
|
|
130
|
+
"type": "text",
|
|
131
|
+
"primaryKey": false,
|
|
132
|
+
"notNull": true,
|
|
133
|
+
"autoincrement": false,
|
|
134
|
+
"default": "CURRENT_TIMESTAMP"
|
|
135
|
+
},
|
|
136
|
+
"last_updated": {
|
|
137
|
+
"name": "last_updated",
|
|
138
|
+
"type": "text",
|
|
139
|
+
"primaryKey": false,
|
|
140
|
+
"notNull": true,
|
|
141
|
+
"autoincrement": false,
|
|
142
|
+
"default": "CURRENT_TIMESTAMP"
|
|
143
|
+
},
|
|
144
|
+
"attempt_num": {
|
|
145
|
+
"name": "attempt_num",
|
|
146
|
+
"type": "integer",
|
|
147
|
+
"primaryKey": false,
|
|
148
|
+
"notNull": true,
|
|
149
|
+
"autoincrement": false,
|
|
150
|
+
"default": 0
|
|
151
|
+
},
|
|
152
|
+
"title": {
|
|
153
|
+
"name": "title",
|
|
154
|
+
"type": "text",
|
|
155
|
+
"primaryKey": false,
|
|
156
|
+
"notNull": true,
|
|
157
|
+
"autoincrement": false,
|
|
158
|
+
"default": "'PENDING'"
|
|
159
|
+
},
|
|
160
|
+
"status": {
|
|
161
|
+
"name": "status",
|
|
162
|
+
"type": "text",
|
|
163
|
+
"primaryKey": false,
|
|
164
|
+
"notNull": true,
|
|
165
|
+
"autoincrement": false,
|
|
166
|
+
"default": "'Open'"
|
|
167
|
+
},
|
|
168
|
+
"priority": {
|
|
169
|
+
"name": "priority",
|
|
170
|
+
"type": "text",
|
|
171
|
+
"primaryKey": false,
|
|
172
|
+
"notNull": false,
|
|
173
|
+
"autoincrement": false,
|
|
174
|
+
"default": "'Low'"
|
|
175
|
+
},
|
|
176
|
+
"token_limit": {
|
|
177
|
+
"name": "token_limit",
|
|
178
|
+
"type": "integer",
|
|
179
|
+
"primaryKey": false,
|
|
180
|
+
"notNull": false,
|
|
181
|
+
"autoincrement": false
|
|
182
|
+
},
|
|
183
|
+
"description": {
|
|
184
|
+
"name": "description",
|
|
185
|
+
"type": "text",
|
|
186
|
+
"primaryKey": false,
|
|
187
|
+
"notNull": false,
|
|
188
|
+
"autoincrement": false
|
|
189
|
+
},
|
|
190
|
+
"assignee_id": {
|
|
191
|
+
"name": "assignee_id",
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"primaryKey": false,
|
|
194
|
+
"notNull": false,
|
|
195
|
+
"autoincrement": false
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"indexes": {},
|
|
199
|
+
"foreignKeys": {
|
|
200
|
+
"issues_assignee_id_agents_id_fk": {
|
|
201
|
+
"name": "issues_assignee_id_agents_id_fk",
|
|
202
|
+
"tableFrom": "issues",
|
|
203
|
+
"tableTo": "agents",
|
|
204
|
+
"columnsFrom": [
|
|
205
|
+
"assignee_id"
|
|
206
|
+
],
|
|
207
|
+
"columnsTo": [
|
|
208
|
+
"id"
|
|
209
|
+
],
|
|
210
|
+
"onDelete": "no action",
|
|
211
|
+
"onUpdate": "no action"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"compositePrimaryKeys": {},
|
|
215
|
+
"uniqueConstraints": {},
|
|
216
|
+
"checkConstraints": {}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"views": {},
|
|
220
|
+
"enums": {},
|
|
221
|
+
"_meta": {
|
|
222
|
+
"schemas": {},
|
|
223
|
+
"tables": {},
|
|
224
|
+
"columns": {}
|
|
225
|
+
},
|
|
226
|
+
"internal": {
|
|
227
|
+
"indexes": {}
|
|
228
|
+
}
|
|
229
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baton-issue-tracker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A CLI issue tracker for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"node": ">=22.0.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@inquirer/prompts": "^8.5.1",
|
|
40
41
|
"better-sqlite3": "^12.10.0",
|
|
41
42
|
"drizzle-orm": "^0.45.2"
|
|
42
43
|
}
|
package/source/cli.js
CHANGED
|
@@ -23,7 +23,12 @@ import { run as runView} from './commands/view.js';
|
|
|
23
23
|
import { run as runSearch } from './commands/search.js';
|
|
24
24
|
import { run as runList } from './commands/list.js';
|
|
25
25
|
import { run as runCreate } from './commands/create.js'
|
|
26
|
-
import { run as runUpdate } from './commands/update.js'
|
|
26
|
+
import { run as runUpdate } from './commands/update.js';
|
|
27
|
+
import { run as runPriority } from './commands/priority.js';
|
|
28
|
+
import { run as runLog } from './commands/log.js';
|
|
29
|
+
import { run as runRegister } from './commands/register.js';
|
|
30
|
+
|
|
31
|
+
import { authenticateContext } from './services/authService.js';
|
|
27
32
|
|
|
28
33
|
const HELP = `baton — AI agent issue tracker CLI
|
|
29
34
|
|
|
@@ -32,6 +37,7 @@ Usage:
|
|
|
32
37
|
|
|
33
38
|
Commands:
|
|
34
39
|
init Initialize storage and seed issues from product specs
|
|
40
|
+
register Register a new AI agent or human user
|
|
35
41
|
next Work on the highest-priority open issue
|
|
36
42
|
loop Run the agent autonomously for multiple steps
|
|
37
43
|
status Show issue counts and overall progress
|
|
@@ -40,7 +46,9 @@ Commands:
|
|
|
40
46
|
list Lists issues filtered by status and priority
|
|
41
47
|
create Creates an issue with specified fields
|
|
42
48
|
approve Move an issue from in-review to closed
|
|
43
|
-
|
|
49
|
+
priority Set an issue's priority level
|
|
50
|
+
update Updates an issue's specified fields
|
|
51
|
+
log Show activity history for an issue
|
|
44
52
|
|
|
45
53
|
Options:
|
|
46
54
|
init --force Re-initialize an existing tracker database
|
|
@@ -48,6 +56,8 @@ Options:
|
|
|
48
56
|
init --json Output as JSON (for AI agents)
|
|
49
57
|
init <path> Same as --specs <path> (positional)
|
|
50
58
|
Default specs: docs/specs/project-requirements.md
|
|
59
|
+
register --name <name> Name of the agent or user
|
|
60
|
+
register --type <type> agent | human (default: agent)
|
|
51
61
|
loop --steps <n> Number of autonomous steps (alias: -n)
|
|
52
62
|
loop -n <n>
|
|
53
63
|
loop --json Output as JSON (for AI agents)
|
|
@@ -66,19 +76,21 @@ Options:
|
|
|
66
76
|
create --token-limit <n> Optional token budget for this issue
|
|
67
77
|
create --json Output as JSON (for AI agents)
|
|
68
78
|
approve <id> [--json]
|
|
79
|
+
priority <id> <level> [--json] low | medium | high
|
|
69
80
|
update --title <text> New title
|
|
70
81
|
update --description <text> New description
|
|
71
82
|
update --token-limit <n> New token budget
|
|
72
83
|
update --status <s> open | in-progress | closed
|
|
73
84
|
update --priority <level> low | medium | high
|
|
74
85
|
update --json Output as JSON (for AI agents)
|
|
75
|
-
|
|
86
|
+
log <id> [--json]
|
|
76
87
|
|
|
77
88
|
Examples:
|
|
78
89
|
baton init
|
|
79
90
|
baton init --specs ./my-specs.md
|
|
80
91
|
baton init ./my-specs.md
|
|
81
92
|
baton init --force
|
|
93
|
+
baton register --name claude-dev --type agent
|
|
82
94
|
baton next
|
|
83
95
|
baton loop --steps 5
|
|
84
96
|
baton status
|
|
@@ -90,8 +102,11 @@ Examples:
|
|
|
90
102
|
baton create --title "Fix login bug" --priority high
|
|
91
103
|
baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
|
|
92
104
|
baton approve 5
|
|
105
|
+
baton priority 5 high
|
|
106
|
+
baton priority 3 low
|
|
93
107
|
baton update 3 --title "Revised title"
|
|
94
108
|
baton update 7 --status closed --priority medium
|
|
109
|
+
baton log 5
|
|
95
110
|
`;
|
|
96
111
|
|
|
97
112
|
/**
|
|
@@ -107,8 +122,12 @@ async function main() {
|
|
|
107
122
|
return;
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
// Authenticate the user and context before executing any command.
|
|
126
|
+
authenticateContext(command);
|
|
127
|
+
|
|
110
128
|
const handlers = {
|
|
111
129
|
init: () => runInit(args),
|
|
130
|
+
register: () => runRegister(args),
|
|
112
131
|
next: () => runNext(args),
|
|
113
132
|
loop: () => runLoop(args),
|
|
114
133
|
status: () => runStatus(args),
|
|
@@ -116,12 +135,19 @@ async function main() {
|
|
|
116
135
|
search: () => runSearch(args),
|
|
117
136
|
list: () => runList(args),
|
|
118
137
|
approve: () => runApprove(args),
|
|
138
|
+
priority: () => runPriority(args),
|
|
119
139
|
create: () => runCreate(args),
|
|
120
|
-
update: () => runUpdate(args)
|
|
140
|
+
update: () => runUpdate(args),
|
|
141
|
+
log: () => runLog(args),
|
|
121
142
|
};
|
|
122
|
-
|
|
143
|
+
|
|
123
144
|
const handler = handlers[command];
|
|
124
145
|
if (!handler) {
|
|
146
|
+
if (wantsHelp(args)) {
|
|
147
|
+
console.log(HELP);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
125
151
|
console.error(`Error: Unknown command "${command}".`);
|
|
126
152
|
console.error('Run `baton --help` for usage.');
|
|
127
153
|
process.exit(1);
|
|
@@ -1,52 +1,237 @@
|
|
|
1
1
|
// create.js
|
|
2
2
|
// AI was consulted for some portions of this file.
|
|
3
|
-
// create command allows the user to create an issue
|
|
3
|
+
// create command allows the user to create an issue
|
|
4
4
|
// Usage: baton create --title <text> --description <text> --priority <level> --token-limit <n>
|
|
5
|
-
//
|
|
5
|
+
// baton create (launches interactive prompt mode)
|
|
6
|
+
//
|
|
6
7
|
// Examples:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
8
|
+
// baton create --title "Fix login bug" --priority high
|
|
9
|
+
// baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
|
|
10
|
+
// baton create # interactive mode
|
|
11
|
+
//
|
|
9
12
|
// Options:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// --title <text> Issue title (defaults to "Issue #<id>" if omitted)
|
|
14
|
+
// --description <text> Issue description
|
|
15
|
+
// --priority <level> low | medium | high (default: low)
|
|
16
|
+
// --token-limit <n> Optional token budget for this issue
|
|
17
|
+
// -h, --help Show this help
|
|
15
18
|
|
|
16
19
|
import { createIssue } from "../services/issuesService.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
20
|
+
import { hasFlag, parseArgs, renderOutput, serializeIssue } from "../util.js";
|
|
21
|
+
import { issueSchema } from "../models/schema.js";
|
|
22
|
+
import { input, select, confirm, editor } from "@inquirer/prompts";
|
|
23
|
+
import { spawnSync } from "child_process";
|
|
24
|
+
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
25
|
+
import { tmpdir } from "os";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
|
|
28
|
+
const ALLOWED_CREATE_FIELDS = ['title', 'priority', 'tokenLimit', 'description'];
|
|
29
|
+
|
|
30
|
+
const VALID_FLAGS = new Set([
|
|
31
|
+
...ALLOWED_CREATE_FIELDS.map(key => issueSchema[key].flag),
|
|
32
|
+
"--json",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Build select choices from issueSchema.priority.values so the list stays in
|
|
36
|
+
// sync with the Priority enum without any duplication.
|
|
37
|
+
const PRIORITY_HINTS = {
|
|
38
|
+
Low: "routine work, no urgency",
|
|
39
|
+
Medium: "should be resolved this week",
|
|
40
|
+
High: "blocking or time-sensitive",
|
|
41
|
+
};
|
|
42
|
+
const PRIORITY_CHOICES = issueSchema.priority.values.map((v) => ({
|
|
43
|
+
name: `${v.padEnd(6)} -- ${PRIORITY_HINTS[v] ?? v}`,
|
|
44
|
+
value: v,
|
|
45
|
+
}));
|
|
46
|
+
const DEFAULT_PRIORITY = issueSchema.priority.values[0];
|
|
47
|
+
|
|
48
|
+
const DESCRIPTION_PLACEHOLDER = [
|
|
49
|
+
"## What is the issue?",
|
|
50
|
+
"",
|
|
51
|
+
"## Steps to reproduce (if applicable)",
|
|
52
|
+
"",
|
|
53
|
+
"## Expected vs actual behaviour",
|
|
54
|
+
"",
|
|
55
|
+
"## Additional context",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Opens the user's $EDITOR with an optional seed template and returns the
|
|
61
|
+
* saved contents, or null if the user left it unchanged / empty.
|
|
62
|
+
*
|
|
63
|
+
* Falls back gracefully: if no $EDITOR is set the inquirer `editor` prompt is
|
|
64
|
+
* used instead (which has its own built-in text area).
|
|
65
|
+
*
|
|
66
|
+
* @param {string} template Initial file content shown to the user.
|
|
67
|
+
* @returns {Promise<string|null>}
|
|
68
|
+
*/
|
|
69
|
+
async function openEditorForDescription(template = DESCRIPTION_PLACEHOLDER) {
|
|
70
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
71
|
+
|
|
72
|
+
if (!editorBin) {
|
|
73
|
+
// If no $EDITOR set, then fall back to @inquirer/prompts built-in editor widget
|
|
74
|
+
const result = await editor({
|
|
75
|
+
message:
|
|
76
|
+
"Description (opens in-terminal editor -- save & quit when done):",
|
|
77
|
+
default: template,
|
|
78
|
+
waitForUseInput: false,
|
|
79
|
+
});
|
|
80
|
+
const cleaned = result.trim();
|
|
81
|
+
return cleaned && cleaned !== template.trim() ? cleaned : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Write template to a temp file, open $EDITOR, read back the result
|
|
85
|
+
const tmpPath = join(tmpdir(), `baton-issue-${Date.now()}.md`);
|
|
86
|
+
writeFileSync(tmpPath, template, "utf8");
|
|
87
|
+
|
|
88
|
+
const result = spawnSync(editorBin, [tmpPath], { stdio: "inherit" });
|
|
89
|
+
|
|
90
|
+
if (result.error) {
|
|
91
|
+
unlinkSync(tmpPath);
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Could not open $EDITOR (${editorBin}): ${result.error.message}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const saved = readFileSync(tmpPath, "utf8");
|
|
98
|
+
unlinkSync(tmpPath);
|
|
99
|
+
|
|
100
|
+
const cleaned = saved.trim();
|
|
101
|
+
// Return null if the user saved without changing anything
|
|
102
|
+
return cleaned && cleaned !== template.trim() ? cleaned : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Guides the user through issue creation with interactive prompts.
|
|
107
|
+
* @returns {Promise<object>} Options object ready to pass to createIssue()
|
|
108
|
+
*/
|
|
109
|
+
async function runInteractiveMode() {
|
|
110
|
+
console.log("\n Baton -- interactive issue creation\n");
|
|
111
|
+
|
|
112
|
+
// Title
|
|
113
|
+
const title = await input({
|
|
114
|
+
message: "Title:",
|
|
115
|
+
required: true,
|
|
116
|
+
validate: (val) => val.trim().length > 0 || "Title cannot be empty.",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Priority
|
|
120
|
+
const priority = await select({
|
|
121
|
+
message: "Priority:",
|
|
122
|
+
choices: PRIORITY_CHOICES,
|
|
123
|
+
default: DEFAULT_PRIORITY,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Token limit
|
|
127
|
+
const wantsTokenLimit = await confirm({
|
|
128
|
+
message: "Set a token limit for this issue?",
|
|
129
|
+
default: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let tokenLimit = null;
|
|
133
|
+
if (wantsTokenLimit) {
|
|
134
|
+
const raw = await input({
|
|
135
|
+
message: "Token limit (positive integer):",
|
|
136
|
+
validate: (val) => {
|
|
137
|
+
const n = Number(val);
|
|
138
|
+
return (Number.isInteger(n) && n > 0) || "Must be a positive integer.";
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
tokenLimit = Number(raw);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Description
|
|
145
|
+
const wantsDescription = await confirm({
|
|
146
|
+
message: "Add a description?",
|
|
147
|
+
default: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
let description = null;
|
|
151
|
+
if (wantsDescription) {
|
|
152
|
+
const editorBin = process.env.EDITOR || process.env.VISUAL;
|
|
153
|
+
const hint = editorBin ? `opens ${editorBin}` : "in-terminal editor";
|
|
154
|
+
console.log(
|
|
155
|
+
` -> ${hint} -- fill in what's relevant, save and quit when done.\n`,
|
|
156
|
+
);
|
|
157
|
+
description = await openEditorForDescription();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Preview & confirm
|
|
161
|
+
console.log("\n" + "-".repeat(48));
|
|
162
|
+
console.log(` Title : ${title}`);
|
|
163
|
+
console.log(` Priority : ${priority}`);
|
|
164
|
+
console.log(` Token limit: ${tokenLimit ?? "(none)"}`);
|
|
165
|
+
if (description) {
|
|
166
|
+
const preview = description.split("\n").slice(0, 3).join(" ").slice(0, 72);
|
|
167
|
+
console.log(
|
|
168
|
+
` Description: ${preview}${description.length > 72 ? "..." : ""}`,
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` Description: (none)`);
|
|
172
|
+
}
|
|
173
|
+
console.log("-".repeat(48) + "\n");
|
|
174
|
+
|
|
175
|
+
const confirmed = await confirm({
|
|
176
|
+
message: "Create this issue?",
|
|
177
|
+
default: true,
|
|
178
|
+
});
|
|
179
|
+
if (!confirmed) {
|
|
180
|
+
console.log("Aborted -- no issue created.");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { title, priority, tokenLimit, description };
|
|
185
|
+
}
|
|
19
186
|
|
|
20
187
|
/**
|
|
21
188
|
* Initializes a new issue in the database with the specified fields
|
|
189
|
+
* Drops into interactive mode when no flags are provided
|
|
190
|
+
*
|
|
22
191
|
* @param {string[]} args - The command line arguments
|
|
23
|
-
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
192
|
+
* @returns {Promise<number>} The exit code: 0 is success, 1 is error
|
|
24
193
|
*/
|
|
25
194
|
export async function run(args) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
195
|
+
const isJson = hasFlag(args, "--json");
|
|
196
|
+
|
|
197
|
+
// Flag validation -- VALID_FLAGS is derived from issueSchema, so this stays
|
|
198
|
+
// current automatically when fields are added or renamed there
|
|
199
|
+
const providedFlags = args.filter((a) => a.startsWith("--"));
|
|
200
|
+
for (const flag of providedFlags) {
|
|
201
|
+
if (!VALID_FLAGS.has(flag)) {
|
|
202
|
+
const knownFlags = [...VALID_FLAGS].join(", ");
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Unknown flag: ${flag}\nValid flags: ${knownFlags}\n` +
|
|
205
|
+
`Tip: run \`baton create\` with no flags to use interactive mode.`,
|
|
206
|
+
);
|
|
35
207
|
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Interactive mode: no flags provided (human at a keyboard)
|
|
212
|
+
// Flag mode: at least one flag present (scripts, CI, power users)
|
|
213
|
+
const nonJsonFlags = providedFlags.filter((f) => f !== "--json");
|
|
214
|
+
const isInteractive = nonJsonFlags.length === 0;
|
|
36
215
|
|
|
37
|
-
|
|
38
|
-
|
|
216
|
+
const options = isInteractive
|
|
217
|
+
? await runInteractiveMode()
|
|
218
|
+
: parseArgs(args);
|
|
39
219
|
|
|
40
|
-
|
|
41
|
-
|
|
220
|
+
const issue = await createIssue(options);
|
|
221
|
+
const envelope = { status: "success", issue: serializeIssue(issue) };
|
|
42
222
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
223
|
+
renderOutput(isJson, envelope, () => {
|
|
224
|
+
console.log(`\nCreated issue #${issue.id}: "${issue.title}"`);
|
|
225
|
+
});
|
|
46
226
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
227
|
+
return 0;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// when user hits Ctrl+C
|
|
230
|
+
if (error.name === "ExitPromptError") {
|
|
231
|
+
console.log("\nAborted.");
|
|
232
|
+
return 0;
|
|
51
233
|
}
|
|
234
|
+
console.error(`Failed to create issue: ${error.message}`);
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
52
237
|
}
|