@eunjae/il 1.2.0 → 1.3.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.
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
2
|
import { Command } from "commander";
|
|
6
|
-
import writeFileAtomic from "write-file-atomic";
|
|
7
3
|
import { z } from "zod";
|
|
4
|
+
import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
8
5
|
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import writeFileAtomic from "write-file-atomic";
|
|
8
|
+
import lockfile from "proper-lockfile";
|
|
9
9
|
import { Octokit } from "octokit";
|
|
10
10
|
import { DateTime } from "luxon";
|
|
11
|
-
import lockfile from "proper-lockfile";
|
|
12
11
|
import { createHash } from "node:crypto";
|
|
13
12
|
import { ulid } from "ulid";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
|
+
//#region lib/constants.ts
|
|
16
|
+
const APP_NAME = "il";
|
|
17
|
+
const APP_DIR = `.${APP_NAME}`;
|
|
18
|
+
const LOCK_DIR = ".lock";
|
|
19
|
+
const LOCK_FILE = "store.lock";
|
|
20
|
+
const TASKS_DIR = "tasks";
|
|
21
|
+
const STATUS_ORDER = [
|
|
22
|
+
"backlog",
|
|
23
|
+
"active",
|
|
24
|
+
"paused",
|
|
25
|
+
"completed",
|
|
26
|
+
"cancelled"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
15
30
|
//#region lib/json.ts
|
|
16
31
|
const readJsonFile = async (filePath) => {
|
|
17
32
|
const raw = await readFile(filePath, "utf8");
|
|
@@ -25,12 +40,7 @@ const writeJsonAtomic = async (filePath, data) => {
|
|
|
25
40
|
};
|
|
26
41
|
|
|
27
42
|
//#endregion
|
|
28
|
-
//#region lib/
|
|
29
|
-
const emptyAliases = () => ({
|
|
30
|
-
T: {},
|
|
31
|
-
R: {}
|
|
32
|
-
});
|
|
33
|
-
const aliasFilePath = (workspaceRoot) => path.join(workspaceRoot, "aliases.json");
|
|
43
|
+
//#region lib/config.ts
|
|
34
44
|
const fileExists$2 = async (filePath) => {
|
|
35
45
|
try {
|
|
36
46
|
await access(filePath);
|
|
@@ -39,62 +49,194 @@ const fileExists$2 = async (filePath) => {
|
|
|
39
49
|
return false;
|
|
40
50
|
}
|
|
41
51
|
};
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const resolveGlobalConfigPath = () => {
|
|
53
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config");
|
|
54
|
+
return path.join(configHome, APP_NAME, "config.json");
|
|
55
|
+
};
|
|
56
|
+
const readConfigFile = async (filePath) => {
|
|
57
|
+
if (!await fileExists$2(filePath)) return {};
|
|
58
|
+
return await readJsonFile(filePath) ?? {};
|
|
59
|
+
};
|
|
60
|
+
const resolveGithubToken = async (workspaceRoot) => {
|
|
61
|
+
if (process.env.IL_GITHUB_TOKEN) return process.env.IL_GITHUB_TOKEN;
|
|
62
|
+
const workspaceConfig = await readConfigFile(path.join(workspaceRoot, "config.json"));
|
|
63
|
+
if (workspaceConfig.github?.token) return workspaceConfig.github.token;
|
|
64
|
+
return (await readConfigFile(resolveGlobalConfigPath())).github?.token ?? null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region lib/workspace.ts
|
|
69
|
+
const exists = async (filePath) => {
|
|
70
|
+
try {
|
|
71
|
+
await access(filePath);
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const findGitRoot = async (startDir) => {
|
|
78
|
+
let current = path.resolve(startDir);
|
|
79
|
+
while (true) {
|
|
80
|
+
if (await exists(path.join(current, ".git"))) return current;
|
|
81
|
+
const parent = path.dirname(current);
|
|
82
|
+
if (parent === current) return null;
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const resolveGlobalWorkspaceRoot = () => {
|
|
87
|
+
const dataHome = process.env.XDG_DATA_HOME ?? path.join(homedir(), ".local", "share");
|
|
88
|
+
return path.join(dataHome, APP_NAME);
|
|
89
|
+
};
|
|
90
|
+
const resolveWorkspace = async (options) => {
|
|
91
|
+
if (options.store) return {
|
|
92
|
+
root: path.resolve(options.store),
|
|
93
|
+
kind: "explicit"
|
|
94
|
+
};
|
|
95
|
+
const repoRoot = await findGitRoot(options.cwd ?? process.cwd());
|
|
96
|
+
if (options.repo) {
|
|
97
|
+
if (!repoRoot) throw new Error("Not inside a git repository");
|
|
98
|
+
return {
|
|
99
|
+
root: path.join(repoRoot, APP_DIR),
|
|
100
|
+
kind: "repo"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (options.global) return {
|
|
104
|
+
root: resolveGlobalWorkspaceRoot(),
|
|
105
|
+
kind: "global"
|
|
106
|
+
};
|
|
107
|
+
if (repoRoot) {
|
|
108
|
+
const repoWorkspace = path.join(repoRoot, APP_DIR);
|
|
109
|
+
if (await exists(repoWorkspace)) return {
|
|
110
|
+
root: repoWorkspace,
|
|
111
|
+
kind: "repo"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
46
114
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
115
|
+
root: resolveGlobalWorkspaceRoot(),
|
|
116
|
+
kind: "global"
|
|
49
117
|
};
|
|
50
118
|
};
|
|
51
|
-
const
|
|
52
|
-
await
|
|
119
|
+
const ensureWorkspaceLayout = async (workspaceRoot) => {
|
|
120
|
+
await mkdir(path.join(workspaceRoot, TASKS_DIR), { recursive: true });
|
|
121
|
+
await mkdir(path.join(workspaceRoot, LOCK_DIR), { recursive: true });
|
|
122
|
+
await Promise.all(STATUS_ORDER.map((status) => mkdir(path.join(workspaceRoot, TASKS_DIR, status), { recursive: true })));
|
|
53
123
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region lib/lock.ts
|
|
127
|
+
const withWorkspaceLock = async (workspaceRoot, fn) => {
|
|
128
|
+
await ensureWorkspaceLayout(workspaceRoot);
|
|
129
|
+
const lockPath = path.join(workspaceRoot, LOCK_DIR, LOCK_FILE);
|
|
130
|
+
await writeFile(lockPath, "", { flag: "a" });
|
|
131
|
+
const release = await lockfile.lock(lockPath, {
|
|
132
|
+
stale: 6e4,
|
|
133
|
+
retries: {
|
|
134
|
+
retries: 5,
|
|
135
|
+
factor: 1.5,
|
|
136
|
+
minTimeout: 50,
|
|
137
|
+
maxTimeout: 1e3
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
return await fn();
|
|
142
|
+
} finally {
|
|
143
|
+
await release();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region lib/time.ts
|
|
149
|
+
const DEFAULT_ZONE = "Europe/Paris";
|
|
150
|
+
const toIsoDate = (value, label) => {
|
|
151
|
+
const date = value.toISODate();
|
|
152
|
+
if (!date) throw new Error(`Failed to generate ${label} date`);
|
|
153
|
+
return date;
|
|
154
|
+
};
|
|
155
|
+
const buildDateRange = (start, end) => {
|
|
156
|
+
const dates = [];
|
|
157
|
+
let cursor = start.startOf("day");
|
|
158
|
+
const final = end.startOf("day");
|
|
159
|
+
while (cursor <= final) {
|
|
160
|
+
dates.push(toIsoDate(cursor, "range"));
|
|
161
|
+
cursor = cursor.plus({ days: 1 });
|
|
162
|
+
}
|
|
163
|
+
return dates;
|
|
164
|
+
};
|
|
165
|
+
const nowIso = () => {
|
|
166
|
+
const value = DateTime.now().setZone(DEFAULT_ZONE).toISO();
|
|
167
|
+
if (!value) throw new Error("Failed to generate timestamp");
|
|
168
|
+
return value;
|
|
169
|
+
};
|
|
170
|
+
const todayDate = (date) => {
|
|
171
|
+
const value = date ? DateTime.fromISO(date, { zone: DEFAULT_ZONE }).toISODate() : DateTime.now().setZone(DEFAULT_ZONE).toISODate();
|
|
172
|
+
if (!value) throw new Error("Failed to generate date");
|
|
173
|
+
return value;
|
|
174
|
+
};
|
|
175
|
+
const listDatesForScope = (scope) => {
|
|
176
|
+
const now = DateTime.now().setZone(DEFAULT_ZONE);
|
|
177
|
+
switch (scope) {
|
|
178
|
+
case "today": return [toIsoDate(now, "today")];
|
|
179
|
+
case "yesterday": return [toIsoDate(now.minus({ days: 1 }), "yesterday")];
|
|
180
|
+
case "this_week": return buildDateRange(now.startOf("week"), now.endOf("week"));
|
|
181
|
+
case "last_week": {
|
|
182
|
+
const lastWeek = now.minus({ weeks: 1 });
|
|
183
|
+
return buildDateRange(lastWeek.startOf("week"), lastWeek.endOf("week"));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region lib/pr.ts
|
|
190
|
+
const parseGitHubPrUrl = (value) => {
|
|
191
|
+
let url;
|
|
192
|
+
try {
|
|
193
|
+
url = new URL(value);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
198
|
+
if (parts.length < 4 || parts[2] !== "pull") return null;
|
|
199
|
+
const number = Number(parts[3]);
|
|
200
|
+
if (!Number.isInteger(number)) return null;
|
|
57
201
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
202
|
+
url: value,
|
|
203
|
+
provider: "github",
|
|
204
|
+
repo: {
|
|
205
|
+
host: url.host,
|
|
206
|
+
owner: parts[0],
|
|
207
|
+
name: parts[1]
|
|
208
|
+
},
|
|
209
|
+
number
|
|
60
210
|
};
|
|
61
211
|
};
|
|
62
|
-
const
|
|
63
|
-
const allocateAliasInMap = (aliases, prefix, taskId) => {
|
|
64
|
-
const used = new Set(Object.keys(aliases[prefix]).map((key) => Number.parseInt(key, 10)));
|
|
65
|
-
let next = 1;
|
|
66
|
-
while (used.has(next)) next += 1;
|
|
67
|
-
const key = formatAliasKey(next);
|
|
68
|
-
aliases[prefix][key] = taskId;
|
|
212
|
+
const buildPrAttachment = (parsed, fetched) => {
|
|
69
213
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
214
|
+
url: parsed.url,
|
|
215
|
+
provider: "github",
|
|
216
|
+
repo: parsed.repo,
|
|
217
|
+
number: parsed.number,
|
|
218
|
+
fetched
|
|
72
219
|
};
|
|
73
220
|
};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
221
|
+
const fetchGitHubPr = async (parsed, token) => {
|
|
222
|
+
if (!parsed.repo || !parsed.number) return null;
|
|
223
|
+
if (!token) return null;
|
|
224
|
+
const data = (await new Octokit({ auth: token }).rest.pulls.get({
|
|
225
|
+
owner: parsed.repo.owner,
|
|
226
|
+
repo: parsed.repo.name,
|
|
227
|
+
pull_number: parsed.number
|
|
228
|
+
})).data;
|
|
229
|
+
const state = data.merged ? "merged" : data.state === "open" ? "open" : "closed";
|
|
230
|
+
return {
|
|
231
|
+
at: nowIso(),
|
|
232
|
+
title: data.title,
|
|
233
|
+
author: { login: data.user?.login ?? "unknown" },
|
|
234
|
+
state,
|
|
235
|
+
draft: data.draft ?? false,
|
|
236
|
+
updated_at: data.updated_at
|
|
237
|
+
};
|
|
81
238
|
};
|
|
82
239
|
|
|
83
|
-
//#endregion
|
|
84
|
-
//#region lib/constants.ts
|
|
85
|
-
const APP_NAME = "il";
|
|
86
|
-
const APP_DIR = `.${APP_NAME}`;
|
|
87
|
-
const LOCK_DIR = ".lock";
|
|
88
|
-
const LOCK_FILE = "store.lock";
|
|
89
|
-
const TASKS_DIR = "tasks";
|
|
90
|
-
const STATUS_ORDER = [
|
|
91
|
-
"backlog",
|
|
92
|
-
"active",
|
|
93
|
-
"paused",
|
|
94
|
-
"completed",
|
|
95
|
-
"cancelled"
|
|
96
|
-
];
|
|
97
|
-
|
|
98
240
|
//#endregion
|
|
99
241
|
//#region lib/types.ts
|
|
100
242
|
const taskStatuses = [
|
|
@@ -107,18 +249,29 @@ const taskStatuses = [
|
|
|
107
249
|
const taskTypes = ["regular", "pr_review"];
|
|
108
250
|
|
|
109
251
|
//#endregion
|
|
110
|
-
//#region lib/
|
|
111
|
-
const logEntrySchema = z.object({
|
|
112
|
-
ts: z.string(),
|
|
113
|
-
msg: z.string(),
|
|
114
|
-
status: z.enum(taskStatuses).optional()
|
|
115
|
-
});
|
|
252
|
+
//#region lib/schemas/dayAssignment.ts
|
|
116
253
|
const dayAssignmentSchema = z.object({
|
|
117
254
|
date: z.string(),
|
|
118
|
-
action: z.enum([
|
|
255
|
+
action: z.enum([
|
|
256
|
+
"assign",
|
|
257
|
+
"unassign",
|
|
258
|
+
"reorder"
|
|
259
|
+
]),
|
|
119
260
|
ts: z.string(),
|
|
261
|
+
order: z.number().int().positive().optional(),
|
|
120
262
|
msg: z.string().optional()
|
|
121
263
|
});
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region lib/schemas/logEntry.ts
|
|
267
|
+
const logEntrySchema = z.object({
|
|
268
|
+
ts: z.string(),
|
|
269
|
+
msg: z.string(),
|
|
270
|
+
status: z.enum(taskStatuses).optional()
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region lib/schemas/pr.ts
|
|
122
275
|
const prRepoSchema = z.object({
|
|
123
276
|
host: z.string(),
|
|
124
277
|
owner: z.string(),
|
|
@@ -143,6 +296,9 @@ const prAttachmentSchema = z.object({
|
|
|
143
296
|
number: z.number().int().positive().optional(),
|
|
144
297
|
fetched: prFetchedSchema.optional()
|
|
145
298
|
});
|
|
299
|
+
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region lib/schemas/task.ts
|
|
146
302
|
const taskMetadataSchema = z.object({
|
|
147
303
|
url: z.string().optional(),
|
|
148
304
|
pr: prAttachmentSchema.optional()
|
|
@@ -173,6 +329,107 @@ const validateTaskOrThrow = (task) => {
|
|
|
173
329
|
return taskSchema.parse(task);
|
|
174
330
|
};
|
|
175
331
|
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region lib/id.ts
|
|
334
|
+
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
335
|
+
const base32Encode = (input) => {
|
|
336
|
+
let bits = 0;
|
|
337
|
+
let value = 0;
|
|
338
|
+
let output = "";
|
|
339
|
+
for (const byte of input) {
|
|
340
|
+
value = value << 8 | byte;
|
|
341
|
+
bits += 8;
|
|
342
|
+
while (bits >= 5) {
|
|
343
|
+
output += BASE32_ALPHABET[value >>> bits - 5 & 31];
|
|
344
|
+
bits -= 5;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (bits > 0) output += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
348
|
+
return output;
|
|
349
|
+
};
|
|
350
|
+
const generateTaskId = (taskType) => {
|
|
351
|
+
return `${taskType === "pr_review" ? "R" : "T"}${ulid()}`;
|
|
352
|
+
};
|
|
353
|
+
const generateStableRef = (taskType, taskId) => {
|
|
354
|
+
return `${taskType === "pr_review" ? "R" : "T"}-${base32Encode(createHash("sha256").update(taskId).digest()).slice(0, 6)}`;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region lib/taskFactory.ts
|
|
359
|
+
const buildTask = (input) => {
|
|
360
|
+
const id = generateTaskId(input.type);
|
|
361
|
+
const ref = generateStableRef(input.type, id);
|
|
362
|
+
const timestamp = nowIso();
|
|
363
|
+
return {
|
|
364
|
+
id,
|
|
365
|
+
ref,
|
|
366
|
+
type: input.type,
|
|
367
|
+
title: input.title,
|
|
368
|
+
status: "backlog",
|
|
369
|
+
created_at: timestamp,
|
|
370
|
+
updated_at: timestamp,
|
|
371
|
+
metadata: input.metadata ?? {},
|
|
372
|
+
logs: [],
|
|
373
|
+
day_assignments: []
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region lib/aliasRepo.ts
|
|
379
|
+
const emptyAliases = () => ({
|
|
380
|
+
T: {},
|
|
381
|
+
R: {}
|
|
382
|
+
});
|
|
383
|
+
const aliasFilePath = (workspaceRoot) => path.join(workspaceRoot, "aliases.json");
|
|
384
|
+
const fileExists$1 = async (filePath) => {
|
|
385
|
+
try {
|
|
386
|
+
await access(filePath);
|
|
387
|
+
return true;
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const readAliases = async (workspaceRoot) => {
|
|
393
|
+
const filePath = aliasFilePath(workspaceRoot);
|
|
394
|
+
if (!await fileExists$1(filePath)) return emptyAliases();
|
|
395
|
+
const data = await readJsonFile(filePath);
|
|
396
|
+
return {
|
|
397
|
+
T: data.T ?? {},
|
|
398
|
+
R: data.R ?? {}
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
const writeAliases = async (workspaceRoot, aliases) => {
|
|
402
|
+
await writeJsonAtomic(aliasFilePath(workspaceRoot), aliases);
|
|
403
|
+
};
|
|
404
|
+
const parseAlias = (alias) => {
|
|
405
|
+
const match = alias.match(/^([TR])(\d+)$/);
|
|
406
|
+
if (!match) return null;
|
|
407
|
+
return {
|
|
408
|
+
prefix: match[1],
|
|
409
|
+
key: match[2]
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
const formatAliasKey = (value) => String(value).padStart(2, "0");
|
|
413
|
+
const allocateAliasInMap = (aliases, prefix, taskId) => {
|
|
414
|
+
const used = new Set(Object.keys(aliases[prefix]).map((key) => Number.parseInt(key, 10)));
|
|
415
|
+
let next = 1;
|
|
416
|
+
while (used.has(next)) next += 1;
|
|
417
|
+
const key = formatAliasKey(next);
|
|
418
|
+
aliases[prefix][key] = taskId;
|
|
419
|
+
return {
|
|
420
|
+
alias: `${prefix}${key}`,
|
|
421
|
+
aliases
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const allocateAlias = async (workspaceRoot, prefix, taskId) => {
|
|
425
|
+
return allocateAliasInMap(await readAliases(workspaceRoot), prefix, taskId);
|
|
426
|
+
};
|
|
427
|
+
const resolveAlias = async (workspaceRoot, alias) => {
|
|
428
|
+
const parsed = parseAlias(alias);
|
|
429
|
+
if (!parsed) return null;
|
|
430
|
+
return (await readAliases(workspaceRoot))[parsed.prefix][parsed.key] ?? null;
|
|
431
|
+
};
|
|
432
|
+
|
|
176
433
|
//#endregion
|
|
177
434
|
//#region lib/taskRepo.ts
|
|
178
435
|
const taskFileName = (taskId) => `${taskId}.json`;
|
|
@@ -180,7 +437,7 @@ const getStatusDir = (workspaceRoot, status) => path.join(workspaceRoot, TASKS_D
|
|
|
180
437
|
const getTaskPath = (workspaceRoot, status, taskId) => {
|
|
181
438
|
return path.join(getStatusDir(workspaceRoot, status), taskFileName(taskId));
|
|
182
439
|
};
|
|
183
|
-
const fileExists
|
|
440
|
+
const fileExists = async (filePath) => {
|
|
184
441
|
try {
|
|
185
442
|
await access(filePath);
|
|
186
443
|
return true;
|
|
@@ -191,7 +448,7 @@ const fileExists$1 = async (filePath) => {
|
|
|
191
448
|
const findTaskPathById = async (workspaceRoot, taskId) => {
|
|
192
449
|
for (const status of STATUS_ORDER) {
|
|
193
450
|
const candidate = getTaskPath(workspaceRoot, status, taskId);
|
|
194
|
-
if (await fileExists
|
|
451
|
+
if (await fileExists(candidate)) return {
|
|
195
452
|
filePath: candidate,
|
|
196
453
|
status
|
|
197
454
|
};
|
|
@@ -229,6 +486,116 @@ const moveTaskFile = async (workspaceRoot, taskId, fromStatus, toStatus) => {
|
|
|
229
486
|
return toPath;
|
|
230
487
|
};
|
|
231
488
|
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region lib/taskStore.ts
|
|
491
|
+
const createTaskInStore = async (workspaceRoot, task) => {
|
|
492
|
+
const { alias, aliases } = await allocateAlias(workspaceRoot, task.type === "pr_review" ? "R" : "T", task.id);
|
|
493
|
+
await writeAliases(workspaceRoot, aliases);
|
|
494
|
+
await saveTask(workspaceRoot, task.status, task);
|
|
495
|
+
return {
|
|
496
|
+
alias,
|
|
497
|
+
task
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region lib/addTask.ts
|
|
503
|
+
var AddTaskError = class extends Error {
|
|
504
|
+
code;
|
|
505
|
+
constructor(code, message) {
|
|
506
|
+
super(message);
|
|
507
|
+
this.code = code;
|
|
508
|
+
this.name = "AddTaskError";
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const addTaskInputSchema = z.object({
|
|
512
|
+
type: z.enum(taskTypes),
|
|
513
|
+
title: z.string().optional(),
|
|
514
|
+
url: z.string().optional(),
|
|
515
|
+
prUrl: z.string().optional(),
|
|
516
|
+
workspaceRoot: z.string()
|
|
517
|
+
});
|
|
518
|
+
const addTask = async (input) => {
|
|
519
|
+
const parsedInput = addTaskInputSchema.parse(input);
|
|
520
|
+
return withWorkspaceLock(parsedInput.workspaceRoot, async () => {
|
|
521
|
+
let prAttachment;
|
|
522
|
+
let prTitle;
|
|
523
|
+
if (parsedInput.type === "pr_review" && !parsedInput.prUrl) throw new AddTaskError("MISSING_PR_URL", "PR review tasks require PR URL");
|
|
524
|
+
if (parsedInput.prUrl) {
|
|
525
|
+
const parsed = parseGitHubPrUrl(parsedInput.prUrl);
|
|
526
|
+
if (!parsed) throw new AddTaskError("INVALID_PR_URL", "Invalid PR URL");
|
|
527
|
+
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(parsedInput.workspaceRoot));
|
|
528
|
+
prAttachment = buildPrAttachment(parsed, fetched ?? void 0);
|
|
529
|
+
prTitle = fetched?.title ?? `PR #${parsed.number} ${parsed.repo.owner}/${parsed.repo.name}`;
|
|
530
|
+
}
|
|
531
|
+
const isExplicitTitle = Boolean(parsedInput.title);
|
|
532
|
+
let finalTitle = parsedInput.title ?? prTitle;
|
|
533
|
+
if (!finalTitle) throw new AddTaskError("MISSING_TITLE", "Title is required when no PR URL is provided");
|
|
534
|
+
if (parsedInput.type === "pr_review" && !isExplicitTitle) finalTitle = `Review: ${finalTitle}`;
|
|
535
|
+
const metadata = {
|
|
536
|
+
url: parsedInput.url,
|
|
537
|
+
pr: prAttachment
|
|
538
|
+
};
|
|
539
|
+
const task = buildTask({
|
|
540
|
+
type: parsedInput.type,
|
|
541
|
+
title: finalTitle,
|
|
542
|
+
metadata
|
|
543
|
+
});
|
|
544
|
+
taskSchema.parse(task);
|
|
545
|
+
return createTaskInStore(parsedInput.workspaceRoot, task);
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region cli/schemas/addTask.ts
|
|
551
|
+
const addTaskCliSchema = z.object({
|
|
552
|
+
title: z.string().optional(),
|
|
553
|
+
type: z.enum(taskTypes),
|
|
554
|
+
url: z.string().optional(),
|
|
555
|
+
pr: z.string().optional()
|
|
556
|
+
}).superRefine((data, ctx) => {
|
|
557
|
+
if (data.type === "pr_review" && !data.pr) ctx.addIssue({
|
|
558
|
+
code: z.ZodIssueCode.custom,
|
|
559
|
+
message: "PR review tasks require --pr",
|
|
560
|
+
path: ["pr"]
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region cli/commands/add.ts
|
|
566
|
+
const registerAddCommand = (program, helpers) => {
|
|
567
|
+
program.command("add").description("add a task").argument("[title]", "task title").option("--type <type>", "regular|pr_review", "regular").option("--url <url>", "attach URL").option("--pr <url>", "attach PR URL").action(helpers.handleAction(async (title, options, command) => {
|
|
568
|
+
const parsed = addTaskCliSchema.safeParse({
|
|
569
|
+
title,
|
|
570
|
+
type: options.type ?? "regular",
|
|
571
|
+
url: options.url,
|
|
572
|
+
pr: options.pr
|
|
573
|
+
});
|
|
574
|
+
if (!parsed.success) {
|
|
575
|
+
const message = parsed.error.errors.map((issue) => issue.message).join(", ");
|
|
576
|
+
helpers.failWithHelp(command, message);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
580
|
+
try {
|
|
581
|
+
const created = await addTask({
|
|
582
|
+
type: parsed.data.type,
|
|
583
|
+
title: parsed.data.title,
|
|
584
|
+
url: parsed.data.url,
|
|
585
|
+
prUrl: parsed.data.pr,
|
|
586
|
+
workspaceRoot
|
|
587
|
+
});
|
|
588
|
+
process.stdout.write(`Created ${created.alias} ${created.task.ref} ${created.task.title}\n`);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (error instanceof AddTaskError) {
|
|
591
|
+
helpers.failWithHelp(command, error.message);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}));
|
|
597
|
+
};
|
|
598
|
+
|
|
232
599
|
//#endregion
|
|
233
600
|
//#region lib/aliasReconcile.ts
|
|
234
601
|
const reconcileAliases = async (workspaceRoot) => {
|
|
@@ -250,121 +617,15 @@ const reconcileAliases = async (workspaceRoot) => {
|
|
|
250
617
|
};
|
|
251
618
|
|
|
252
619
|
//#endregion
|
|
253
|
-
//#region
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
const resolveGlobalConfigPath = () => {
|
|
263
|
-
const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config");
|
|
264
|
-
return path.join(configHome, APP_NAME, "config.json");
|
|
265
|
-
};
|
|
266
|
-
const readConfigFile = async (filePath) => {
|
|
267
|
-
if (!await fileExists(filePath)) return {};
|
|
268
|
-
return await readJsonFile(filePath) ?? {};
|
|
269
|
-
};
|
|
270
|
-
const resolveGithubToken = async (workspaceRoot) => {
|
|
271
|
-
if (process.env.IL_GITHUB_TOKEN) return process.env.IL_GITHUB_TOKEN;
|
|
272
|
-
const workspaceConfig = await readConfigFile(path.join(workspaceRoot, "config.json"));
|
|
273
|
-
if (workspaceConfig.github?.token) return workspaceConfig.github.token;
|
|
274
|
-
return (await readConfigFile(resolveGlobalConfigPath())).github?.token ?? null;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
//#endregion
|
|
278
|
-
//#region lib/time.ts
|
|
279
|
-
const DEFAULT_ZONE = "Europe/Paris";
|
|
280
|
-
const toIsoDate = (value, label) => {
|
|
281
|
-
const date = value.toISODate();
|
|
282
|
-
if (!date) throw new Error(`Failed to generate ${label} date`);
|
|
283
|
-
return date;
|
|
284
|
-
};
|
|
285
|
-
const buildDateRange = (start, end) => {
|
|
286
|
-
const dates = [];
|
|
287
|
-
let cursor = start.startOf("day");
|
|
288
|
-
const final = end.startOf("day");
|
|
289
|
-
while (cursor <= final) {
|
|
290
|
-
dates.push(toIsoDate(cursor, "range"));
|
|
291
|
-
cursor = cursor.plus({ days: 1 });
|
|
292
|
-
}
|
|
293
|
-
return dates;
|
|
294
|
-
};
|
|
295
|
-
const nowIso = () => {
|
|
296
|
-
const value = DateTime.now().setZone(DEFAULT_ZONE).toISO();
|
|
297
|
-
if (!value) throw new Error("Failed to generate timestamp");
|
|
298
|
-
return value;
|
|
299
|
-
};
|
|
300
|
-
const todayDate = (date) => {
|
|
301
|
-
const value = date ? DateTime.fromISO(date, { zone: DEFAULT_ZONE }).toISODate() : DateTime.now().setZone(DEFAULT_ZONE).toISODate();
|
|
302
|
-
if (!value) throw new Error("Failed to generate date");
|
|
303
|
-
return value;
|
|
304
|
-
};
|
|
305
|
-
const listDatesForScope = (scope) => {
|
|
306
|
-
const now = DateTime.now().setZone(DEFAULT_ZONE);
|
|
307
|
-
switch (scope) {
|
|
308
|
-
case "today": return [toIsoDate(now, "today")];
|
|
309
|
-
case "yesterday": return [toIsoDate(now.minus({ days: 1 }), "yesterday")];
|
|
310
|
-
case "this_week": return buildDateRange(now.startOf("week"), now.endOf("week"));
|
|
311
|
-
case "last_week": {
|
|
312
|
-
const lastWeek = now.minus({ weeks: 1 });
|
|
313
|
-
return buildDateRange(lastWeek.startOf("week"), lastWeek.endOf("week"));
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
//#endregion
|
|
319
|
-
//#region lib/pr.ts
|
|
320
|
-
const parseGitHubPrUrl = (value) => {
|
|
321
|
-
let url;
|
|
322
|
-
try {
|
|
323
|
-
url = new URL(value);
|
|
324
|
-
} catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
const parts = url.pathname.split("/").filter(Boolean);
|
|
328
|
-
if (parts.length < 4 || parts[2] !== "pull") return null;
|
|
329
|
-
const number = Number(parts[3]);
|
|
330
|
-
if (!Number.isInteger(number)) return null;
|
|
331
|
-
return {
|
|
332
|
-
url: value,
|
|
333
|
-
provider: "github",
|
|
334
|
-
repo: {
|
|
335
|
-
host: url.host,
|
|
336
|
-
owner: parts[0],
|
|
337
|
-
name: parts[1]
|
|
338
|
-
},
|
|
339
|
-
number
|
|
340
|
-
};
|
|
341
|
-
};
|
|
342
|
-
const buildPrAttachment = (parsed, fetched) => {
|
|
343
|
-
return {
|
|
344
|
-
url: parsed.url,
|
|
345
|
-
provider: "github",
|
|
346
|
-
repo: parsed.repo,
|
|
347
|
-
number: parsed.number,
|
|
348
|
-
fetched
|
|
349
|
-
};
|
|
350
|
-
};
|
|
351
|
-
const fetchGitHubPr = async (parsed, token) => {
|
|
352
|
-
if (!parsed.repo || !parsed.number) return null;
|
|
353
|
-
if (!token) return null;
|
|
354
|
-
const data = (await new Octokit({ auth: token }).rest.pulls.get({
|
|
355
|
-
owner: parsed.repo.owner,
|
|
356
|
-
repo: parsed.repo.name,
|
|
357
|
-
pull_number: parsed.number
|
|
358
|
-
})).data;
|
|
359
|
-
const state = data.merged ? "merged" : data.state === "open" ? "open" : "closed";
|
|
360
|
-
return {
|
|
361
|
-
at: nowIso(),
|
|
362
|
-
title: data.title,
|
|
363
|
-
author: { login: data.user?.login ?? "unknown" },
|
|
364
|
-
state,
|
|
365
|
-
draft: data.draft ?? false,
|
|
366
|
-
updated_at: data.updated_at
|
|
367
|
-
};
|
|
620
|
+
//#region cli/commands/alias.ts
|
|
621
|
+
const registerAliasCommand = (program, helpers) => {
|
|
622
|
+
program.command("alias").description("alias helpers").command("reconcile").description("reconcile alias mapping").action(helpers.handleAction(async () => {
|
|
623
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
624
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
625
|
+
await reconcileAliases(workspaceRoot);
|
|
626
|
+
process.stdout.write("Aliases reconciled.\n");
|
|
627
|
+
});
|
|
628
|
+
}));
|
|
368
629
|
};
|
|
369
630
|
|
|
370
631
|
//#endregion
|
|
@@ -396,10 +657,78 @@ const applyTaskEdit = (task, dottedPath, rawValue) => {
|
|
|
396
657
|
if (!parsed) throw new Error("Invalid PR URL");
|
|
397
658
|
updated.metadata.pr = buildPrAttachment(parsed, updated.metadata.pr?.fetched);
|
|
398
659
|
}
|
|
399
|
-
return validateTaskOrThrow({
|
|
400
|
-
...updated,
|
|
401
|
-
updated_at: nowIso()
|
|
402
|
-
});
|
|
660
|
+
return validateTaskOrThrow({
|
|
661
|
+
...updated,
|
|
662
|
+
updated_at: nowIso()
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region lib/taskResolver.ts
|
|
668
|
+
const stableRefPattern = /^[TR]-[A-Z0-9]{6}$/;
|
|
669
|
+
const resolveTaskId = async (workspaceRoot, identifier) => {
|
|
670
|
+
const aliasMatch = await resolveAlias(workspaceRoot, identifier);
|
|
671
|
+
if (aliasMatch) return aliasMatch;
|
|
672
|
+
if (stableRefPattern.test(identifier)) {
|
|
673
|
+
const match = (await listAllTasks(workspaceRoot)).find((stored) => stored.task.ref === identifier);
|
|
674
|
+
if (!match) throw new Error(`Task not found for ref: ${identifier}`);
|
|
675
|
+
return match.task.id;
|
|
676
|
+
}
|
|
677
|
+
if (!await findTaskPathById(workspaceRoot, identifier)) throw new Error(`Task not found: ${identifier}`);
|
|
678
|
+
return identifier;
|
|
679
|
+
};
|
|
680
|
+
const resolveTask = async (workspaceRoot, identifier) => {
|
|
681
|
+
const found = await findTaskPathById(workspaceRoot, await resolveTaskId(workspaceRoot, identifier));
|
|
682
|
+
if (!found) throw new Error(`Task not found: ${identifier}`);
|
|
683
|
+
return {
|
|
684
|
+
task: await loadTaskFromPath(found.filePath),
|
|
685
|
+
status: found.status,
|
|
686
|
+
filePath: found.filePath
|
|
687
|
+
};
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region cli/commands/edit.ts
|
|
692
|
+
const registerEditCommand = (program, helpers) => {
|
|
693
|
+
program.command("edit").description("edit a task field").argument("<id>", "task identifier").argument("<path>", "dotted path").argument("<value>", "new value").action(helpers.handleAction(async (identifier, dottedPath, value) => {
|
|
694
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
695
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
696
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
697
|
+
const updated = applyTaskEdit(stored.task, dottedPath, value);
|
|
698
|
+
await saveTask(workspaceRoot, stored.status, updated);
|
|
699
|
+
process.stdout.write(`Updated ${updated.ref}\n`);
|
|
700
|
+
});
|
|
701
|
+
}));
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region lib/gitignore.ts
|
|
706
|
+
const lockEntry = `${APP_DIR}/.lock/`;
|
|
707
|
+
const ensureLockIgnored = async (repoRoot) => {
|
|
708
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
709
|
+
let current = "";
|
|
710
|
+
try {
|
|
711
|
+
current = await readFile(gitignorePath, "utf8");
|
|
712
|
+
} catch {
|
|
713
|
+
current = "";
|
|
714
|
+
}
|
|
715
|
+
if (current.split("\n").some((line) => line.trim() === lockEntry)) return false;
|
|
716
|
+
const separator = current.endsWith("\n") || current.length === 0 ? "" : "\n";
|
|
717
|
+
await writeFile(gitignorePath, `${current}${separator}${lockEntry}\n`, "utf8");
|
|
718
|
+
return true;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region cli/commands/init.ts
|
|
723
|
+
const registerInitCommand = (program, helpers) => {
|
|
724
|
+
program.command("init").description("initialize repo workspace").action(helpers.handleAction(async () => {
|
|
725
|
+
const repoRoot = await findGitRoot(process.cwd());
|
|
726
|
+
if (!repoRoot) throw new Error("Not inside a git repository");
|
|
727
|
+
const workspaceRoot = path.join(repoRoot, APP_DIR);
|
|
728
|
+
await ensureWorkspaceLayout(workspaceRoot);
|
|
729
|
+
await ensureLockIgnored(repoRoot);
|
|
730
|
+
process.stdout.write(`Initialized workspace at ${workspaceRoot}\n`);
|
|
731
|
+
}));
|
|
403
732
|
};
|
|
404
733
|
|
|
405
734
|
//#endregion
|
|
@@ -443,108 +772,245 @@ const formatTaskDetails = (task, alias) => {
|
|
|
443
772
|
if (task.day_assignments.length > 0) {
|
|
444
773
|
lines.push("day_assignments:");
|
|
445
774
|
for (const entry of task.day_assignments) {
|
|
775
|
+
const order = entry.order ? ` order:${entry.order}` : "";
|
|
446
776
|
const msg = entry.msg ? ` (${entry.msg})` : "";
|
|
447
|
-
lines.push(`- ${entry.date} ${entry.action} ${entry.ts}${msg}`);
|
|
777
|
+
lines.push(`- ${entry.date} ${entry.action}${order} ${entry.ts}${msg}`);
|
|
448
778
|
}
|
|
449
779
|
}
|
|
450
780
|
return lines.join("\n");
|
|
451
781
|
};
|
|
452
782
|
|
|
453
783
|
//#endregion
|
|
454
|
-
//#region lib/
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
784
|
+
//#region lib/fsm.ts
|
|
785
|
+
const transitionTable = {
|
|
786
|
+
backlog: {
|
|
787
|
+
start: "active",
|
|
788
|
+
cancel: "cancelled"
|
|
789
|
+
},
|
|
790
|
+
active: {
|
|
791
|
+
pause: "paused",
|
|
792
|
+
complete: "completed",
|
|
793
|
+
cancel: "cancelled"
|
|
794
|
+
},
|
|
795
|
+
paused: {
|
|
796
|
+
start: "active",
|
|
797
|
+
complete: "completed",
|
|
798
|
+
cancel: "cancelled"
|
|
799
|
+
},
|
|
800
|
+
completed: {},
|
|
801
|
+
cancelled: {}
|
|
802
|
+
};
|
|
803
|
+
const getNextStatus = (current, action) => {
|
|
804
|
+
const next = transitionTable[current][action];
|
|
805
|
+
if (!next) throw new Error(`Invalid transition: ${current} -> ${action}`);
|
|
806
|
+
return next;
|
|
468
807
|
};
|
|
469
808
|
|
|
470
809
|
//#endregion
|
|
471
|
-
//#region lib/
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
810
|
+
//#region lib/taskOperations.ts
|
|
811
|
+
const defaultMessages = {
|
|
812
|
+
start: "Started task",
|
|
813
|
+
pause: "Paused task",
|
|
814
|
+
complete: "Completed task",
|
|
815
|
+
cancel: "Cancelled task"
|
|
479
816
|
};
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
817
|
+
const appendLog = (task, message, status) => {
|
|
818
|
+
const log = {
|
|
819
|
+
ts: nowIso(),
|
|
820
|
+
msg: message,
|
|
821
|
+
status
|
|
822
|
+
};
|
|
823
|
+
return {
|
|
824
|
+
...task,
|
|
825
|
+
logs: [...task.logs, log],
|
|
826
|
+
updated_at: nowIso()
|
|
827
|
+
};
|
|
488
828
|
};
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
829
|
+
const applyTransition = (task, action, message) => {
|
|
830
|
+
const nextStatus = getNextStatus(task.status, action);
|
|
831
|
+
const logMessage = message ?? defaultMessages[action];
|
|
832
|
+
const log = {
|
|
833
|
+
ts: nowIso(),
|
|
834
|
+
msg: logMessage,
|
|
835
|
+
status: nextStatus
|
|
836
|
+
};
|
|
837
|
+
return {
|
|
838
|
+
task: {
|
|
839
|
+
...task,
|
|
840
|
+
status: nextStatus,
|
|
841
|
+
updated_at: nowIso(),
|
|
842
|
+
logs: [...task.logs, log]
|
|
843
|
+
},
|
|
844
|
+
nextStatus
|
|
845
|
+
};
|
|
492
846
|
};
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
847
|
+
const isAssignedOnDate = (task, date) => {
|
|
848
|
+
const latest = getLatestDayAssignment(task, date);
|
|
849
|
+
if (!latest) return false;
|
|
850
|
+
return latest.event.action === "assign" || latest.event.action === "reorder";
|
|
851
|
+
};
|
|
852
|
+
const getLatestDayAssignment = (task, date) => {
|
|
853
|
+
let latestIndex = -1;
|
|
854
|
+
for (let i = 0; i < task.day_assignments.length; i += 1) if (task.day_assignments[i]?.date === date) latestIndex = i;
|
|
855
|
+
if (latestIndex === -1) return null;
|
|
856
|
+
const event = task.day_assignments[latestIndex];
|
|
857
|
+
if (!event) return null;
|
|
858
|
+
return {
|
|
859
|
+
event,
|
|
860
|
+
index: latestIndex
|
|
497
861
|
};
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (options.global) return {
|
|
507
|
-
root: resolveGlobalWorkspaceRoot(),
|
|
508
|
-
kind: "global"
|
|
862
|
+
};
|
|
863
|
+
const appendDayAssignment = (task, date, action, msg, order) => {
|
|
864
|
+
const event = {
|
|
865
|
+
date,
|
|
866
|
+
action,
|
|
867
|
+
ts: nowIso(),
|
|
868
|
+
order,
|
|
869
|
+
msg
|
|
509
870
|
};
|
|
510
|
-
if (repoRoot) {
|
|
511
|
-
const repoWorkspace = path.join(repoRoot, APP_DIR);
|
|
512
|
-
if (await exists(repoWorkspace)) return {
|
|
513
|
-
root: repoWorkspace,
|
|
514
|
-
kind: "repo"
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
871
|
return {
|
|
518
|
-
|
|
519
|
-
|
|
872
|
+
...task,
|
|
873
|
+
day_assignments: [...task.day_assignments, event],
|
|
874
|
+
updated_at: nowIso()
|
|
520
875
|
};
|
|
521
876
|
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
877
|
+
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region cli/commands/list.ts
|
|
880
|
+
const listScopes = [
|
|
881
|
+
"today",
|
|
882
|
+
"yesterday",
|
|
883
|
+
"this_week",
|
|
884
|
+
"last_week"
|
|
885
|
+
];
|
|
886
|
+
const isTaskStatus = (value) => {
|
|
887
|
+
return taskStatuses.includes(value);
|
|
888
|
+
};
|
|
889
|
+
const isListScope = (value) => {
|
|
890
|
+
return listScopes.includes(value);
|
|
891
|
+
};
|
|
892
|
+
const registerListCommand = (program, helpers) => {
|
|
893
|
+
program.command("list").description("list tasks").argument("[status]", "task status or date scope").action(helpers.handleAction(async (status, command) => {
|
|
894
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
895
|
+
const aliasLookup = buildAliasLookup(await readAliases(workspaceRoot));
|
|
896
|
+
const lines = [];
|
|
897
|
+
if (status && !isTaskStatus(status) && !isListScope(status)) {
|
|
898
|
+
helpers.failWithHelp(command, `Invalid status or scope: ${status}`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (status && isListScope(status)) {
|
|
902
|
+
const dates = listDatesForScope(status);
|
|
903
|
+
const tasks = await listAllTasks(workspaceRoot);
|
|
904
|
+
for (const entry of tasks) {
|
|
905
|
+
if (!dates.some((date) => isAssignedOnDate(entry.task, date))) continue;
|
|
906
|
+
const alias = aliasLookup.get(entry.task.id);
|
|
907
|
+
let line = formatTaskListLine(entry.task, alias);
|
|
908
|
+
if (entry.task.metadata.pr?.fetched?.state) line += ` PR:${entry.task.metadata.pr.fetched.state}`;
|
|
909
|
+
lines.push(line);
|
|
910
|
+
}
|
|
911
|
+
helpers.printLines(lines);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const statuses = status ? [status] : [...taskStatuses];
|
|
915
|
+
for (const currentStatus of statuses) {
|
|
916
|
+
const stored = await listTasksByStatus(workspaceRoot, currentStatus);
|
|
917
|
+
for (const entry of stored) {
|
|
918
|
+
const alias = aliasLookup.get(entry.task.id);
|
|
919
|
+
let line = formatTaskListLine(entry.task, alias);
|
|
920
|
+
if (entry.task.metadata.pr?.fetched?.state) line += ` PR:${entry.task.metadata.pr.fetched.state}`;
|
|
921
|
+
lines.push(line);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
helpers.printLines(lines);
|
|
925
|
+
}));
|
|
526
926
|
};
|
|
527
927
|
|
|
528
928
|
//#endregion
|
|
529
|
-
//#region
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
retries: 5,
|
|
538
|
-
factor: 1.5,
|
|
539
|
-
minTimeout: 50,
|
|
540
|
-
maxTimeout: 1e3
|
|
929
|
+
//#region cli/commands/log.ts
|
|
930
|
+
const registerLogCommand = (program, helpers) => {
|
|
931
|
+
program.command("log").description("append a log entry").argument("<id>", "task identifier").argument("<message>", "log message").option("--status <status>", "include status in log entry").action(helpers.handleAction(async (identifier, message, options, command) => {
|
|
932
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
933
|
+
const status = options.status;
|
|
934
|
+
if (status && !taskStatuses.includes(status)) {
|
|
935
|
+
helpers.failWithHelp(command, `Invalid status: ${status}`);
|
|
936
|
+
return;
|
|
541
937
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
938
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
939
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
940
|
+
const updated = appendLog(stored.task, message, status);
|
|
941
|
+
await saveTask(workspaceRoot, stored.status, updated);
|
|
942
|
+
process.stdout.write(`Logged on ${updated.ref}\n`);
|
|
943
|
+
});
|
|
944
|
+
}));
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region cli/commands/pr.ts
|
|
949
|
+
const registerPrCommands = (program, helpers) => {
|
|
950
|
+
program.command("attach-pr").description("attach PR metadata to a task").argument("<id>", "task identifier").argument("<url>", "PR URL").option("-m, --message <message>", "log message").action(helpers.handleAction(async (identifier, url, options, command) => {
|
|
951
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
952
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
953
|
+
const parsed = parseGitHubPrUrl(url);
|
|
954
|
+
if (!parsed) {
|
|
955
|
+
helpers.failWithHelp(command, "Invalid PR URL");
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(workspaceRoot));
|
|
959
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
960
|
+
const next = appendLog({
|
|
961
|
+
...stored.task,
|
|
962
|
+
metadata: {
|
|
963
|
+
...stored.task.metadata,
|
|
964
|
+
pr: buildPrAttachment(parsed, fetched ?? stored.task.metadata.pr?.fetched)
|
|
965
|
+
},
|
|
966
|
+
updated_at: nowIso()
|
|
967
|
+
}, options.message ?? `Attached PR ${url}`);
|
|
968
|
+
await saveTask(workspaceRoot, stored.status, next);
|
|
969
|
+
process.stdout.write(`Attached PR to ${next.ref}\n`);
|
|
970
|
+
});
|
|
971
|
+
}));
|
|
972
|
+
program.command("refresh").description("refresh PR metadata").argument("<id>", "task identifier").option("-m, --message <message>", "log message").action(helpers.handleAction(async (identifier, options) => {
|
|
973
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
974
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
975
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
976
|
+
const prUrl = stored.task.metadata.pr?.url;
|
|
977
|
+
if (!prUrl) {
|
|
978
|
+
process.stdout.write("No PR attached.\n");
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const parsed = parseGitHubPrUrl(prUrl);
|
|
982
|
+
if (!parsed) throw new Error("Invalid PR URL");
|
|
983
|
+
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(workspaceRoot));
|
|
984
|
+
if (!fetched) throw new Error("No GitHub token configured");
|
|
985
|
+
const baseUpdated = {
|
|
986
|
+
...stored.task,
|
|
987
|
+
metadata: {
|
|
988
|
+
...stored.task.metadata,
|
|
989
|
+
pr: buildPrAttachment(parsed, fetched)
|
|
990
|
+
},
|
|
991
|
+
updated_at: nowIso()
|
|
992
|
+
};
|
|
993
|
+
const previousState = stored.task.metadata.pr?.fetched?.state;
|
|
994
|
+
const nextState = fetched.state;
|
|
995
|
+
const isTerminal = stored.task.status === "completed" || stored.task.status === "cancelled";
|
|
996
|
+
let finalTask = baseUpdated;
|
|
997
|
+
let nextStatus = stored.status;
|
|
998
|
+
if (!isTerminal && previousState !== nextState) {
|
|
999
|
+
if (nextState === "merged") {
|
|
1000
|
+
const result = applyTransition(baseUpdated, "complete", options.message ?? "PR merged");
|
|
1001
|
+
finalTask = result.task;
|
|
1002
|
+
nextStatus = result.nextStatus;
|
|
1003
|
+
} else if (nextState === "closed") {
|
|
1004
|
+
const result = applyTransition(baseUpdated, "cancel", options.message ?? "PR closed without merge");
|
|
1005
|
+
finalTask = result.task;
|
|
1006
|
+
nextStatus = result.nextStatus;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
await saveTask(workspaceRoot, stored.status, finalTask);
|
|
1010
|
+
if (nextStatus !== stored.status) await moveTaskFile(workspaceRoot, finalTask.id, stored.status, nextStatus);
|
|
1011
|
+
process.stdout.write(`Refreshed ${finalTask.ref}\n`);
|
|
1012
|
+
});
|
|
1013
|
+
}));
|
|
548
1014
|
};
|
|
549
1015
|
|
|
550
1016
|
//#endregion
|
|
@@ -560,172 +1026,196 @@ const taskMatchesQuery = (task, query) => {
|
|
|
560
1026
|
};
|
|
561
1027
|
|
|
562
1028
|
//#endregion
|
|
563
|
-
//#region
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
if (bits > 0) output += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
578
|
-
return output;
|
|
579
|
-
};
|
|
580
|
-
const generateTaskId = (taskType) => {
|
|
581
|
-
return `${taskType === "pr_review" ? "R" : "T"}${ulid()}`;
|
|
582
|
-
};
|
|
583
|
-
const generateStableRef = (taskType, taskId) => {
|
|
584
|
-
return `${taskType === "pr_review" ? "R" : "T"}-${base32Encode(createHash("sha256").update(taskId).digest()).slice(0, 6)}`;
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
//#endregion
|
|
588
|
-
//#region lib/taskFactory.ts
|
|
589
|
-
const buildTask = (input) => {
|
|
590
|
-
const id = generateTaskId(input.type);
|
|
591
|
-
const ref = generateStableRef(input.type, id);
|
|
592
|
-
const timestamp = nowIso();
|
|
593
|
-
return {
|
|
594
|
-
id,
|
|
595
|
-
ref,
|
|
596
|
-
type: input.type,
|
|
597
|
-
title: input.title,
|
|
598
|
-
status: "backlog",
|
|
599
|
-
created_at: timestamp,
|
|
600
|
-
updated_at: timestamp,
|
|
601
|
-
metadata: input.metadata ?? {},
|
|
602
|
-
logs: [],
|
|
603
|
-
day_assignments: []
|
|
604
|
-
};
|
|
1029
|
+
//#region cli/commands/search.ts
|
|
1030
|
+
const registerSearchCommand = (program, helpers) => {
|
|
1031
|
+
program.command("search").description("search tasks").argument("<query>", "search query").action(helpers.handleAction(async (query) => {
|
|
1032
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1033
|
+
const aliasLookup = buildAliasLookup(await readAliases(workspaceRoot));
|
|
1034
|
+
const lines = (await listAllTasks(workspaceRoot)).filter((stored) => taskMatchesQuery(stored.task, query)).map((stored) => {
|
|
1035
|
+
const alias = aliasLookup.get(stored.task.id);
|
|
1036
|
+
let line = formatTaskListLine(stored.task, alias);
|
|
1037
|
+
if (stored.task.metadata.pr?.fetched?.state) line += ` PR:${stored.task.metadata.pr.fetched.state}`;
|
|
1038
|
+
return line;
|
|
1039
|
+
});
|
|
1040
|
+
helpers.printLines(lines);
|
|
1041
|
+
}));
|
|
605
1042
|
};
|
|
606
1043
|
|
|
607
1044
|
//#endregion
|
|
608
|
-
//#region
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
complete: "completed",
|
|
617
|
-
cancel: "cancelled"
|
|
618
|
-
},
|
|
619
|
-
paused: {
|
|
620
|
-
start: "active",
|
|
621
|
-
complete: "completed",
|
|
622
|
-
cancel: "cancelled"
|
|
623
|
-
},
|
|
624
|
-
completed: {},
|
|
625
|
-
cancelled: {}
|
|
626
|
-
};
|
|
627
|
-
const getNextStatus = (current, action) => {
|
|
628
|
-
const next = transitionTable[current][action];
|
|
629
|
-
if (!next) throw new Error(`Invalid transition: ${current} -> ${action}`);
|
|
630
|
-
return next;
|
|
1045
|
+
//#region cli/commands/show.ts
|
|
1046
|
+
const registerShowCommand = (program, helpers) => {
|
|
1047
|
+
program.command("show").description("show a task").argument("<id>", "task identifier").action(helpers.handleAction(async (identifier) => {
|
|
1048
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1049
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1050
|
+
const alias = buildAliasLookup(await readAliases(workspaceRoot)).get(stored.task.id);
|
|
1051
|
+
process.stdout.write(`${formatTaskDetails(stored.task, alias)}\n`);
|
|
1052
|
+
}));
|
|
631
1053
|
};
|
|
632
1054
|
|
|
633
1055
|
//#endregion
|
|
634
|
-
//#region
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
ts: nowIso(),
|
|
644
|
-
msg: message,
|
|
645
|
-
status
|
|
646
|
-
};
|
|
647
|
-
return {
|
|
648
|
-
...task,
|
|
649
|
-
logs: [...task.logs, log],
|
|
650
|
-
updated_at: nowIso()
|
|
651
|
-
};
|
|
1056
|
+
//#region cli/commands/today.ts
|
|
1057
|
+
const resolveDate = (helpers, command, dateInput) => {
|
|
1058
|
+
try {
|
|
1059
|
+
return todayDate(dateInput);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1062
|
+
helpers.failWithHelp(command, message);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
652
1065
|
};
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1066
|
+
const collectAssignments = (tasks, date) => {
|
|
1067
|
+
const entries = [];
|
|
1068
|
+
for (const stored of tasks) {
|
|
1069
|
+
const latest = getLatestDayAssignment(stored.task, date);
|
|
1070
|
+
if (!latest) continue;
|
|
1071
|
+
if (latest.event.action === "assign" || latest.event.action === "reorder") entries.push({
|
|
1072
|
+
stored,
|
|
1073
|
+
lastEvent: latest.event,
|
|
1074
|
+
lastIndex: latest.index
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return entries;
|
|
1078
|
+
};
|
|
1079
|
+
const sortAssignments = (entries) => {
|
|
1080
|
+
return [...entries].sort((a, b) => {
|
|
1081
|
+
const orderA = a.lastEvent.order;
|
|
1082
|
+
const orderB = b.lastEvent.order;
|
|
1083
|
+
if (orderA != null && orderB != null) return orderA - orderB;
|
|
1084
|
+
if (orderA != null) return -1;
|
|
1085
|
+
if (orderB != null) return 1;
|
|
1086
|
+
return a.lastIndex - b.lastIndex;
|
|
1087
|
+
});
|
|
670
1088
|
};
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1089
|
+
const buildOrderedAssignments = (tasks, date) => {
|
|
1090
|
+
return sortAssignments(collectAssignments(tasks, date));
|
|
1091
|
+
};
|
|
1092
|
+
const resolveMoveTarget = (helpers, command, position, currentIndex, total) => {
|
|
1093
|
+
if (position === "up") return Math.max(0, currentIndex - 1);
|
|
1094
|
+
if (position === "down") return Math.min(total - 1, currentIndex + 1);
|
|
1095
|
+
if (position === "top") return 0;
|
|
1096
|
+
if (position === "bottom") return total - 1;
|
|
1097
|
+
const parsed = Number(position);
|
|
1098
|
+
if (!Number.isInteger(parsed)) {
|
|
1099
|
+
helpers.failWithHelp(command, `Invalid position: ${position}`);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (parsed < 1 || parsed > total) {
|
|
1103
|
+
helpers.failWithHelp(command, `Position out of range: ${position}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
return parsed - 1;
|
|
675
1107
|
};
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1108
|
+
const registerTodayCommands = (program, helpers) => {
|
|
1109
|
+
program.command("today").description("assign a task for today").argument("<id>", "task identifier").option("--date <date>", "YYYY-MM-DD").option("-m, --message <message>", "assignment message").action(helpers.handleAction(async (identifier, options, command) => {
|
|
1110
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1111
|
+
const date = resolveDate(helpers, command, options.date);
|
|
1112
|
+
if (!date) return;
|
|
1113
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1114
|
+
const tasks = await listAllTasks(workspaceRoot);
|
|
1115
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1116
|
+
if (isAssignedOnDate(stored.task, date)) {
|
|
1117
|
+
process.stdout.write(`Already assigned ${stored.task.ref} to ${date}\n`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const order = buildOrderedAssignments(tasks, date).length + 1;
|
|
1121
|
+
const updated = appendDayAssignment(stored.task, date, "assign", options.message, order);
|
|
1122
|
+
await saveTask(workspaceRoot, stored.status, updated);
|
|
1123
|
+
process.stdout.write(`Assigned ${updated.ref} to ${date}\n`);
|
|
1124
|
+
});
|
|
1125
|
+
})).command("move").description("reorder tasks assigned to a day").argument("<id>", "task identifier").argument("<position>", "up, down, top, bottom, or 1-based position").option("--date <date>", "YYYY-MM-DD").action(helpers.handleAction(async (identifier, position, options, command) => {
|
|
1126
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1127
|
+
const date = resolveDate(helpers, command, options.date);
|
|
1128
|
+
if (!date) return;
|
|
1129
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1130
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1131
|
+
const ordered = buildOrderedAssignments(await listAllTasks(workspaceRoot), date);
|
|
1132
|
+
const currentIndex = ordered.findIndex((entry) => entry.stored.task.id === stored.task.id);
|
|
1133
|
+
if (currentIndex === -1) {
|
|
1134
|
+
process.stdout.write(`Task ${stored.task.ref} is not assigned to ${date}\n`);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const targetIndex = resolveMoveTarget(helpers, command, position, currentIndex, ordered.length);
|
|
1138
|
+
if (targetIndex === void 0) return;
|
|
1139
|
+
if (targetIndex === currentIndex) {
|
|
1140
|
+
process.stdout.write(`Task ${stored.task.ref} is already in position ${currentIndex + 1}\n`);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const reordered = [...ordered];
|
|
1144
|
+
const [moved] = reordered.splice(currentIndex, 1);
|
|
1145
|
+
if (!moved) {
|
|
1146
|
+
process.stdout.write(`Task ${stored.task.ref} is not assigned to ${date}\n`);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
reordered.splice(targetIndex, 0, moved);
|
|
1150
|
+
for (const [index, entry] of reordered.entries()) {
|
|
1151
|
+
const nextOrder = index + 1;
|
|
1152
|
+
if (entry.lastEvent.order === nextOrder) continue;
|
|
1153
|
+
const updated = appendDayAssignment(entry.stored.task, date, "reorder", void 0, nextOrder);
|
|
1154
|
+
await saveTask(workspaceRoot, entry.stored.status, updated);
|
|
1155
|
+
}
|
|
1156
|
+
process.stdout.write(`Moved ${stored.task.ref} to position ${targetIndex + 1}\n`);
|
|
1157
|
+
});
|
|
1158
|
+
}));
|
|
1159
|
+
program.command("untoday").description("unassign a task from today").argument("<id>", "task identifier").option("--date <date>", "YYYY-MM-DD").action(helpers.handleAction(async (identifier, options, command) => {
|
|
1160
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1161
|
+
const date = resolveDate(helpers, command, options.date);
|
|
1162
|
+
if (!date) return;
|
|
1163
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1164
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1165
|
+
if (!isAssignedOnDate(stored.task, date)) {
|
|
1166
|
+
process.stdout.write(`Already unassigned ${stored.task.ref} from ${date}\n`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const updated = appendDayAssignment(stored.task, date, "unassign");
|
|
1170
|
+
await saveTask(workspaceRoot, stored.status, updated);
|
|
1171
|
+
process.stdout.write(`Unassigned ${updated.ref} from ${date}\n`);
|
|
1172
|
+
});
|
|
1173
|
+
}));
|
|
688
1174
|
};
|
|
689
1175
|
|
|
690
1176
|
//#endregion
|
|
691
|
-
//#region
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1177
|
+
//#region cli/commands/transitions.ts
|
|
1178
|
+
const transitionNames = [
|
|
1179
|
+
"start",
|
|
1180
|
+
"pause",
|
|
1181
|
+
"complete",
|
|
1182
|
+
"cancel"
|
|
1183
|
+
];
|
|
1184
|
+
const registerTransitionCommand = (program, helpers, name) => {
|
|
1185
|
+
program.command(name).description(`${name} a task`).argument("<id>", "task identifier").option("-m, --message <message>", "custom log message").action(helpers.handleAction(async (identifier, options) => {
|
|
1186
|
+
const workspaceRoot = await helpers.resolveWorkspaceFor(true);
|
|
1187
|
+
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1188
|
+
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1189
|
+
const { task: updated, nextStatus } = applyTransition(stored.task, name, options.message);
|
|
1190
|
+
await saveTask(workspaceRoot, stored.status, updated);
|
|
1191
|
+
await moveTaskFile(workspaceRoot, updated.id, stored.status, nextStatus);
|
|
1192
|
+
process.stdout.write(`Updated ${updated.ref} -> ${nextStatus}\n`);
|
|
1193
|
+
});
|
|
1194
|
+
}));
|
|
703
1195
|
};
|
|
704
|
-
const
|
|
705
|
-
const
|
|
706
|
-
if (!found) throw new Error(`Task not found: ${identifier}`);
|
|
707
|
-
return {
|
|
708
|
-
task: await loadTaskFromPath(found.filePath),
|
|
709
|
-
status: found.status,
|
|
710
|
-
filePath: found.filePath
|
|
711
|
-
};
|
|
1196
|
+
const registerTransitionCommands = (program, helpers) => {
|
|
1197
|
+
for (const name of transitionNames) registerTransitionCommand(program, helpers, name);
|
|
712
1198
|
};
|
|
713
1199
|
|
|
714
1200
|
//#endregion
|
|
715
|
-
//#region
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1201
|
+
//#region cli/commands/where.ts
|
|
1202
|
+
const registerWhereCommand = (program, helpers) => {
|
|
1203
|
+
program.command("where").description("show resolved workspace").action(helpers.handleAction(async () => {
|
|
1204
|
+
const opts = program.opts();
|
|
1205
|
+
const workspace = await resolveWorkspace({
|
|
1206
|
+
store: opts.store,
|
|
1207
|
+
global: opts.global,
|
|
1208
|
+
repo: opts.repo
|
|
1209
|
+
});
|
|
1210
|
+
process.stdout.write(`${workspace.root} (${workspace.kind})\n`);
|
|
1211
|
+
}));
|
|
724
1212
|
};
|
|
725
1213
|
|
|
726
1214
|
//#endregion
|
|
727
|
-
//#region
|
|
728
|
-
const
|
|
1215
|
+
//#region cli/helpers.ts
|
|
1216
|
+
const isPackageJson = (value) => {
|
|
1217
|
+
return value !== null && typeof value === "object" && "version" in value;
|
|
1218
|
+
};
|
|
729
1219
|
const handleAction = (fn) => async (...args) => {
|
|
730
1220
|
try {
|
|
731
1221
|
await fn(...args);
|
|
@@ -735,16 +1225,6 @@ const handleAction = (fn) => async (...args) => {
|
|
|
735
1225
|
process.exitCode = 1;
|
|
736
1226
|
}
|
|
737
1227
|
};
|
|
738
|
-
const resolveWorkspaceFor = async (ensure) => {
|
|
739
|
-
const opts = program.opts();
|
|
740
|
-
const workspace = await resolveWorkspace({
|
|
741
|
-
store: opts.store,
|
|
742
|
-
global: opts.global,
|
|
743
|
-
repo: opts.repo
|
|
744
|
-
});
|
|
745
|
-
if (ensure) await ensureWorkspaceLayout(workspace.root);
|
|
746
|
-
return workspace.root;
|
|
747
|
-
};
|
|
748
1228
|
const printLines = (lines) => {
|
|
749
1229
|
if (lines.length === 0) {
|
|
750
1230
|
process.stdout.write("No tasks.\n");
|
|
@@ -757,20 +1237,23 @@ const failWithHelp = (command, message) => {
|
|
|
757
1237
|
command.outputHelp();
|
|
758
1238
|
process.exitCode = 1;
|
|
759
1239
|
};
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1240
|
+
const createCliHelpers = (program) => {
|
|
1241
|
+
const resolveWorkspaceFor = async (ensure) => {
|
|
1242
|
+
const opts = program.opts();
|
|
1243
|
+
const workspace = await resolveWorkspace({
|
|
1244
|
+
store: opts.store,
|
|
1245
|
+
global: opts.global,
|
|
1246
|
+
repo: opts.repo
|
|
1247
|
+
});
|
|
1248
|
+
if (ensure) await ensureWorkspaceLayout(workspace.root);
|
|
1249
|
+
return workspace.root;
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
handleAction,
|
|
1253
|
+
resolveWorkspaceFor,
|
|
1254
|
+
printLines,
|
|
1255
|
+
failWithHelp
|
|
1256
|
+
};
|
|
774
1257
|
};
|
|
775
1258
|
const resolvePackageVersion = async () => {
|
|
776
1259
|
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -789,271 +1272,27 @@ const resolvePackageVersion = async () => {
|
|
|
789
1272
|
current = parent;
|
|
790
1273
|
}
|
|
791
1274
|
};
|
|
1275
|
+
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region cli/index.ts
|
|
1278
|
+
const program = new Command();
|
|
1279
|
+
const helpers = createCliHelpers(program);
|
|
792
1280
|
const packageVersion = await resolvePackageVersion();
|
|
793
1281
|
program.name("il").description("Terminal task manager").option("--store <path>", "explicit workspace path").option("--global", "use global workspace").option("--repo", "use repo workspace");
|
|
794
1282
|
if (packageVersion) program.version(packageVersion);
|
|
795
1283
|
program.showHelpAfterError();
|
|
796
|
-
program
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
program
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
global: opts.global,
|
|
809
|
-
repo: opts.repo
|
|
810
|
-
});
|
|
811
|
-
process.stdout.write(`${workspace.root} (${workspace.kind})\n`);
|
|
812
|
-
}));
|
|
813
|
-
program.command("add").description("add a task").argument("[title]", "task title").option("--type <type>", "regular|pr_review", "regular").option("--url <url>", "attach URL").option("--pr <url>", "attach PR URL").action(handleAction(async (title, options, command) => {
|
|
814
|
-
const taskType = options.type;
|
|
815
|
-
if (!taskTypes.includes(taskType)) {
|
|
816
|
-
failWithHelp(command, `Invalid type: ${taskType}`);
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
if (taskType === "pr_review" && !options.pr) {
|
|
820
|
-
failWithHelp(command, "PR review tasks require --pr");
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
824
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
825
|
-
let prAttachment;
|
|
826
|
-
let prTitle;
|
|
827
|
-
if (options.pr) {
|
|
828
|
-
const parsed = parseGitHubPrUrl(options.pr);
|
|
829
|
-
if (!parsed) {
|
|
830
|
-
failWithHelp(command, "Invalid PR URL");
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(workspaceRoot));
|
|
834
|
-
prAttachment = buildPrAttachment(parsed, fetched ?? void 0);
|
|
835
|
-
prTitle = fetched?.title ?? `PR #${parsed.number} ${parsed.repo.owner}/${parsed.repo.name}`;
|
|
836
|
-
}
|
|
837
|
-
const isExplicitTitle = Boolean(title);
|
|
838
|
-
let finalTitle = title ?? prTitle;
|
|
839
|
-
if (!finalTitle) {
|
|
840
|
-
failWithHelp(command, "Title is required when no PR URL is provided");
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (taskType === "pr_review" && !isExplicitTitle) finalTitle = `Review: ${finalTitle}`;
|
|
844
|
-
const metadata = {
|
|
845
|
-
url: options.url,
|
|
846
|
-
pr: prAttachment
|
|
847
|
-
};
|
|
848
|
-
const task = buildTask({
|
|
849
|
-
type: taskType,
|
|
850
|
-
title: finalTitle,
|
|
851
|
-
metadata
|
|
852
|
-
});
|
|
853
|
-
taskSchema.parse(task);
|
|
854
|
-
const created = await createTaskInStore(workspaceRoot, task);
|
|
855
|
-
process.stdout.write(`Created ${created.alias} ${created.task.ref} ${created.task.title}\n`);
|
|
856
|
-
});
|
|
857
|
-
}));
|
|
858
|
-
program.command("list").description("list tasks").argument("[status]", "task status or date scope").action(handleAction(async (status, command) => {
|
|
859
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
860
|
-
const aliasLookup = buildAliasLookup(await readAliases(workspaceRoot));
|
|
861
|
-
const lines = [];
|
|
862
|
-
if (status && !isTaskStatus(status) && !isListScope(status)) {
|
|
863
|
-
failWithHelp(command, `Invalid status or scope: ${status}`);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (status && isListScope(status)) {
|
|
867
|
-
const dates = listDatesForScope(status);
|
|
868
|
-
const tasks = await listAllTasks(workspaceRoot);
|
|
869
|
-
for (const entry of tasks) {
|
|
870
|
-
if (!dates.some((date) => isAssignedOnDate(entry.task, date))) continue;
|
|
871
|
-
const alias = aliasLookup.get(entry.task.id);
|
|
872
|
-
let line = formatTaskListLine(entry.task, alias);
|
|
873
|
-
if (entry.task.metadata.pr?.fetched?.state) line += ` PR:${entry.task.metadata.pr.fetched.state}`;
|
|
874
|
-
lines.push(line);
|
|
875
|
-
}
|
|
876
|
-
printLines(lines);
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
const statuses = status ? [status] : [...taskStatuses];
|
|
880
|
-
for (const currentStatus of statuses) {
|
|
881
|
-
const stored = await listTasksByStatus(workspaceRoot, currentStatus);
|
|
882
|
-
for (const entry of stored) {
|
|
883
|
-
const alias = aliasLookup.get(entry.task.id);
|
|
884
|
-
let line = formatTaskListLine(entry.task, alias);
|
|
885
|
-
if (entry.task.metadata.pr?.fetched?.state) line += ` PR:${entry.task.metadata.pr.fetched.state}`;
|
|
886
|
-
lines.push(line);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
printLines(lines);
|
|
890
|
-
}));
|
|
891
|
-
program.command("show").description("show a task").argument("<id>", "task identifier").action(handleAction(async (identifier, command) => {
|
|
892
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
893
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
894
|
-
const alias = buildAliasLookup(await readAliases(workspaceRoot)).get(stored.task.id);
|
|
895
|
-
process.stdout.write(`${formatTaskDetails(stored.task, alias)}\n`);
|
|
896
|
-
}));
|
|
897
|
-
const addTransitionCommand = (name) => {
|
|
898
|
-
program.command(name).description(`${name} a task`).argument("<id>", "task identifier").option("-m, --message <message>", "custom log message").action(handleAction(async (identifier, options, command) => {
|
|
899
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
900
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
901
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
902
|
-
const { task: updated, nextStatus } = applyTransition(stored.task, name, options.message);
|
|
903
|
-
await saveTask(workspaceRoot, stored.status, updated);
|
|
904
|
-
await moveTaskFile(workspaceRoot, updated.id, stored.status, nextStatus);
|
|
905
|
-
process.stdout.write(`Updated ${updated.ref} -> ${nextStatus}\n`);
|
|
906
|
-
});
|
|
907
|
-
}));
|
|
908
|
-
};
|
|
909
|
-
addTransitionCommand("start");
|
|
910
|
-
addTransitionCommand("pause");
|
|
911
|
-
addTransitionCommand("complete");
|
|
912
|
-
addTransitionCommand("cancel");
|
|
913
|
-
program.command("log").description("append a log entry").argument("<id>", "task identifier").argument("<message>", "log message").option("--status <status>", "include status in log entry").action(handleAction(async (identifier, message, options, command) => {
|
|
914
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
915
|
-
const status = options.status;
|
|
916
|
-
if (status && !taskStatuses.includes(status)) {
|
|
917
|
-
failWithHelp(command, `Invalid status: ${status}`);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
921
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
922
|
-
const updated = appendLog(stored.task, message, status);
|
|
923
|
-
await saveTask(workspaceRoot, stored.status, updated);
|
|
924
|
-
process.stdout.write(`Logged on ${updated.ref}\n`);
|
|
925
|
-
});
|
|
926
|
-
}));
|
|
927
|
-
program.command("edit").description("edit a task field").argument("<id>", "task identifier").argument("<path>", "dotted path").argument("<value>", "new value").action(handleAction(async (identifier, dottedPath, value, command) => {
|
|
928
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
929
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
930
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
931
|
-
const updated = applyTaskEdit(stored.task, dottedPath, value);
|
|
932
|
-
await saveTask(workspaceRoot, stored.status, updated);
|
|
933
|
-
process.stdout.write(`Updated ${updated.ref}\n`);
|
|
934
|
-
});
|
|
935
|
-
}));
|
|
936
|
-
program.command("search").description("search tasks").argument("<query>", "search query").action(handleAction(async (query, command) => {
|
|
937
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
938
|
-
const aliasLookup = buildAliasLookup(await readAliases(workspaceRoot));
|
|
939
|
-
printLines((await listAllTasks(workspaceRoot)).filter((stored) => taskMatchesQuery(stored.task, query)).map((stored) => {
|
|
940
|
-
const alias = aliasLookup.get(stored.task.id);
|
|
941
|
-
let line = formatTaskListLine(stored.task, alias);
|
|
942
|
-
if (stored.task.metadata.pr?.fetched?.state) line += ` PR:${stored.task.metadata.pr.fetched.state}`;
|
|
943
|
-
return line;
|
|
944
|
-
}));
|
|
945
|
-
}));
|
|
946
|
-
program.command("today").description("assign a task for today").argument("<id>", "task identifier").option("--date <date>", "YYYY-MM-DD").option("-m, --message <message>", "assignment message").action(handleAction(async (identifier, options, command) => {
|
|
947
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
948
|
-
let date;
|
|
949
|
-
try {
|
|
950
|
-
date = todayDate(options.date);
|
|
951
|
-
} catch (error) {
|
|
952
|
-
failWithHelp(command, error instanceof Error ? error.message : String(error));
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
956
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
957
|
-
if (isAssignedOnDate(stored.task, date)) {
|
|
958
|
-
process.stdout.write(`Already assigned ${stored.task.ref} to ${date}\n`);
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
const updated = appendDayAssignment(stored.task, date, "assign", options.message);
|
|
962
|
-
await saveTask(workspaceRoot, stored.status, updated);
|
|
963
|
-
process.stdout.write(`Assigned ${updated.ref} to ${date}\n`);
|
|
964
|
-
});
|
|
965
|
-
}));
|
|
966
|
-
program.command("untoday").description("unassign a task from today").argument("<id>", "task identifier").option("--date <date>", "YYYY-MM-DD").action(handleAction(async (identifier, options, command) => {
|
|
967
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
968
|
-
let date;
|
|
969
|
-
try {
|
|
970
|
-
date = todayDate(options.date);
|
|
971
|
-
} catch (error) {
|
|
972
|
-
failWithHelp(command, error instanceof Error ? error.message : String(error));
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
976
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
977
|
-
if (!isAssignedOnDate(stored.task, date)) {
|
|
978
|
-
process.stdout.write(`Already unassigned ${stored.task.ref} from ${date}\n`);
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
const updated = appendDayAssignment(stored.task, date, "unassign");
|
|
982
|
-
await saveTask(workspaceRoot, stored.status, updated);
|
|
983
|
-
process.stdout.write(`Unassigned ${updated.ref} from ${date}\n`);
|
|
984
|
-
});
|
|
985
|
-
}));
|
|
986
|
-
program.command("attach-pr").description("attach PR metadata to a task").argument("<id>", "task identifier").argument("<url>", "PR URL").option("-m, --message <message>", "log message").action(handleAction(async (identifier, url, options, command) => {
|
|
987
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
988
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
989
|
-
const parsed = parseGitHubPrUrl(url);
|
|
990
|
-
if (!parsed) {
|
|
991
|
-
failWithHelp(command, "Invalid PR URL");
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(workspaceRoot));
|
|
995
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
996
|
-
const next = appendLog({
|
|
997
|
-
...stored.task,
|
|
998
|
-
metadata: {
|
|
999
|
-
...stored.task.metadata,
|
|
1000
|
-
pr: buildPrAttachment(parsed, fetched ?? stored.task.metadata.pr?.fetched)
|
|
1001
|
-
},
|
|
1002
|
-
updated_at: nowIso()
|
|
1003
|
-
}, options.message ?? `Attached PR ${url}`);
|
|
1004
|
-
await saveTask(workspaceRoot, stored.status, next);
|
|
1005
|
-
process.stdout.write(`Attached PR to ${next.ref}\n`);
|
|
1006
|
-
});
|
|
1007
|
-
}));
|
|
1008
|
-
program.command("refresh").description("refresh PR metadata").argument("<id>", "task identifier").option("-m, --message <message>", "log message").action(handleAction(async (identifier, options, command) => {
|
|
1009
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
1010
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1011
|
-
const stored = await resolveTask(workspaceRoot, identifier);
|
|
1012
|
-
const prUrl = stored.task.metadata.pr?.url;
|
|
1013
|
-
if (!prUrl) {
|
|
1014
|
-
process.stdout.write("No PR attached.\n");
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
const parsed = parseGitHubPrUrl(prUrl);
|
|
1018
|
-
if (!parsed) throw new Error("Invalid PR URL");
|
|
1019
|
-
const fetched = await fetchGitHubPr(parsed, await resolveGithubToken(workspaceRoot));
|
|
1020
|
-
if (!fetched) throw new Error("No GitHub token configured");
|
|
1021
|
-
const baseUpdated = {
|
|
1022
|
-
...stored.task,
|
|
1023
|
-
metadata: {
|
|
1024
|
-
...stored.task.metadata,
|
|
1025
|
-
pr: buildPrAttachment(parsed, fetched)
|
|
1026
|
-
},
|
|
1027
|
-
updated_at: nowIso()
|
|
1028
|
-
};
|
|
1029
|
-
const previousState = stored.task.metadata.pr?.fetched?.state;
|
|
1030
|
-
const nextState = fetched.state;
|
|
1031
|
-
const isTerminal = stored.task.status === "completed" || stored.task.status === "cancelled";
|
|
1032
|
-
let finalTask = baseUpdated;
|
|
1033
|
-
let nextStatus = stored.status;
|
|
1034
|
-
if (!isTerminal && previousState !== nextState) {
|
|
1035
|
-
if (nextState === "merged") {
|
|
1036
|
-
const result = applyTransition(baseUpdated, "complete", options.message ?? "PR merged");
|
|
1037
|
-
finalTask = result.task;
|
|
1038
|
-
nextStatus = result.nextStatus;
|
|
1039
|
-
} else if (nextState === "closed") {
|
|
1040
|
-
const result = applyTransition(baseUpdated, "cancel", options.message ?? "PR closed without merge");
|
|
1041
|
-
finalTask = result.task;
|
|
1042
|
-
nextStatus = result.nextStatus;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
await saveTask(workspaceRoot, stored.status, finalTask);
|
|
1046
|
-
if (nextStatus !== stored.status) await moveTaskFile(workspaceRoot, finalTask.id, stored.status, nextStatus);
|
|
1047
|
-
process.stdout.write(`Refreshed ${finalTask.ref}\n`);
|
|
1048
|
-
});
|
|
1049
|
-
}));
|
|
1050
|
-
program.command("alias").description("alias helpers").command("reconcile").description("reconcile alias mapping").action(handleAction(async (command) => {
|
|
1051
|
-
const workspaceRoot = await resolveWorkspaceFor(true);
|
|
1052
|
-
await withWorkspaceLock(workspaceRoot, async () => {
|
|
1053
|
-
await reconcileAliases(workspaceRoot);
|
|
1054
|
-
process.stdout.write("Aliases reconciled.\n");
|
|
1055
|
-
});
|
|
1056
|
-
}));
|
|
1284
|
+
registerInitCommand(program, helpers);
|
|
1285
|
+
registerWhereCommand(program, helpers);
|
|
1286
|
+
registerAddCommand(program, helpers);
|
|
1287
|
+
registerListCommand(program, helpers);
|
|
1288
|
+
registerShowCommand(program, helpers);
|
|
1289
|
+
registerTransitionCommands(program, helpers);
|
|
1290
|
+
registerLogCommand(program, helpers);
|
|
1291
|
+
registerEditCommand(program, helpers);
|
|
1292
|
+
registerSearchCommand(program, helpers);
|
|
1293
|
+
registerTodayCommands(program, helpers);
|
|
1294
|
+
registerPrCommands(program, helpers);
|
|
1295
|
+
registerAliasCommand(program, helpers);
|
|
1057
1296
|
program.parseAsync(process.argv);
|
|
1058
1297
|
|
|
1059
1298
|
//#endregion
|