@eunjae/il 0.0.1 → 1.0.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,396 +0,0 @@
1
- #!/usr/bin/env node
2
- import path from 'node:path';
3
- import { Command } from 'commander';
4
- import { readAliases } from '../lib/aliasRepo';
5
- import { reconcileAliases } from '../lib/aliasReconcile';
6
- import { resolveGithubToken } from '../lib/config';
7
- import { APP_DIR } from '../lib/constants';
8
- import { applyTaskEdit } from '../lib/edit';
9
- import { formatTaskDetails, formatTaskListLine, buildAliasLookup } from '../lib/format';
10
- import { ensureLockIgnored } from '../lib/gitignore';
11
- import { withWorkspaceLock } from '../lib/lock';
12
- import { buildPrAttachment, fetchGitHubPr, parseGitHubPrUrl } from '../lib/pr';
13
- import { taskSchema } from '../lib/schema';
14
- import { taskMatchesQuery } from '../lib/search';
15
- import { buildTask } from '../lib/taskFactory';
16
- import { appendDayAssignment, appendLog, applyTransition, isAssignedOnDate } from '../lib/taskOperations';
17
- import { createTaskInStore } from '../lib/taskStore';
18
- import { listAllTasks, listTasksByStatus, moveTaskFile, saveTask } from '../lib/taskRepo';
19
- import { resolveTask } from '../lib/taskResolver';
20
- import { nowIso, todayDate } from '../lib/time';
21
- import { taskStatuses, taskTypes } from '../lib/types';
22
- import { ensureWorkspaceLayout, findGitRoot, resolveWorkspace } from '../lib/workspace';
23
- const program = new Command();
24
- const handleAction = (fn) => async (...args) => {
25
- try {
26
- await fn(...args);
27
- }
28
- catch (error) {
29
- const message = error instanceof Error ? error.message : String(error);
30
- process.stderr.write(`${message}\n`);
31
- process.exitCode = 1;
32
- }
33
- };
34
- const resolveWorkspaceFor = async (ensure) => {
35
- const opts = program.opts();
36
- const workspace = await resolveWorkspace({
37
- store: opts.store,
38
- global: opts.global,
39
- repo: opts.repo
40
- });
41
- if (ensure) {
42
- await ensureWorkspaceLayout(workspace.root);
43
- }
44
- return workspace.root;
45
- };
46
- const printLines = (lines) => {
47
- if (lines.length === 0) {
48
- process.stdout.write('No tasks.\n');
49
- return;
50
- }
51
- process.stdout.write(`${lines.join('\n')}\n`);
52
- };
53
- const isTaskStatus = (value) => {
54
- return taskStatuses.includes(value);
55
- };
56
- program
57
- .name('il')
58
- .description('Terminal task manager')
59
- .option('--store <path>', 'explicit workspace path')
60
- .option('--global', 'use global workspace')
61
- .option('--repo', 'use repo workspace');
62
- program
63
- .command('init')
64
- .description('initialize repo workspace')
65
- .action(handleAction(async () => {
66
- const repoRoot = await findGitRoot(process.cwd());
67
- if (!repoRoot) {
68
- throw new Error('Not inside a git repository');
69
- }
70
- const workspaceRoot = path.join(repoRoot, APP_DIR);
71
- await ensureWorkspaceLayout(workspaceRoot);
72
- await ensureLockIgnored(repoRoot);
73
- process.stdout.write(`Initialized workspace at ${workspaceRoot}\n`);
74
- }));
75
- program
76
- .command('where')
77
- .description('show resolved workspace')
78
- .action(handleAction(async () => {
79
- const opts = program.opts();
80
- const workspace = await resolveWorkspace({
81
- store: opts.store,
82
- global: opts.global,
83
- repo: opts.repo
84
- });
85
- process.stdout.write(`${workspace.root} (${workspace.kind})\n`);
86
- }));
87
- program
88
- .command('add')
89
- .description('add a task')
90
- .argument('[title]', 'task title')
91
- .option('--type <type>', 'regular|pr_review', 'regular')
92
- .option('--url <url>', 'attach URL')
93
- .option('--pr <url>', 'attach PR URL')
94
- .action(handleAction(async (title, options, command) => {
95
- const taskType = options.type;
96
- if (!taskTypes.includes(taskType)) {
97
- throw new Error(`Invalid type: ${taskType}`);
98
- }
99
- if (taskType === 'pr_review' && !options.pr) {
100
- throw new Error('PR review tasks require --pr');
101
- }
102
- const workspaceRoot = await resolveWorkspaceFor(true);
103
- await withWorkspaceLock(workspaceRoot, async () => {
104
- let prAttachment;
105
- let prTitle;
106
- if (options.pr) {
107
- const parsed = parseGitHubPrUrl(options.pr);
108
- if (!parsed) {
109
- throw new Error('Invalid PR URL');
110
- }
111
- const token = await resolveGithubToken(workspaceRoot);
112
- const fetched = await fetchGitHubPr(parsed, token);
113
- prAttachment = buildPrAttachment(parsed, fetched ?? undefined);
114
- prTitle = fetched?.title ?? `PR #${parsed.number} ${parsed.repo.owner}/${parsed.repo.name}`;
115
- }
116
- const isExplicitTitle = Boolean(title);
117
- let finalTitle = title ?? prTitle;
118
- if (!finalTitle) {
119
- throw new Error('Title is required when no PR URL is provided');
120
- }
121
- if (taskType === 'pr_review' && !isExplicitTitle) {
122
- finalTitle = `Review: ${finalTitle}`;
123
- }
124
- const metadata = {
125
- url: options.url,
126
- pr: prAttachment
127
- };
128
- const task = buildTask({
129
- type: taskType,
130
- title: finalTitle,
131
- metadata
132
- });
133
- taskSchema.parse(task);
134
- const created = await createTaskInStore(workspaceRoot, task);
135
- process.stdout.write(`Created ${created.alias} ${created.task.ref} ${created.task.title}\n`);
136
- });
137
- }));
138
- program
139
- .command('list')
140
- .description('list tasks')
141
- .argument('[status]', 'task status')
142
- .action(handleAction(async (status, command) => {
143
- const workspaceRoot = await resolveWorkspaceFor(true);
144
- const aliases = await readAliases(workspaceRoot);
145
- const aliasLookup = buildAliasLookup(aliases);
146
- if (status && !isTaskStatus(status)) {
147
- throw new Error(`Invalid status: ${status}`);
148
- }
149
- const statuses = status ? [status] : [...taskStatuses];
150
- const lines = [];
151
- for (const currentStatus of statuses) {
152
- const stored = await listTasksByStatus(workspaceRoot, currentStatus);
153
- for (const entry of stored) {
154
- const alias = aliasLookup.get(entry.task.id);
155
- let line = formatTaskListLine(entry.task, alias);
156
- if (entry.task.metadata.pr?.fetched?.state) {
157
- line += ` PR:${entry.task.metadata.pr.fetched.state}`;
158
- }
159
- lines.push(line);
160
- }
161
- }
162
- printLines(lines);
163
- }));
164
- program
165
- .command('show')
166
- .description('show a task')
167
- .argument('<id>', 'task identifier')
168
- .action(handleAction(async (identifier, command) => {
169
- const workspaceRoot = await resolveWorkspaceFor(true);
170
- const stored = await resolveTask(workspaceRoot, identifier);
171
- const aliases = await readAliases(workspaceRoot);
172
- const aliasLookup = buildAliasLookup(aliases);
173
- const alias = aliasLookup.get(stored.task.id);
174
- process.stdout.write(`${formatTaskDetails(stored.task, alias)}\n`);
175
- }));
176
- const addTransitionCommand = (name) => {
177
- program
178
- .command(name)
179
- .description(`${name} a task`)
180
- .argument('<id>', 'task identifier')
181
- .option('-m, --message <message>', 'custom log message')
182
- .action(handleAction(async (identifier, options, command) => {
183
- const workspaceRoot = await resolveWorkspaceFor(true);
184
- await withWorkspaceLock(workspaceRoot, async () => {
185
- const stored = await resolveTask(workspaceRoot, identifier);
186
- const { task: updated, nextStatus } = applyTransition(stored.task, name, options.message);
187
- await saveTask(workspaceRoot, stored.status, updated);
188
- await moveTaskFile(workspaceRoot, updated.id, stored.status, nextStatus);
189
- process.stdout.write(`Updated ${updated.ref} -> ${nextStatus}\n`);
190
- });
191
- }));
192
- };
193
- addTransitionCommand('start');
194
- addTransitionCommand('pause');
195
- addTransitionCommand('complete');
196
- addTransitionCommand('cancel');
197
- program
198
- .command('log')
199
- .description('append a log entry')
200
- .argument('<id>', 'task identifier')
201
- .argument('<message>', 'log message')
202
- .option('--status <status>', 'include status in log entry')
203
- .action(handleAction(async (identifier, message, options, command) => {
204
- const workspaceRoot = await resolveWorkspaceFor(true);
205
- const status = options.status;
206
- if (status && !taskStatuses.includes(status)) {
207
- throw new Error(`Invalid status: ${status}`);
208
- }
209
- await withWorkspaceLock(workspaceRoot, async () => {
210
- const stored = await resolveTask(workspaceRoot, identifier);
211
- const updated = appendLog(stored.task, message, status);
212
- await saveTask(workspaceRoot, stored.status, updated);
213
- process.stdout.write(`Logged on ${updated.ref}\n`);
214
- });
215
- }));
216
- program
217
- .command('edit')
218
- .description('edit a task field')
219
- .argument('<id>', 'task identifier')
220
- .argument('<path>', 'dotted path')
221
- .argument('<value>', 'new value')
222
- .action(handleAction(async (identifier, dottedPath, value, command) => {
223
- const workspaceRoot = await resolveWorkspaceFor(true);
224
- await withWorkspaceLock(workspaceRoot, async () => {
225
- const stored = await resolveTask(workspaceRoot, identifier);
226
- const updated = applyTaskEdit(stored.task, dottedPath, value);
227
- await saveTask(workspaceRoot, stored.status, updated);
228
- process.stdout.write(`Updated ${updated.ref}\n`);
229
- });
230
- }));
231
- program
232
- .command('search')
233
- .description('search tasks')
234
- .argument('<query>', 'search query')
235
- .action(handleAction(async (query, command) => {
236
- const workspaceRoot = await resolveWorkspaceFor(true);
237
- const aliases = await readAliases(workspaceRoot);
238
- const aliasLookup = buildAliasLookup(aliases);
239
- const tasks = await listAllTasks(workspaceRoot);
240
- const lines = tasks
241
- .filter((stored) => taskMatchesQuery(stored.task, query))
242
- .map((stored) => {
243
- const alias = aliasLookup.get(stored.task.id);
244
- let line = formatTaskListLine(stored.task, alias);
245
- if (stored.task.metadata.pr?.fetched?.state) {
246
- line += ` PR:${stored.task.metadata.pr.fetched.state}`;
247
- }
248
- return line;
249
- });
250
- printLines(lines);
251
- }));
252
- program
253
- .command('today')
254
- .description('assign or list today tasks')
255
- .argument('[id]', 'task identifier')
256
- .option('--date <date>', 'YYYY-MM-DD')
257
- .option('-m, --message <message>', 'assignment message')
258
- .action(handleAction(async (identifier, options, command) => {
259
- const workspaceRoot = await resolveWorkspaceFor(true);
260
- const date = todayDate(options.date);
261
- if (!identifier) {
262
- const aliases = await readAliases(workspaceRoot);
263
- const aliasLookup = buildAliasLookup(aliases);
264
- const tasks = await listAllTasks(workspaceRoot);
265
- const assigned = tasks.filter((stored) => isAssignedOnDate(stored.task, date));
266
- const lines = assigned.map((stored) => formatTaskListLine(stored.task, aliasLookup.get(stored.task.id)));
267
- printLines(lines);
268
- return;
269
- }
270
- await withWorkspaceLock(workspaceRoot, async () => {
271
- const stored = await resolveTask(workspaceRoot, identifier);
272
- if (isAssignedOnDate(stored.task, date)) {
273
- process.stdout.write(`Already assigned ${stored.task.ref} to ${date}\n`);
274
- return;
275
- }
276
- const updated = appendDayAssignment(stored.task, date, 'assign', options.message);
277
- await saveTask(workspaceRoot, stored.status, updated);
278
- process.stdout.write(`Assigned ${updated.ref} to ${date}\n`);
279
- });
280
- }));
281
- program
282
- .command('untoday')
283
- .description('unassign a task from today')
284
- .argument('<id>', 'task identifier')
285
- .option('--date <date>', 'YYYY-MM-DD')
286
- .action(handleAction(async (identifier, options, command) => {
287
- const workspaceRoot = await resolveWorkspaceFor(true);
288
- const date = todayDate(options.date);
289
- await withWorkspaceLock(workspaceRoot, async () => {
290
- const stored = await resolveTask(workspaceRoot, identifier);
291
- if (!isAssignedOnDate(stored.task, date)) {
292
- process.stdout.write(`Already unassigned ${stored.task.ref} from ${date}\n`);
293
- return;
294
- }
295
- const updated = appendDayAssignment(stored.task, date, 'unassign');
296
- await saveTask(workspaceRoot, stored.status, updated);
297
- process.stdout.write(`Unassigned ${updated.ref} from ${date}\n`);
298
- });
299
- }));
300
- program
301
- .command('attach-pr')
302
- .description('attach PR metadata to a task')
303
- .argument('<id>', 'task identifier')
304
- .argument('<url>', 'PR URL')
305
- .option('-m, --message <message>', 'log message')
306
- .action(handleAction(async (identifier, url, options, command) => {
307
- const workspaceRoot = await resolveWorkspaceFor(true);
308
- await withWorkspaceLock(workspaceRoot, async () => {
309
- const parsed = parseGitHubPrUrl(url);
310
- if (!parsed) {
311
- throw new Error('Invalid PR URL');
312
- }
313
- const token = await resolveGithubToken(workspaceRoot);
314
- const fetched = await fetchGitHubPr(parsed, token);
315
- const stored = await resolveTask(workspaceRoot, identifier);
316
- const updated = {
317
- ...stored.task,
318
- metadata: {
319
- ...stored.task.metadata,
320
- pr: buildPrAttachment(parsed, fetched ?? stored.task.metadata.pr?.fetched)
321
- },
322
- updated_at: nowIso()
323
- };
324
- const logMessage = options.message ?? `Attached PR ${url}`;
325
- const next = appendLog(updated, logMessage);
326
- await saveTask(workspaceRoot, stored.status, next);
327
- process.stdout.write(`Attached PR to ${next.ref}\n`);
328
- });
329
- }));
330
- program
331
- .command('refresh')
332
- .description('refresh PR metadata')
333
- .argument('<id>', 'task identifier')
334
- .option('-m, --message <message>', 'log message')
335
- .action(handleAction(async (identifier, options, command) => {
336
- const workspaceRoot = await resolveWorkspaceFor(true);
337
- await withWorkspaceLock(workspaceRoot, async () => {
338
- const stored = await resolveTask(workspaceRoot, identifier);
339
- const prUrl = stored.task.metadata.pr?.url;
340
- if (!prUrl) {
341
- process.stdout.write('No PR attached.\n');
342
- return;
343
- }
344
- const parsed = parseGitHubPrUrl(prUrl);
345
- if (!parsed) {
346
- throw new Error('Invalid PR URL');
347
- }
348
- const token = await resolveGithubToken(workspaceRoot);
349
- const fetched = await fetchGitHubPr(parsed, token);
350
- if (!fetched) {
351
- throw new Error('No GitHub token configured');
352
- }
353
- const baseUpdated = {
354
- ...stored.task,
355
- metadata: {
356
- ...stored.task.metadata,
357
- pr: buildPrAttachment(parsed, fetched)
358
- },
359
- updated_at: nowIso()
360
- };
361
- const previousState = stored.task.metadata.pr?.fetched?.state;
362
- const nextState = fetched.state;
363
- const isTerminal = stored.task.status === 'completed' || stored.task.status === 'cancelled';
364
- let finalTask = baseUpdated;
365
- let nextStatus = stored.status;
366
- if (!isTerminal && previousState !== nextState) {
367
- if (nextState === 'merged') {
368
- const result = applyTransition(baseUpdated, 'complete', options.message ?? 'PR merged');
369
- finalTask = result.task;
370
- nextStatus = result.nextStatus;
371
- }
372
- else if (nextState === 'closed') {
373
- const result = applyTransition(baseUpdated, 'cancel', options.message ?? 'PR closed without merge');
374
- finalTask = result.task;
375
- nextStatus = result.nextStatus;
376
- }
377
- }
378
- await saveTask(workspaceRoot, stored.status, finalTask);
379
- if (nextStatus !== stored.status) {
380
- await moveTaskFile(workspaceRoot, finalTask.id, stored.status, nextStatus);
381
- }
382
- process.stdout.write(`Refreshed ${finalTask.ref}\n`);
383
- });
384
- }));
385
- const aliasCommand = program.command('alias').description('alias helpers');
386
- aliasCommand
387
- .command('reconcile')
388
- .description('reconcile alias mapping')
389
- .action(handleAction(async (command) => {
390
- const workspaceRoot = await resolveWorkspaceFor(true);
391
- await withWorkspaceLock(workspaceRoot, async () => {
392
- await reconcileAliases(workspaceRoot);
393
- process.stdout.write('Aliases reconciled.\n');
394
- });
395
- }));
396
- program.parseAsync(process.argv);