@lumoai/cli 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.
- package/README.md +90 -0
- package/dist/cli/src/commands/auth-login.js +55 -0
- package/dist/cli/src/commands/auth-logout.js +14 -0
- package/dist/cli/src/commands/doc-bind.js +52 -0
- package/dist/cli/src/commands/doc-create.js +138 -0
- package/dist/cli/src/commands/doc-delete.js +52 -0
- package/dist/cli/src/commands/doc-list.js +91 -0
- package/dist/cli/src/commands/doc-move.js +113 -0
- package/dist/cli/src/commands/doc-share-list.js +62 -0
- package/dist/cli/src/commands/doc-share.js +77 -0
- package/dist/cli/src/commands/doc-show.js +99 -0
- package/dist/cli/src/commands/doc-unbind.js +47 -0
- package/dist/cli/src/commands/doc-unshare.js +71 -0
- package/dist/cli/src/commands/doc-update.js +144 -0
- package/dist/cli/src/commands/hook.js +21 -0
- package/dist/cli/src/commands/milestone-create.js +84 -0
- package/dist/cli/src/commands/milestone-delete.js +96 -0
- package/dist/cli/src/commands/milestone-list.js +55 -0
- package/dist/cli/src/commands/milestone-show.js +106 -0
- package/dist/cli/src/commands/milestone-update.js +167 -0
- package/dist/cli/src/commands/project-list.js +57 -0
- package/dist/cli/src/commands/session-attach.js +80 -0
- package/dist/cli/src/commands/session-detach.js +60 -0
- package/dist/cli/src/commands/session-status.js +58 -0
- package/dist/cli/src/commands/sprint-add.js +62 -0
- package/dist/cli/src/commands/sprint-close.js +151 -0
- package/dist/cli/src/commands/sprint-create.js +80 -0
- package/dist/cli/src/commands/sprint-delete.js +88 -0
- package/dist/cli/src/commands/sprint-list.js +85 -0
- package/dist/cli/src/commands/sprint-remove.js +57 -0
- package/dist/cli/src/commands/sprint-show.js +81 -0
- package/dist/cli/src/commands/sprint-start.js +68 -0
- package/dist/cli/src/commands/sprint-summary.js +138 -0
- package/dist/cli/src/commands/sprint-update.js +148 -0
- package/dist/cli/src/commands/task-comment.js +95 -0
- package/dist/cli/src/commands/task-context.js +137 -0
- package/dist/cli/src/commands/task-create.js +202 -0
- package/dist/cli/src/commands/task-list.js +94 -0
- package/dist/cli/src/commands/task-show.js +74 -0
- package/dist/cli/src/commands/task-update.js +468 -0
- package/dist/cli/src/commands/whoami.js +16 -0
- package/dist/cli/src/index.js +492 -0
- package/dist/cli/src/lib/api.js +33 -0
- package/dist/cli/src/lib/browser.js +33 -0
- package/dist/cli/src/lib/config.js +92 -0
- package/dist/cli/src/lib/doc-input.js +80 -0
- package/dist/cli/src/lib/doc-sort-order.js +23 -0
- package/dist/cli/src/lib/doc-tree.js +54 -0
- package/dist/cli/src/lib/format.js +33 -0
- package/dist/cli/src/lib/hook-log.js +81 -0
- package/dist/cli/src/lib/hook-runner.js +156 -0
- package/dist/cli/src/lib/markdown-tiptap.js +7 -0
- package/dist/cli/src/lib/prompt.js +71 -0
- package/dist/cli/src/lib/resolve-doc-id.js +34 -0
- package/dist/cli/src/lib/resolve-doc.js +27 -0
- package/dist/cli/src/lib/resolve-member.js +58 -0
- package/dist/cli/src/lib/resolve.js +170 -0
- package/dist/cli/src/lib/tag-resolver.js +49 -0
- package/dist/shared/src/index.js +35 -0
- package/dist/shared/src/markdown-tiptap.js +267 -0
- package/package.json +48 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatMilestoneList = formatMilestoneList;
|
|
4
|
+
exports.milestoneList = milestoneList;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
function formatDate(iso) {
|
|
9
|
+
if (!iso)
|
|
10
|
+
return '-';
|
|
11
|
+
return iso.slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render milestones as fixed-width rows:
|
|
15
|
+
* <STATUS> <target-date or -> <name>
|
|
16
|
+
*
|
|
17
|
+
* Sorted server-side by targetDate asc nulls last, createdAt asc.
|
|
18
|
+
*/
|
|
19
|
+
function formatMilestoneList(rows) {
|
|
20
|
+
if (rows.length === 0)
|
|
21
|
+
return 'No milestones.';
|
|
22
|
+
const statusW = Math.max(...rows.map(r => r.status.length));
|
|
23
|
+
const dateW = Math.max(...rows.map(r => formatDate(r.targetDate).length));
|
|
24
|
+
return rows
|
|
25
|
+
.map(r => `${r.status.padEnd(statusW)} ${formatDate(r.targetDate).padEnd(dateW)} ${r.name}`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
}
|
|
28
|
+
async function milestoneList(options) {
|
|
29
|
+
const creds = (0, config_1.readCredentials)();
|
|
30
|
+
if (!creds) {
|
|
31
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
35
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
36
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
37
|
+
let projectId;
|
|
38
|
+
try {
|
|
39
|
+
projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, options.project);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
console.error(`Error: ${msg}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const res = await fetch(`${base}/api/projects/${projectId}/milestones`, {
|
|
47
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
console.error(`Error: milestone list failed (HTTP ${res.status})`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
process.stdout.write(formatMilestoneList(data.milestones) + '\n');
|
|
55
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatMilestoneShow = formatMilestoneShow;
|
|
4
|
+
exports.milestoneShow = milestoneShow;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
const format_1 = require("../lib/format");
|
|
9
|
+
function fmtDate(iso) {
|
|
10
|
+
return iso ? iso.slice(0, 10) : '-';
|
|
11
|
+
}
|
|
12
|
+
function formatMilestoneShow(m, tasks) {
|
|
13
|
+
const total = m.taskCounts.TODO +
|
|
14
|
+
m.taskCounts.IN_PROGRESS +
|
|
15
|
+
m.taskCounts.IN_REVIEW +
|
|
16
|
+
m.taskCounts.DONE;
|
|
17
|
+
const lines = [
|
|
18
|
+
`Milestone: ${m.name}`,
|
|
19
|
+
`Status: ${m.status}`,
|
|
20
|
+
`Start: ${fmtDate(m.startDate)}`,
|
|
21
|
+
`Target: ${fmtDate(m.targetDate)}`,
|
|
22
|
+
`Project: ${m.projectName}`,
|
|
23
|
+
`Description:`,
|
|
24
|
+
` ${m.description && m.description.length > 0 ? m.description : '-'}`,
|
|
25
|
+
``,
|
|
26
|
+
`Tasks: ${total} total (TODO ${m.taskCounts.TODO} / IN_PROGRESS ${m.taskCounts.IN_PROGRESS} / IN_REVIEW ${m.taskCounts.IN_REVIEW} / DONE ${m.taskCounts.DONE})`,
|
|
27
|
+
];
|
|
28
|
+
if (tasks.length > 0) {
|
|
29
|
+
lines.push('', (0, format_1.formatTaskListTable)(tasks));
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
async function milestoneShow(identifier, opts) {
|
|
34
|
+
const creds = (0, config_1.readCredentials)();
|
|
35
|
+
if (!creds) {
|
|
36
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
40
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
41
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
42
|
+
let milestoneId;
|
|
43
|
+
try {
|
|
44
|
+
const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
45
|
+
milestoneId = resolved.id;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
// Fetch the milestone (with taskCounts) and the task list in parallel.
|
|
52
|
+
let milestoneRes;
|
|
53
|
+
let tasksRes;
|
|
54
|
+
try {
|
|
55
|
+
;
|
|
56
|
+
[milestoneRes, tasksRes] = await Promise.all([
|
|
57
|
+
fetch(`${base}/api/milestones/${milestoneId}`, {
|
|
58
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
59
|
+
}),
|
|
60
|
+
fetch(`${base}/api/milestones/${milestoneId}/tasks`, {
|
|
61
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
62
|
+
}),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
if (milestoneRes.status === 401 || tasksRes.status === 401) {
|
|
71
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
if (!milestoneRes.ok) {
|
|
75
|
+
console.error(`Error: milestone show failed (HTTP ${milestoneRes.status})`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
if (!tasksRes.ok) {
|
|
79
|
+
console.error(`Error: milestone tasks failed (HTTP ${tasksRes.status})`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
const { milestone } = (await milestoneRes.json());
|
|
83
|
+
const { tasks } = (await tasksRes.json());
|
|
84
|
+
// Look up project display name. If the caller passed `--project <ref>`
|
|
85
|
+
// we already know it; otherwise hit /api/projects once.
|
|
86
|
+
let projectName = opts.project ?? '';
|
|
87
|
+
if (!projectName) {
|
|
88
|
+
const pr = await fetch(`${base}/api/projects`, {
|
|
89
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
90
|
+
});
|
|
91
|
+
if (pr.ok) {
|
|
92
|
+
const { projects } = (await pr.json());
|
|
93
|
+
projectName = projects.find(p => p.id === milestone.projectId)?.name ?? '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(formatMilestoneShow({
|
|
97
|
+
id: milestone.id,
|
|
98
|
+
name: milestone.name,
|
|
99
|
+
status: milestone.status,
|
|
100
|
+
startDate: milestone.startDate,
|
|
101
|
+
targetDate: milestone.targetDate,
|
|
102
|
+
description: milestone.description,
|
|
103
|
+
projectName,
|
|
104
|
+
taskCounts: milestone.taskCounts,
|
|
105
|
+
}, tasks) + '\n');
|
|
106
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeMilestoneStatus = normalizeMilestoneStatus;
|
|
4
|
+
exports.buildMilestoneUpdatePayload = buildMilestoneUpdatePayload;
|
|
5
|
+
exports.formatMilestoneUpdateSummary = formatMilestoneUpdateSummary;
|
|
6
|
+
exports.milestoneUpdate = milestoneUpdate;
|
|
7
|
+
const config_1 = require("../lib/config");
|
|
8
|
+
const api_1 = require("../lib/api");
|
|
9
|
+
const resolve_1 = require("../lib/resolve");
|
|
10
|
+
const ALLOWED_STATUSES = [
|
|
11
|
+
'PLANNED',
|
|
12
|
+
'ACTIVE',
|
|
13
|
+
'COMPLETED',
|
|
14
|
+
'CANCELLED',
|
|
15
|
+
];
|
|
16
|
+
function normalizeMilestoneStatus(value) {
|
|
17
|
+
if (!value)
|
|
18
|
+
return null;
|
|
19
|
+
const upper = value.toUpperCase().replace(/-/g, '_');
|
|
20
|
+
return ALLOWED_STATUSES.includes(upper)
|
|
21
|
+
? upper
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
function buildMilestoneUpdatePayload(opts) {
|
|
25
|
+
const payload = {};
|
|
26
|
+
const flagsGiven = [];
|
|
27
|
+
if (opts.name !== undefined) {
|
|
28
|
+
payload.name = opts.name;
|
|
29
|
+
flagsGiven.push('--name');
|
|
30
|
+
}
|
|
31
|
+
if (opts.description !== undefined) {
|
|
32
|
+
payload.description = opts.description === '' ? null : opts.description;
|
|
33
|
+
flagsGiven.push('--description');
|
|
34
|
+
}
|
|
35
|
+
if (opts.status !== undefined) {
|
|
36
|
+
payload.status = opts.status;
|
|
37
|
+
flagsGiven.push('--status');
|
|
38
|
+
}
|
|
39
|
+
if (opts.start !== undefined) {
|
|
40
|
+
payload.startDate = opts.start === '' ? null : opts.start;
|
|
41
|
+
flagsGiven.push('--start');
|
|
42
|
+
}
|
|
43
|
+
if (opts.target !== undefined) {
|
|
44
|
+
payload.targetDate = opts.target === '' ? null : opts.target;
|
|
45
|
+
flagsGiven.push('--target');
|
|
46
|
+
}
|
|
47
|
+
return { payload, flagsGiven };
|
|
48
|
+
}
|
|
49
|
+
function fmtDate(v) {
|
|
50
|
+
if (v === null)
|
|
51
|
+
return '∅';
|
|
52
|
+
if (v === undefined)
|
|
53
|
+
return '-';
|
|
54
|
+
return v.slice(0, 10);
|
|
55
|
+
}
|
|
56
|
+
function formatMilestoneUpdateSummary(before, after) {
|
|
57
|
+
const changes = [];
|
|
58
|
+
if (after.name !== undefined && after.name !== before.name) {
|
|
59
|
+
changes.push(`name "${before.name}" → "${after.name}"`);
|
|
60
|
+
}
|
|
61
|
+
if (after.description !== undefined) {
|
|
62
|
+
changes.push(after.description === null ? 'description → ∅' : 'description updated');
|
|
63
|
+
}
|
|
64
|
+
if (after.status !== undefined && after.status !== before.status) {
|
|
65
|
+
changes.push(`status ${before.status} → ${after.status}`);
|
|
66
|
+
}
|
|
67
|
+
if (after.startDate !== undefined) {
|
|
68
|
+
changes.push(`start → ${fmtDate(after.startDate)}`);
|
|
69
|
+
}
|
|
70
|
+
if (after.targetDate !== undefined) {
|
|
71
|
+
changes.push(`target → ${fmtDate(after.targetDate)}`);
|
|
72
|
+
}
|
|
73
|
+
return `Updated milestone "${before.name}": ${changes.join(', ')}`;
|
|
74
|
+
}
|
|
75
|
+
async function milestoneUpdate(identifier, opts) {
|
|
76
|
+
// Validate status flag eagerly.
|
|
77
|
+
let normalizedStatus;
|
|
78
|
+
if (opts.status !== undefined) {
|
|
79
|
+
const n = normalizeMilestoneStatus(opts.status);
|
|
80
|
+
if (!n) {
|
|
81
|
+
console.error(`Error: invalid status "${opts.status}". Allowed: planned, active, completed, cancelled`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
normalizedStatus = n;
|
|
85
|
+
}
|
|
86
|
+
const { payload, flagsGiven } = buildMilestoneUpdatePayload({
|
|
87
|
+
...opts,
|
|
88
|
+
...(normalizedStatus !== undefined && { status: normalizedStatus }),
|
|
89
|
+
});
|
|
90
|
+
if (flagsGiven.length === 0) {
|
|
91
|
+
console.error('Error: provide at least one field to update (--name, --description, --status, --start, --target)');
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const creds = (0, config_1.readCredentials)();
|
|
95
|
+
if (!creds) {
|
|
96
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
100
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
101
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
102
|
+
let milestoneId;
|
|
103
|
+
try {
|
|
104
|
+
const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
|
|
105
|
+
milestoneId = resolved.id;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
// Fetch current values so we can render a delta summary. Wrap network in
|
|
112
|
+
// try/catch + check 401 explicitly to match milestone-create / task-create.
|
|
113
|
+
let beforeRes;
|
|
114
|
+
try {
|
|
115
|
+
beforeRes = await fetch(`${base}/api/milestones/${milestoneId}`, {
|
|
116
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
if (beforeRes.status === 401) {
|
|
125
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (!beforeRes.ok) {
|
|
129
|
+
console.error(`Error: milestone fetch failed (HTTP ${beforeRes.status})`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
const { milestone: before } = (await beforeRes.json());
|
|
133
|
+
let res;
|
|
134
|
+
try {
|
|
135
|
+
res = await fetch(`${base}/api/milestones/${milestoneId}`, {
|
|
136
|
+
method: 'PATCH',
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${creds.token}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(payload),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
if (res.status === 401) {
|
|
150
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
let errMsg = `milestone update failed (HTTP ${res.status})`;
|
|
155
|
+
try {
|
|
156
|
+
const errBody = (await res.json());
|
|
157
|
+
if (errBody.error)
|
|
158
|
+
errMsg = errBody.error;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// body wasn't JSON; keep the status-only message
|
|
162
|
+
}
|
|
163
|
+
console.error(`Error: ${errMsg}`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(formatMilestoneUpdateSummary(before, payload) + '\n');
|
|
167
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatProjectList = formatProjectList;
|
|
4
|
+
exports.projectList = projectList;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
/**
|
|
9
|
+
* Render projects as `slug-style-name Display Name` lines. The slug form
|
|
10
|
+
* matches what `lumo task create --project <ref>` accepts (slugify(name)),
|
|
11
|
+
* so users can copy a slug straight into the create flag.
|
|
12
|
+
*
|
|
13
|
+
* Slug source of truth: `cli/src/lib/resolve.ts` (mirrors server's
|
|
14
|
+
* `lib/utils/slugify.ts`). Keep that single copy in sync with the server.
|
|
15
|
+
*/
|
|
16
|
+
function formatProjectList(projects) {
|
|
17
|
+
if (projects.length === 0)
|
|
18
|
+
return 'No projects.';
|
|
19
|
+
const rows = projects
|
|
20
|
+
.filter(p => !p.isIdea)
|
|
21
|
+
.map(p => ({ slug: (0, resolve_1.slugify)(p.name), name: p.name }));
|
|
22
|
+
if (rows.length === 0)
|
|
23
|
+
return 'No projects.';
|
|
24
|
+
const w = Math.max(...rows.map(r => r.slug.length));
|
|
25
|
+
return rows.map(r => `${r.slug.padEnd(w)} ${r.name}`).join('\n');
|
|
26
|
+
}
|
|
27
|
+
async function projectList() {
|
|
28
|
+
const creds = (0, config_1.readCredentials)();
|
|
29
|
+
if (!creds) {
|
|
30
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
34
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
35
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/projects`;
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(url, {
|
|
39
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 401) {
|
|
48
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
console.error(`Error: project list failed (HTTP ${res.status})`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const data = (await res.json());
|
|
56
|
+
process.stdout.write(formatProjectList(data.projects) + '\n');
|
|
57
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionAttach = sessionAttach;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
/**
|
|
7
|
+
* `lumo session attach <identifier>` — bind the currently-running
|
|
8
|
+
* Claude Code session to a task.
|
|
9
|
+
*
|
|
10
|
+
* Required environment: `CLAUDE_CODE_SESSION_ID` (set automatically by
|
|
11
|
+
* Claude Code). Must be invoked from inside a Claude Code session — there
|
|
12
|
+
* is no longer a global "current task" pointer for terminals outside CC.
|
|
13
|
+
*
|
|
14
|
+
* The binding lives entirely on the server (`Session.taskId`); subsequent
|
|
15
|
+
* hooks read it back via the session row. The CLI keeps no local sentinel.
|
|
16
|
+
*/
|
|
17
|
+
async function sessionAttach(identifier) {
|
|
18
|
+
if (!identifier) {
|
|
19
|
+
console.error('Error: missing <identifier>. Usage: lumo session attach <LUM-42>');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
23
|
+
if (!sessionId) {
|
|
24
|
+
console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
|
|
25
|
+
'`lumo session attach` must be run inside a Claude Code session.');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const creds = (0, config_1.readCredentials)();
|
|
29
|
+
if (!creds) {
|
|
30
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
34
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
35
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
Authorization: `Bearer ${creds.token}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 401) {
|
|
53
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
if (res.status === 404) {
|
|
57
|
+
let message = 'Not found';
|
|
58
|
+
try {
|
|
59
|
+
const body = (await res.json());
|
|
60
|
+
if (body.error)
|
|
61
|
+
message = body.error;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// fall through
|
|
65
|
+
}
|
|
66
|
+
console.error(`Error: ${message}`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
console.error(`Error: bind-task failed (HTTP ${res.status})`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const result = (await res.json());
|
|
74
|
+
console.log(`Attached session ${sessionId} to ${result.taskIdentifier} "${result.taskTitle}"`);
|
|
75
|
+
console.log(`Re-tagged ${result.retaggedEventCount} previously-untagged event${result.retaggedEventCount === 1 ? '' : 's'} in this session.`);
|
|
76
|
+
if (result.memorySection !== '') {
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(result.memorySection);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionDetach = sessionDetach;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
/**
|
|
7
|
+
* `lumo session detach` — clear the task binding on the current Claude Code
|
|
8
|
+
* session. Idempotent: re-detaching an already-unbound session reports
|
|
9
|
+
* "already unbound" instead of erroring.
|
|
10
|
+
*
|
|
11
|
+
* Past HookEvent rows keep their original taskId — only future events on
|
|
12
|
+
* this session will be unbound. Re-attach later with `session attach
|
|
13
|
+
* <LUM-N>` to point the session at a different task.
|
|
14
|
+
*/
|
|
15
|
+
async function sessionDetach() {
|
|
16
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
17
|
+
if (!sessionId) {
|
|
18
|
+
console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
|
|
19
|
+
'`lumo session detach` must be run inside a Claude Code session.');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const creds = (0, config_1.readCredentials)();
|
|
23
|
+
if (!creds) {
|
|
24
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
28
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
29
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
30
|
+
let res;
|
|
31
|
+
try {
|
|
32
|
+
res = await fetch(url, {
|
|
33
|
+
method: 'DELETE',
|
|
34
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (res.status === 401) {
|
|
43
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (res.status === 404) {
|
|
47
|
+
process.stdout.write(`Session ${sessionId} has no server-side state yet — nothing to detach.\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
console.error(`Error: session detach failed (HTTP ${res.status})`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const data = (await res.json());
|
|
55
|
+
if (data.alreadyUnbound) {
|
|
56
|
+
process.stdout.write(`Session ${sessionId} was already unbound.\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
process.stdout.write(`Detached session ${sessionId} from ${data.previousTaskIdentifier} "${data.previousTaskTitle}".\n`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionStatus = sessionStatus;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
async function sessionStatus() {
|
|
7
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
8
|
+
if (!sessionId) {
|
|
9
|
+
console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
|
|
10
|
+
'`lumo session status` must be run inside a Claude Code session.');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const creds = (0, config_1.readCredentials)();
|
|
14
|
+
if (!creds) {
|
|
15
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
19
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
20
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}`;
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(url, {
|
|
24
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
if (res.status === 401) {
|
|
33
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 404) {
|
|
37
|
+
// Session row only materializes after the first hook event or an
|
|
38
|
+
// explicit `session attach`. Until then there's nothing to report.
|
|
39
|
+
process.stdout.write(`Session ${sessionId} has no server-side state yet (no task bound, no hook events).\n` +
|
|
40
|
+
`Run \`lumo session attach <LUM-N>\` to bind it.\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
console.error(`Error: session status failed (HTTP ${res.status})`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const data = (await res.json());
|
|
48
|
+
if (data.taskIdentifier && data.taskTitle) {
|
|
49
|
+
process.stdout.write(`Session ${sessionId}\n` +
|
|
50
|
+
` Bound to: ${data.taskIdentifier} "${data.taskTitle}"\n` +
|
|
51
|
+
` Events: ${data.eventCount}\n`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
process.stdout.write(`Session ${sessionId}\n` +
|
|
55
|
+
` Bound to: (no task)\n` +
|
|
56
|
+
` Events: ${data.eventCount}\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sprintAdd = sprintAdd;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const resolve_1 = require("../lib/resolve");
|
|
7
|
+
async function sprintAdd(identifier, taskIdentifier, opts) {
|
|
8
|
+
const creds = (0, config_1.readCredentials)();
|
|
9
|
+
if (!creds) {
|
|
10
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
14
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
15
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
16
|
+
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
17
|
+
let resolved;
|
|
18
|
+
try {
|
|
19
|
+
resolved = await (0, resolve_1.resolveSprintId)(base, creds.token, identifier, opts.team, workspaceSlug);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
// Resolve task → id via /api/tasks/by-identifier/<LUM-N>
|
|
26
|
+
const tr = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(taskIdentifier)}`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
27
|
+
if (!tr.ok) {
|
|
28
|
+
console.error(`Error: task ${taskIdentifier} not found (HTTP ${tr.status})`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
const { task } = (await tr.json());
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(`${base}/api/sprints/${resolved.id}/tasks`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${creds.token}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ taskId: task.id }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
let errMsg = `sprint add failed (HTTP ${res.status})`;
|
|
50
|
+
try {
|
|
51
|
+
const errBody = (await res.json());
|
|
52
|
+
if (errBody.error)
|
|
53
|
+
errMsg = errBody.error;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
console.error(`Error: ${errMsg}`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(`Added ${taskIdentifier} to sprint #${resolved.number}\n`);
|
|
62
|
+
}
|