exponential-cli 1.14.0 → 1.18.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 +87 -2
- package/dist/index.js +1608 -244
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import { Command as
|
|
3
|
+
// src/program.ts
|
|
4
|
+
import { Command as Command25 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -125,7 +125,7 @@ function handleError(error, json = false) {
|
|
|
125
125
|
// src/commands/auth.ts
|
|
126
126
|
function createAuthCommand() {
|
|
127
127
|
const auth = new Command("auth").description("Authentication commands");
|
|
128
|
-
auth.command("login").description("Configure CLI with your API token").requiredOption("--token <token>", "JWT token from /
|
|
128
|
+
auth.command("login").description("Configure CLI with your API token").requiredOption("--token <token>", "JWT token from /settings/api-keys").requiredOption("--api-url <url>", "API URL (e.g., https://www.exponential.im)").action(async (options) => {
|
|
129
129
|
try {
|
|
130
130
|
let apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
131
131
|
if (!apiUrl.startsWith("http://localhost") && !apiUrl.startsWith("https://")) {
|
|
@@ -209,6 +209,33 @@ function createAuthCommand() {
|
|
|
209
209
|
// src/commands/actions.ts
|
|
210
210
|
import { Command as Command2 } from "commander";
|
|
211
211
|
|
|
212
|
+
// src/utils/resolve.ts
|
|
213
|
+
async function resolveWorkspaceId(client, slugOrId) {
|
|
214
|
+
return (await resolveWorkspace(client, slugOrId)).id;
|
|
215
|
+
}
|
|
216
|
+
async function resolveWorkspace(client, slugOrId) {
|
|
217
|
+
const config = getConfig();
|
|
218
|
+
const candidate = slugOrId ?? config.defaultWorkspaceSlug ?? config.defaultWorkspaceId;
|
|
219
|
+
if (!candidate) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
"No workspace specified and no default workspace set. Pass --workspace <slug> or run `exponential workspaces set-default <slug>`."
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const workspaces = await client.workspaces.list();
|
|
225
|
+
const byId = workspaces.find((w) => w.id === candidate);
|
|
226
|
+
if (byId) return byId;
|
|
227
|
+
const bySlug = workspaces.find((w) => w.slug === candidate);
|
|
228
|
+
if (bySlug) return bySlug;
|
|
229
|
+
const known = workspaces.map((w) => w.slug).join(", ");
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Workspace "${candidate}" not found. Available: ${known || "(none)"}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
async function resolveProductId(client, workspaceId, slugOrId) {
|
|
235
|
+
const product = await client.products.resolve(workspaceId, slugOrId);
|
|
236
|
+
return product.id;
|
|
237
|
+
}
|
|
238
|
+
|
|
212
239
|
// src/utils/output.ts
|
|
213
240
|
import chalk3 from "chalk";
|
|
214
241
|
function shouldUseJson(forceJson, forcePretty) {
|
|
@@ -236,10 +263,12 @@ function transformAction(action) {
|
|
|
236
263
|
slug: action.workspace.slug,
|
|
237
264
|
name: action.workspace.name
|
|
238
265
|
} : null,
|
|
266
|
+
// `action.upsertBySource` returns assignees as bare `{ userId }` rows;
|
|
267
|
+
// every other procedure nests the user. Accept both.
|
|
239
268
|
assignees: action.assignees?.map((a) => ({
|
|
240
|
-
id: a.user.
|
|
241
|
-
name: a.user
|
|
242
|
-
email: a.user
|
|
269
|
+
id: a.user?.id ?? a.userId ?? "",
|
|
270
|
+
name: a.user?.name ?? null,
|
|
271
|
+
email: a.user?.email ?? null
|
|
243
272
|
})) ?? [],
|
|
244
273
|
createdAt: action.createdAt?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
274
|
completedAt: action.completedAt?.toISOString() ?? null
|
|
@@ -271,12 +300,15 @@ function outputActionJson(action) {
|
|
|
271
300
|
const output = transformAction(action);
|
|
272
301
|
console.log(JSON.stringify(output, null, 2));
|
|
273
302
|
}
|
|
303
|
+
function outputActionUpsertJson(result) {
|
|
304
|
+
console.log(JSON.stringify({ outcome: result.outcome, action: transformAction(result.action) }, null, 2));
|
|
305
|
+
}
|
|
274
306
|
function outputActionPretty(action) {
|
|
275
307
|
const statusColor = getKanbanStatusColor(action.kanbanStatus);
|
|
276
|
-
const
|
|
308
|
+
const statusBadge2 = action.kanbanStatus ? chalk3[statusColor](`[${action.kanbanStatus}]`) : chalk3.gray("[NO STATUS]");
|
|
277
309
|
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
278
310
|
console.log(`
|
|
279
|
-
${
|
|
311
|
+
${statusBadge2} ${chalk3.bold(action.name)}`);
|
|
280
312
|
console.log(chalk3.gray(` ID: ${action.id}`));
|
|
281
313
|
if (action.project) {
|
|
282
314
|
console.log(` ${chalk3.cyan("Project:")} ${action.project.name}`);
|
|
@@ -309,9 +341,9 @@ Actions (${actions.length} total)`));
|
|
|
309
341
|
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
310
342
|
for (const action of actions) {
|
|
311
343
|
const statusColor = getKanbanStatusColor(action.kanbanStatus);
|
|
312
|
-
const
|
|
344
|
+
const statusBadge2 = action.kanbanStatus ? chalk3[statusColor](`[${action.kanbanStatus}]`) : chalk3.gray("[NO STATUS]");
|
|
313
345
|
console.log(`
|
|
314
|
-
${
|
|
346
|
+
${statusBadge2} ${chalk3.bold(action.name)}`);
|
|
315
347
|
console.log(chalk3.gray(` ID: ${action.id}`));
|
|
316
348
|
if (action.project) {
|
|
317
349
|
console.log(` ${chalk3.cyan("Project:")} ${action.project.name}`);
|
|
@@ -930,9 +962,9 @@ function outputFeaturesPretty(features) {
|
|
|
930
962
|
Features (${features.length} total)`));
|
|
931
963
|
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
932
964
|
for (const f of features) {
|
|
933
|
-
const
|
|
965
|
+
const statusBadge2 = chalk3[getFeatureStatusColor(f.status)](`[${f.status}]`);
|
|
934
966
|
const ticketCount = f._count?.tickets != null ? chalk3.gray(` \u2014 ${f._count.tickets} tickets`) : "";
|
|
935
|
-
console.log(` ${
|
|
967
|
+
console.log(` ${statusBadge2} ${chalk3.bold(f.name)}${ticketCount}`);
|
|
936
968
|
console.log(chalk3.gray(` ID: ${f.id}`));
|
|
937
969
|
if (f.keyResultLinks?.length) {
|
|
938
970
|
const titles = f.keyResultLinks.map((l) => l.keyResult.title).join("; ");
|
|
@@ -1147,10 +1179,10 @@ Tickets (${tickets.length} total)`));
|
|
|
1147
1179
|
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
1148
1180
|
for (const t of tickets) {
|
|
1149
1181
|
const id = t.shortId ?? (t.number != null ? `#${t.number}` : "?");
|
|
1150
|
-
const
|
|
1182
|
+
const statusBadge2 = chalk3[getTicketStatusColor(t.status)](`[${t.status}]`);
|
|
1151
1183
|
const typeBadge = chalk3.gray(`[${t.type}]`);
|
|
1152
1184
|
const blocked = t.isBlocked ? chalk3.red(" BLOCKED") : "";
|
|
1153
|
-
console.log(` ${
|
|
1185
|
+
console.log(` ${statusBadge2} ${typeBadge} ${chalk3.bold(id)} ${t.title}${blocked}`);
|
|
1154
1186
|
console.log(chalk3.gray(` ID: ${t.id}`));
|
|
1155
1187
|
}
|
|
1156
1188
|
console.log();
|
|
@@ -1270,11 +1302,11 @@ function outputEpicsPretty(epics) {
|
|
|
1270
1302
|
Epics (${epics.length} total)`));
|
|
1271
1303
|
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
1272
1304
|
for (const e of epics) {
|
|
1273
|
-
const
|
|
1305
|
+
const statusBadge2 = chalk3[getEpicStatusColor(e.status)](`[${e.status}]`);
|
|
1274
1306
|
const counts = e._count ? chalk3.gray(
|
|
1275
1307
|
` \u2014 ${e._count.tickets ?? 0} tickets, ${e._count.actions ?? 0} actions`
|
|
1276
1308
|
) : "";
|
|
1277
|
-
console.log(` ${
|
|
1309
|
+
console.log(` ${statusBadge2} ${chalk3.bold(e.name)}${counts}`);
|
|
1278
1310
|
console.log(chalk3.gray(` ID: ${e.id}`));
|
|
1279
1311
|
}
|
|
1280
1312
|
console.log();
|
|
@@ -1509,6 +1541,242 @@ Meetings (${meetings.length} total)`));
|
|
|
1509
1541
|
}
|
|
1510
1542
|
console.log();
|
|
1511
1543
|
}
|
|
1544
|
+
function getDecisionStatusColor(status) {
|
|
1545
|
+
switch (status) {
|
|
1546
|
+
case "OPEN":
|
|
1547
|
+
return "yellow";
|
|
1548
|
+
case "PROPOSED":
|
|
1549
|
+
return "blue";
|
|
1550
|
+
case "ACCEPTED":
|
|
1551
|
+
return "green";
|
|
1552
|
+
case "DEPRECATED":
|
|
1553
|
+
return "red";
|
|
1554
|
+
default:
|
|
1555
|
+
return "gray";
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function transformDecision(d) {
|
|
1559
|
+
return {
|
|
1560
|
+
id: d.id,
|
|
1561
|
+
workspaceId: d.workspaceId,
|
|
1562
|
+
number: d.number,
|
|
1563
|
+
label: d.label,
|
|
1564
|
+
statement: d.statement,
|
|
1565
|
+
body: d.body,
|
|
1566
|
+
status: d.status,
|
|
1567
|
+
isOpenQuestion: d.status === "OPEN",
|
|
1568
|
+
reviewState: d.reviewState,
|
|
1569
|
+
source: d.source,
|
|
1570
|
+
decidedAt: d.decidedAt,
|
|
1571
|
+
ownerId: d.ownerId,
|
|
1572
|
+
createdById: d.createdById,
|
|
1573
|
+
confirmedById: d.confirmedById,
|
|
1574
|
+
confirmedAt: d.confirmedAt,
|
|
1575
|
+
transcriptionSessionId: d.transcriptionSessionId,
|
|
1576
|
+
occurrenceId: d.occurrenceId,
|
|
1577
|
+
productId: d.productId,
|
|
1578
|
+
projectId: d.projectId,
|
|
1579
|
+
goalId: d.goalId,
|
|
1580
|
+
keyResultId: d.keyResultId,
|
|
1581
|
+
supersededById: d.supersededById,
|
|
1582
|
+
adrDocumentId: d.adrDocumentId,
|
|
1583
|
+
evidence: d.evidence ?? [],
|
|
1584
|
+
createdAt: d.createdAt,
|
|
1585
|
+
updatedAt: d.updatedAt,
|
|
1586
|
+
owner: d.owner ?? null,
|
|
1587
|
+
deciders: d.deciders ?? [],
|
|
1588
|
+
meeting: d.transcriptionSession ?? null,
|
|
1589
|
+
product: d.product ?? null,
|
|
1590
|
+
project: d.project ?? null,
|
|
1591
|
+
goal: d.goal ?? null,
|
|
1592
|
+
keyResult: d.keyResult ?? null,
|
|
1593
|
+
supersededBy: d.supersededBy ?? null,
|
|
1594
|
+
supersedes: d.supersedes ?? [],
|
|
1595
|
+
links: d.links ?? [],
|
|
1596
|
+
canEdit: d.canEdit ?? null
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function transformDecisionListRow(d) {
|
|
1600
|
+
return {
|
|
1601
|
+
id: d.id,
|
|
1602
|
+
workspaceId: d.workspaceId,
|
|
1603
|
+
number: d.number,
|
|
1604
|
+
label: d.label,
|
|
1605
|
+
statement: d.statement,
|
|
1606
|
+
status: d.status,
|
|
1607
|
+
isOpenQuestion: d.status === "OPEN",
|
|
1608
|
+
reviewState: d.reviewState ?? null,
|
|
1609
|
+
source: d.source,
|
|
1610
|
+
decidedAt: d.decidedAt,
|
|
1611
|
+
updatedAt: d.updatedAt,
|
|
1612
|
+
transcriptionSessionId: d.transcriptionSessionId,
|
|
1613
|
+
occurrenceId: d.occurrenceId,
|
|
1614
|
+
productId: d.productId,
|
|
1615
|
+
projectId: d.projectId,
|
|
1616
|
+
supersededById: d.supersededById,
|
|
1617
|
+
evidenceCount: d.evidenceCount,
|
|
1618
|
+
linkCount: d._count?.links ?? 0,
|
|
1619
|
+
deciderCount: d._count?.deciders ?? 0,
|
|
1620
|
+
product: d.product ?? null,
|
|
1621
|
+
project: d.project ?? null,
|
|
1622
|
+
meeting: d.transcriptionSession ?? null,
|
|
1623
|
+
supersededBy: d.supersededBy ?? null
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function outputDecisionJson(decision) {
|
|
1627
|
+
console.log(JSON.stringify(transformDecision(decision), null, 2));
|
|
1628
|
+
}
|
|
1629
|
+
function outputDecisionPretty(decision) {
|
|
1630
|
+
const color = getDecisionStatusColor(decision.status);
|
|
1631
|
+
const kind = decision.status === "OPEN" ? chalk3.yellow(" [open question]") : "";
|
|
1632
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
1633
|
+
console.log(
|
|
1634
|
+
`
|
|
1635
|
+
${chalk3.gray(decision.label)} ${chalk3.bold(decision.statement)}${kind}`
|
|
1636
|
+
);
|
|
1637
|
+
console.log(chalk3.gray(` ID: ${decision.id}`));
|
|
1638
|
+
console.log(` ${chalk3.gray("Status:")} ${chalk3[color](decision.status)}`);
|
|
1639
|
+
console.log(` ${chalk3.gray("Source:")} ${decision.source}`);
|
|
1640
|
+
if (decision.reviewState && decision.reviewState !== "CONFIRMED") {
|
|
1641
|
+
console.log(` ${chalk3.yellow(decision.reviewState)}`);
|
|
1642
|
+
}
|
|
1643
|
+
if (decision.decidedAt) {
|
|
1644
|
+
console.log(
|
|
1645
|
+
` ${chalk3.gray("Decided:")} ${new Date(decision.decidedAt).toLocaleString()}`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
if (decision.transcriptionSession) {
|
|
1649
|
+
console.log(
|
|
1650
|
+
` ${chalk3.gray("Meeting:")} ${decision.transcriptionSession.title ?? "Untitled"} (${decision.transcriptionSession.id})`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
if (decision.product) {
|
|
1654
|
+
console.log(` ${chalk3.gray("Product:")} ${decision.product.name}`);
|
|
1655
|
+
}
|
|
1656
|
+
if (decision.project) {
|
|
1657
|
+
console.log(` ${chalk3.gray("Project:")} ${decision.project.name}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (decision.owner) {
|
|
1660
|
+
console.log(` ${chalk3.gray("Owner:")} ${decision.owner.name ?? decision.owner.id}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (decision.deciders && decision.deciders.length > 0) {
|
|
1663
|
+
const people = decision.deciders.map((d) => d.email ? `${d.name} <${d.email}>` : d.name).join(", ");
|
|
1664
|
+
console.log(` ${chalk3.gray("Deciders:")} ${people}`);
|
|
1665
|
+
}
|
|
1666
|
+
if (decision.supersededBy) {
|
|
1667
|
+
console.log(
|
|
1668
|
+
` ${chalk3.gray("Superseded by:")} D-${decision.supersededBy.number} ${decision.supersededBy.statement}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
if (decision.supersedes && decision.supersedes.length > 0) {
|
|
1672
|
+
for (const prior of decision.supersedes) {
|
|
1673
|
+
console.log(
|
|
1674
|
+
` ${chalk3.gray("Supersedes:")} D-${prior.number} ${prior.statement}`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (decision.links && decision.links.length > 0) {
|
|
1679
|
+
console.log(` ${chalk3.gray("Implemented by:")}`);
|
|
1680
|
+
for (const link of decision.links) {
|
|
1681
|
+
const target = link.ticket ? `${link.ticket.shortId ?? `#${link.ticket.number}`} ${link.ticket.title}` : link.feature ? `feature: ${link.feature.name}` : "(unknown)";
|
|
1682
|
+
console.log(chalk3.gray(` ${target} [link ${link.id}]`));
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (decision.evidence && decision.evidence.length > 0) {
|
|
1686
|
+
console.log(`
|
|
1687
|
+
${chalk3.bold("Evidence")}`);
|
|
1688
|
+
for (const turn of decision.evidence) {
|
|
1689
|
+
const who = turn.speaker ? `${turn.speaker}: ` : "";
|
|
1690
|
+
console.log(chalk3.gray(` [${turn.turnIndex}] ${who}${turn.text}`));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (decision.body) {
|
|
1694
|
+
console.log(`
|
|
1695
|
+
${decision.body}`);
|
|
1696
|
+
}
|
|
1697
|
+
console.log();
|
|
1698
|
+
}
|
|
1699
|
+
function outputDecisionsJson(decisions, extra = {}) {
|
|
1700
|
+
console.log(
|
|
1701
|
+
JSON.stringify(
|
|
1702
|
+
{
|
|
1703
|
+
decisions: decisions.map(transformDecisionListRow),
|
|
1704
|
+
total: decisions.length,
|
|
1705
|
+
...extra
|
|
1706
|
+
},
|
|
1707
|
+
null,
|
|
1708
|
+
2
|
|
1709
|
+
)
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
function outputDecisionsPretty(decisions) {
|
|
1713
|
+
if (decisions.length === 0) {
|
|
1714
|
+
console.log(chalk3.gray("No decisions found."));
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const openCount = decisions.filter((d) => d.status === "OPEN").length;
|
|
1718
|
+
console.log(
|
|
1719
|
+
chalk3.bold(
|
|
1720
|
+
`
|
|
1721
|
+
Decisions (${decisions.length} total, ${openCount} open question${openCount === 1 ? "" : "s"})`
|
|
1722
|
+
)
|
|
1723
|
+
);
|
|
1724
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
1725
|
+
for (const d of decisions) {
|
|
1726
|
+
const color = getDecisionStatusColor(d.status);
|
|
1727
|
+
const draft = d.reviewState && d.reviewState !== "CONFIRMED" ? chalk3.yellow(` [${d.reviewState.toLowerCase()}]`) : "";
|
|
1728
|
+
console.log(
|
|
1729
|
+
` ${chalk3.gray(d.label)} ${chalk3.bold(d.statement)}${draft} ${chalk3[color](`[${d.status}]`)}`
|
|
1730
|
+
);
|
|
1731
|
+
console.log(chalk3.gray(` ID: ${d.id}`));
|
|
1732
|
+
const context = [
|
|
1733
|
+
d.product ? `Product: ${d.product.name}` : null,
|
|
1734
|
+
d.transcriptionSession ? `Meeting: ${d.transcriptionSession.title ?? d.transcriptionSession.id}` : null,
|
|
1735
|
+
d.decidedAt ? `Decided ${new Date(d.decidedAt).toLocaleDateString()}` : null,
|
|
1736
|
+
d.evidenceCount > 0 ? `${d.evidenceCount} evidence` : null,
|
|
1737
|
+
d._count?.links ? `${d._count.links} linked` : null
|
|
1738
|
+
].filter(Boolean);
|
|
1739
|
+
if (context.length > 0) {
|
|
1740
|
+
console.log(chalk3.gray(` ${context.join(" \xB7 ")}`));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
console.log();
|
|
1744
|
+
}
|
|
1745
|
+
function outputDecisionBatchJson(results) {
|
|
1746
|
+
console.log(
|
|
1747
|
+
JSON.stringify(
|
|
1748
|
+
{
|
|
1749
|
+
results: results.map((r) => ({
|
|
1750
|
+
index: r.index,
|
|
1751
|
+
success: r.success,
|
|
1752
|
+
statement: r.statement ?? r.decision?.statement ?? null,
|
|
1753
|
+
decision: r.decision ? transformDecision(r.decision) : null,
|
|
1754
|
+
error: r.error ?? null
|
|
1755
|
+
})),
|
|
1756
|
+
total: results.length,
|
|
1757
|
+
succeeded: results.filter((r) => r.success).length,
|
|
1758
|
+
failed: results.filter((r) => !r.success).length
|
|
1759
|
+
},
|
|
1760
|
+
null,
|
|
1761
|
+
2
|
|
1762
|
+
)
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
function outputDecisionBatchPretty(results) {
|
|
1766
|
+
for (const r of results) {
|
|
1767
|
+
const statement = r.statement ?? r.decision?.statement ?? "";
|
|
1768
|
+
if (r.success) {
|
|
1769
|
+
console.log(
|
|
1770
|
+
` ${chalk3.green("\u2713")} #${r.index}: ${chalk3.gray(r.decision?.label ?? "")} ${statement}`
|
|
1771
|
+
);
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(` ${chalk3.red("\u2717")} #${r.index}: ${statement} \u2014 ${r.error ?? "failed"}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const ok = results.filter((r) => r.success).length;
|
|
1777
|
+
console.log(chalk3.bold(`
|
|
1778
|
+
${ok}/${results.length} decisions logged`));
|
|
1779
|
+
}
|
|
1512
1780
|
function outputRequirementJson(requirement) {
|
|
1513
1781
|
console.log(JSON.stringify(requirement, null, 2));
|
|
1514
1782
|
}
|
|
@@ -1990,6 +2258,219 @@ ${chalk3.bold(project.name)}`);
|
|
|
1990
2258
|
}
|
|
1991
2259
|
console.log();
|
|
1992
2260
|
}
|
|
2261
|
+
function formatMinutes(totalMins) {
|
|
2262
|
+
if (totalMins <= 0) return "0m";
|
|
2263
|
+
const h = Math.floor(totalMins / 60);
|
|
2264
|
+
const m = totalMins % 60;
|
|
2265
|
+
if (h === 0) return `${m}m`;
|
|
2266
|
+
if (m === 0) return `${h}h`;
|
|
2267
|
+
return `${h}h ${m}m`;
|
|
2268
|
+
}
|
|
2269
|
+
function entryMinutes(entry) {
|
|
2270
|
+
const end = entry.endedAt ? new Date(entry.endedAt) : /* @__PURE__ */ new Date();
|
|
2271
|
+
const ms = end.getTime() - new Date(entry.startedAt).getTime();
|
|
2272
|
+
return ms <= 0 ? 0 : Math.round(ms / 6e4);
|
|
2273
|
+
}
|
|
2274
|
+
function clock(date) {
|
|
2275
|
+
const d = new Date(date);
|
|
2276
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2277
|
+
}
|
|
2278
|
+
function transformTimeEntry(entry) {
|
|
2279
|
+
return {
|
|
2280
|
+
id: entry.id,
|
|
2281
|
+
userId: entry.userId,
|
|
2282
|
+
actionId: entry.actionId,
|
|
2283
|
+
actionName: entry.action?.name ?? null,
|
|
2284
|
+
projectId: entry.action?.projectId ?? null,
|
|
2285
|
+
workspaceId: entry.workspaceId,
|
|
2286
|
+
startedAt: entry.startedAt,
|
|
2287
|
+
endedAt: entry.endedAt,
|
|
2288
|
+
minutes: entryMinutes(entry),
|
|
2289
|
+
status: entry.status,
|
|
2290
|
+
source: entry.source,
|
|
2291
|
+
sourceRef: entry.sourceRef,
|
|
2292
|
+
note: entry.note,
|
|
2293
|
+
createdByAgentId: entry.createdByAgentId
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function statusBadge(status) {
|
|
2297
|
+
return status === "PROPOSED" ? chalk3.yellow("[PROPOSED]") : chalk3.green("[CONFIRMED]");
|
|
2298
|
+
}
|
|
2299
|
+
function outputTimeLogJson(result) {
|
|
2300
|
+
console.log(
|
|
2301
|
+
JSON.stringify(
|
|
2302
|
+
{
|
|
2303
|
+
outcome: result.outcome,
|
|
2304
|
+
entry: result.entry ? transformTimeEntry(result.entry) : null,
|
|
2305
|
+
pieces: (result.pieces ?? []).map(transformTimeEntry),
|
|
2306
|
+
mergedInto: result.mergedInto ?? []
|
|
2307
|
+
},
|
|
2308
|
+
null,
|
|
2309
|
+
2
|
|
2310
|
+
)
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
function outputTimeLogPretty(result) {
|
|
2314
|
+
if (result.outcome === "merged") {
|
|
2315
|
+
console.log(
|
|
2316
|
+
`${chalk3.green("\u2713")} Merged into your manual time (${(result.mergedInto ?? []).length} entr${(result.mergedInto ?? []).length === 1 ? "y" : "ies"} annotated); nothing new written.`
|
|
2317
|
+
);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (result.outcome === "dropped" || !result.entry) {
|
|
2321
|
+
console.log(`${chalk3.yellow("\u2013")} Dropped: your manual time already covers every minute of it.`);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
const pieces = result.pieces && result.pieces.length > 0 ? result.pieces : [result.entry];
|
|
2325
|
+
const verb = { created: "Logged", updated: "Updated", left: "Left untouched (already confirmed)" }[result.outcome] ?? result.outcome;
|
|
2326
|
+
for (const e of pieces) {
|
|
2327
|
+
console.log(
|
|
2328
|
+
`${chalk3.green("\u2713")} ${verb}: ${clock(e.startedAt)}\u2013${e.endedAt ? clock(e.endedAt) : "now"} ${chalk3.bold(formatMinutes(entryMinutes(e)))} ${statusBadge(e.status)} ${e.action?.name ?? e.actionId}`
|
|
2329
|
+
);
|
|
2330
|
+
if (e.note) console.log(chalk3.gray(` ${e.note}`));
|
|
2331
|
+
console.log(chalk3.gray(` ID: ${e.id}${e.sourceRef ? ` ref: ${e.sourceRef}` : ""} owner: ${e.userId}`));
|
|
2332
|
+
}
|
|
2333
|
+
if (pieces.length > 1) console.log(chalk3.gray(` split into ${pieces.length} pieces around your manual time`));
|
|
2334
|
+
}
|
|
2335
|
+
function outputTimeConfirmJson(result, date) {
|
|
2336
|
+
console.log(JSON.stringify({ date, confirmed: result.confirmed }, null, 2));
|
|
2337
|
+
}
|
|
2338
|
+
function outputTimeConfirmPretty(result, date) {
|
|
2339
|
+
if (result.confirmed === 0) {
|
|
2340
|
+
console.log(chalk3.gray(`Nothing proposed on ${date}; nothing to confirm.`));
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
console.log(`${chalk3.green("\u2713")} Confirmed ${result.confirmed} ${result.confirmed === 1 ? "entry" : "entries"} on ${date}.`);
|
|
2344
|
+
}
|
|
2345
|
+
function outputTimeEntriesJson(entries, date) {
|
|
2346
|
+
const rows = entries.map(transformTimeEntry);
|
|
2347
|
+
console.log(
|
|
2348
|
+
JSON.stringify(
|
|
2349
|
+
{
|
|
2350
|
+
date,
|
|
2351
|
+
entries: rows,
|
|
2352
|
+
total: rows.length,
|
|
2353
|
+
totalMinutes: rows.reduce((acc, r) => acc + r.minutes, 0),
|
|
2354
|
+
proposed: rows.filter((r) => r.status === "PROPOSED").length
|
|
2355
|
+
},
|
|
2356
|
+
null,
|
|
2357
|
+
2
|
|
2358
|
+
)
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
function outputTimeEntriesPretty(entries, date) {
|
|
2362
|
+
if (entries.length === 0) {
|
|
2363
|
+
console.log(chalk3.gray(`No time entries on ${date}.`));
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
console.log(chalk3.bold(`
|
|
2367
|
+
Time on ${date}
|
|
2368
|
+
`));
|
|
2369
|
+
let total = 0;
|
|
2370
|
+
for (const e of entries) {
|
|
2371
|
+
const mins = entryMinutes(e);
|
|
2372
|
+
total += mins;
|
|
2373
|
+
const range = `${clock(e.startedAt)}\u2013${e.endedAt ? clock(e.endedAt) : "now"}`;
|
|
2374
|
+
console.log(
|
|
2375
|
+
` ${range} ${chalk3.bold(formatMinutes(mins).padStart(6))} ${statusBadge(e.status)} ${e.action?.name ?? e.actionId}${e.note ? chalk3.gray(` \xB7 ${e.note}`) : ""}`
|
|
2376
|
+
);
|
|
2377
|
+
console.log(chalk3.gray(` ${e.id} ${e.source}${e.sourceRef ? ` ${e.sourceRef}` : ""}`));
|
|
2378
|
+
}
|
|
2379
|
+
const proposed = entries.filter((e) => e.status === "PROPOSED").length;
|
|
2380
|
+
console.log(
|
|
2381
|
+
chalk3.bold(`
|
|
2382
|
+
${formatMinutes(total)} across ${entries.length} entries`) + (proposed > 0 ? chalk3.yellow(`, ${proposed} proposed`) : "")
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
function outputTimeBatchJson(results) {
|
|
2386
|
+
console.log(
|
|
2387
|
+
JSON.stringify(
|
|
2388
|
+
{
|
|
2389
|
+
results: results.map((r) => ({
|
|
2390
|
+
index: r.index,
|
|
2391
|
+
success: r.success,
|
|
2392
|
+
outcome: r.outcome ?? null,
|
|
2393
|
+
sourceRef: r.sourceRef ?? r.entry?.sourceRef ?? null,
|
|
2394
|
+
entry: r.entry ? transformTimeEntry(r.entry) : null,
|
|
2395
|
+
error: r.error ?? null
|
|
2396
|
+
})),
|
|
2397
|
+
total: results.length,
|
|
2398
|
+
succeeded: results.filter((r) => r.success).length,
|
|
2399
|
+
failed: results.filter((r) => !r.success).length
|
|
2400
|
+
},
|
|
2401
|
+
null,
|
|
2402
|
+
2
|
|
2403
|
+
)
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
function outputTimeBatchPretty(results) {
|
|
2407
|
+
for (const r of results) {
|
|
2408
|
+
const ref = r.sourceRef ?? r.entry?.sourceRef ?? "";
|
|
2409
|
+
if (r.success && r.entry) {
|
|
2410
|
+
console.log(
|
|
2411
|
+
` ${chalk3.green("\u2713")} #${r.index}: ${chalk3.gray(r.outcome ?? "")} ${clock(r.entry.startedAt)}\u2013${r.entry.endedAt ? clock(r.entry.endedAt) : "now"} ${r.entry.action?.name ?? r.entry.actionId}${ref ? chalk3.gray(` ${ref}`) : ""}`
|
|
2412
|
+
);
|
|
2413
|
+
} else {
|
|
2414
|
+
console.log(` ${chalk3.red("\u2717")} #${r.index}: ${ref} \u2014 ${r.error ?? "failed"}`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const ok = results.filter((r) => r.success).length;
|
|
2418
|
+
console.log(chalk3.bold(`
|
|
2419
|
+
${ok}/${results.length} entries logged`));
|
|
2420
|
+
}
|
|
2421
|
+
function outputDayReportJson(report, date) {
|
|
2422
|
+
console.log(
|
|
2423
|
+
JSON.stringify(
|
|
2424
|
+
{
|
|
2425
|
+
date,
|
|
2426
|
+
attentionMinutes: report.attentionMinutes,
|
|
2427
|
+
sessionMinutes: report.sessionMinutes,
|
|
2428
|
+
agentRunMinutes: report.agentRunMinutes,
|
|
2429
|
+
unassignedCount: report.unassignedCount,
|
|
2430
|
+
proposedCount: report.proposedCount,
|
|
2431
|
+
byProduct: report.byProduct,
|
|
2432
|
+
byAction: report.byAction,
|
|
2433
|
+
flags: report.flags,
|
|
2434
|
+
entries: report.entries.map((e) => ({
|
|
2435
|
+
...transformTimeEntry(e),
|
|
2436
|
+
productId: e.productId,
|
|
2437
|
+
productName: e.productName,
|
|
2438
|
+
isAgentRun: e.isAgentRun,
|
|
2439
|
+
flags: e.flags,
|
|
2440
|
+
ticket: e.action.ticket ? { id: e.action.ticket.id, number: e.action.ticket.number, shortId: e.action.ticket.shortId } : null
|
|
2441
|
+
}))
|
|
2442
|
+
},
|
|
2443
|
+
null,
|
|
2444
|
+
2
|
|
2445
|
+
)
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
function outputDayReportPretty(report, date) {
|
|
2449
|
+
console.log(chalk3.bold(`
|
|
2450
|
+
${date}`));
|
|
2451
|
+
console.log(
|
|
2452
|
+
` ${chalk3.bold(formatMinutes(report.attentionMinutes))} attention \xB7 ${formatMinutes(report.sessionMinutes)} session \xB7 ${formatMinutes(report.agentRunMinutes)} agent-run` + (report.proposedCount > 0 ? chalk3.yellow(` \xB7 ${report.proposedCount} proposed`) : "") + (report.unassignedCount > 0 ? chalk3.yellow(` \xB7 ${report.unassignedCount} unassigned`) : "")
|
|
2453
|
+
);
|
|
2454
|
+
if (report.entries.length === 0) {
|
|
2455
|
+
console.log(chalk3.gray(" No time recorded."));
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
console.log(chalk3.bold("\nBy product"));
|
|
2459
|
+
for (const row of report.byProduct) {
|
|
2460
|
+
console.log(` ${formatMinutes(row.minutes).padStart(7)} ${row.productId ? row.name : chalk3.yellow(row.name)}`);
|
|
2461
|
+
}
|
|
2462
|
+
console.log(chalk3.bold("\nBy action"));
|
|
2463
|
+
for (const row of report.byAction) {
|
|
2464
|
+
const ticket = row.ticket ? chalk3.gray(` ${row.ticket.shortId ?? `#${row.ticket.number}`}`) : row.projectId ? chalk3.gray(` \xB7 ${row.projectName ?? "project"}`) : chalk3.yellow(" [unassigned]");
|
|
2465
|
+
const agent = row.agentRunMinutes > 0 ? chalk3.gray(` (+${formatMinutes(row.agentRunMinutes)} agent-run)`) : "";
|
|
2466
|
+
const proposed = row.proposedCount > 0 ? chalk3.yellow(` ${row.proposedCount} proposed`) : "";
|
|
2467
|
+
console.log(` ${formatMinutes(row.minutes).padStart(7)} ${row.name}${ticket}${agent}${proposed}`);
|
|
2468
|
+
}
|
|
2469
|
+
if (report.flags.length > 0) {
|
|
2470
|
+
console.log(chalk3.yellow(`
|
|
2471
|
+
${report.flags.length} manual entr${report.flags.length === 1 ? "y looks" : "ies look"} like a forgotten timer`));
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
1993
2474
|
|
|
1994
2475
|
// src/commands/actions.ts
|
|
1995
2476
|
var VALID_PRIORITIES = [
|
|
@@ -2242,6 +2723,35 @@ function createActionsCommand() {
|
|
|
2242
2723
|
handleError(error, useJson);
|
|
2243
2724
|
}
|
|
2244
2725
|
});
|
|
2726
|
+
actions.command("upsert").description(
|
|
2727
|
+
"One Action per external source: find by (--workspace, --source-type, --source-id), refresh its title and links, else create it. The Daily worklog uses --source-type claude-session --source-id <conversation id>."
|
|
2728
|
+
).requiredOption("--source-type <type>", "e.g. claude-session").requiredOption("--source-id <id>", "The source's own id; the same pair always maps to the same Action").requiredOption("-t, --title <title>", "Action title (refreshed on every call)").option("-w, --workspace <slug|id>", "Workspace (defaults to your default workspace)").option("-d, --description <text>", "Description").option("--project <id>", "Link to a project CUID in the same workspace").option("--ticket <id>", "Link to a ticket CUID whose product is in the same workspace").action(
|
|
2729
|
+
async (options, cmd) => {
|
|
2730
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2731
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2732
|
+
try {
|
|
2733
|
+
const client = getClient();
|
|
2734
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2735
|
+
const result = await client.actions.upsertBySource({
|
|
2736
|
+
sourceType: options.sourceType,
|
|
2737
|
+
sourceId: options.sourceId,
|
|
2738
|
+
name: options.title,
|
|
2739
|
+
workspaceId,
|
|
2740
|
+
description: options.description,
|
|
2741
|
+
projectId: options.project,
|
|
2742
|
+
ticketId: options.ticket
|
|
2743
|
+
});
|
|
2744
|
+
if (useJson) {
|
|
2745
|
+
outputActionUpsertJson(result);
|
|
2746
|
+
} else {
|
|
2747
|
+
console.log(result.outcome === "created" ? "Created action:" : "Updated action:");
|
|
2748
|
+
outputActionPretty(result.action);
|
|
2749
|
+
}
|
|
2750
|
+
} catch (error) {
|
|
2751
|
+
handleError(error);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
);
|
|
2245
2755
|
const comment = new Command2("comment").description("Manage comments on an action");
|
|
2246
2756
|
comment.command("list").description("List comments on an action").requiredOption("--id <id>", "Action ID").action(async (options, cmd) => {
|
|
2247
2757
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -2432,79 +2942,715 @@ function createContactsCommand() {
|
|
|
2432
2942
|
handleError(error, useJson);
|
|
2433
2943
|
}
|
|
2434
2944
|
});
|
|
2435
|
-
contacts.command("delete").description("Delete a contact").argument("<id>", "Contact ID").action(async (id, _options, cmd) => {
|
|
2945
|
+
contacts.command("delete").description("Delete a contact").argument("<id>", "Contact ID").action(async (id, _options, cmd) => {
|
|
2946
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2947
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2948
|
+
try {
|
|
2949
|
+
const client = getClient();
|
|
2950
|
+
await client.contacts.delete(id);
|
|
2951
|
+
if (useJson) {
|
|
2952
|
+
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
2953
|
+
} else {
|
|
2954
|
+
console.log(`
|
|
2955
|
+
\u2713 Contact ${id} deleted`);
|
|
2956
|
+
}
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
handleError(error, useJson);
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
contacts.command("add-interaction").description("Add an interaction to a contact").requiredOption("--contact <id>", "Contact ID").requiredOption("--type <type>", `Interaction type (${VALID_INTERACTION_TYPES.join(", ")})`).requiredOption("--direction <dir>", `Direction (${VALID_INTERACTION_DIRECTIONS.join(", ")})`).option("--subject <text>", "Subject line").option("--notes <text>", "Notes").action(async (options, cmd) => {
|
|
2962
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2963
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2964
|
+
try {
|
|
2965
|
+
if (!VALID_INTERACTION_TYPES.includes(options.type)) {
|
|
2966
|
+
throw new Error(`Invalid interaction type "${options.type}". Valid: ${VALID_INTERACTION_TYPES.join(", ")}`);
|
|
2967
|
+
}
|
|
2968
|
+
if (!VALID_INTERACTION_DIRECTIONS.includes(options.direction)) {
|
|
2969
|
+
throw new Error(`Invalid direction "${options.direction}". Valid: ${VALID_INTERACTION_DIRECTIONS.join(", ")}`);
|
|
2970
|
+
}
|
|
2971
|
+
const client = getClient();
|
|
2972
|
+
const interaction = await client.contacts.addInteraction({
|
|
2973
|
+
contactId: options.contact,
|
|
2974
|
+
type: options.type,
|
|
2975
|
+
direction: options.direction,
|
|
2976
|
+
subject: options.subject,
|
|
2977
|
+
notes: options.notes
|
|
2978
|
+
});
|
|
2979
|
+
if (useJson) {
|
|
2980
|
+
outputInteractionJson(interaction);
|
|
2981
|
+
} else {
|
|
2982
|
+
console.log("\n\u2713 Interaction added");
|
|
2983
|
+
outputInteractionPretty(interaction);
|
|
2984
|
+
}
|
|
2985
|
+
} catch (error) {
|
|
2986
|
+
handleError(error, useJson);
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2989
|
+
contacts.command("enrich").description("Queue a web-search enrichment job for an existing contact (runs regardless of the workspace auto-enrich setting)").argument("<id>", "Contact ID").action(async (id, _options, cmd) => {
|
|
2990
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2991
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2992
|
+
try {
|
|
2993
|
+
const client = getClient();
|
|
2994
|
+
const result = await client.contacts.enrich(id);
|
|
2995
|
+
if (useJson) {
|
|
2996
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2997
|
+
} else if (result.enqueued) {
|
|
2998
|
+
console.log(`
|
|
2999
|
+
\u2713 Enrichment queued for contact ${id} (job ${result.enrichmentId})`);
|
|
3000
|
+
} else if (result.reason === "already-in-flight") {
|
|
3001
|
+
console.log(`
|
|
3002
|
+
\u2022 Enrichment already in progress for contact ${id} (job ${result.enrichmentId})`);
|
|
3003
|
+
} else {
|
|
3004
|
+
console.log(`
|
|
3005
|
+
\u2022 Not queued for contact ${id}${result.reason ? ` (${result.reason})` : ""}`);
|
|
3006
|
+
}
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
handleError(error, useJson);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
return contacts;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/commands/decisions.ts
|
|
3015
|
+
import { Command as Command4 } from "commander";
|
|
3016
|
+
import chalk4 from "chalk";
|
|
3017
|
+
|
|
3018
|
+
// src/utils/input.ts
|
|
3019
|
+
import { readFileSync } from "fs";
|
|
3020
|
+
var stdinConsumed = false;
|
|
3021
|
+
function readText(inline, file) {
|
|
3022
|
+
if (inline !== void 0) return inline;
|
|
3023
|
+
if (file === void 0) return void 0;
|
|
3024
|
+
if (file === "-") {
|
|
3025
|
+
if (stdinConsumed) {
|
|
3026
|
+
throw new Error('stdin ("-") can only back one option per invocation.');
|
|
3027
|
+
}
|
|
3028
|
+
stdinConsumed = true;
|
|
3029
|
+
return readFileSync(0, "utf-8");
|
|
3030
|
+
}
|
|
3031
|
+
return readFileSync(file, "utf-8");
|
|
3032
|
+
}
|
|
3033
|
+
function parseDate(value) {
|
|
3034
|
+
if (value === void 0 || value === "") return void 0;
|
|
3035
|
+
const date = /^\d{4}-\d{2}-\d{2}$/.test(value) ? /* @__PURE__ */ new Date(`${value}T00:00:00`) : new Date(value);
|
|
3036
|
+
if (Number.isNaN(date.getTime())) {
|
|
3037
|
+
throw new Error(
|
|
3038
|
+
`Invalid date: "${value}". Use ISO format, e.g. 2026-08-25 or 2026-08-25T14:00.`
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
return date;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// src/commands/decisions.ts
|
|
3045
|
+
var STATUSES = [
|
|
3046
|
+
"OPEN",
|
|
3047
|
+
"PROPOSED",
|
|
3048
|
+
"ACCEPTED",
|
|
3049
|
+
"SUPERSEDED",
|
|
3050
|
+
"DEPRECATED"
|
|
3051
|
+
];
|
|
3052
|
+
var SOURCES = ["MEETING", "MANUAL", "AGENT"];
|
|
3053
|
+
var OPEN_QUESTION_NOTE = `An OPEN QUESTION is a decision with --status OPEN \u2014 there is no separate model, and that is exactly what a meeting's "Open questions" panel shows.`;
|
|
3054
|
+
function parseStatus(value) {
|
|
3055
|
+
const upper = value.toUpperCase();
|
|
3056
|
+
if (!STATUSES.includes(upper)) {
|
|
3057
|
+
throw new Error(
|
|
3058
|
+
`Invalid status "${value}". Use one of: ${STATUSES.join(", ")}. ${OPEN_QUESTION_NOTE}`
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
return upper;
|
|
3062
|
+
}
|
|
3063
|
+
function parseSource(value) {
|
|
3064
|
+
const upper = value.toUpperCase();
|
|
3065
|
+
if (!SOURCES.includes(upper)) {
|
|
3066
|
+
throw new Error(
|
|
3067
|
+
`Invalid source "${value}". Use one of: ${SOURCES.join(", ")}.`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
return upper;
|
|
3071
|
+
}
|
|
3072
|
+
function parseDecider(spec) {
|
|
3073
|
+
const angled = /^\s*(.*?)\s*<\s*([^>]+?)\s*>\s*$/.exec(spec);
|
|
3074
|
+
if (angled) {
|
|
3075
|
+
const name = angled[1]?.trim() ?? "";
|
|
3076
|
+
const email = angled[2].trim();
|
|
3077
|
+
return { name: name === "" ? email : name, email };
|
|
3078
|
+
}
|
|
3079
|
+
const bare = spec.trim();
|
|
3080
|
+
if (bare === "") {
|
|
3081
|
+
throw new Error('Empty --decider. Use "Name <email>", a name, or an email.');
|
|
3082
|
+
}
|
|
3083
|
+
if (bare.includes("@") && !bare.includes(" ")) {
|
|
3084
|
+
return { name: bare, email: bare };
|
|
3085
|
+
}
|
|
3086
|
+
return { name: bare };
|
|
3087
|
+
}
|
|
3088
|
+
function parseEvidence(raw) {
|
|
3089
|
+
let parsed;
|
|
3090
|
+
try {
|
|
3091
|
+
parsed = JSON.parse(raw);
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
throw new Error(
|
|
3094
|
+
`Evidence must be JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
if (!Array.isArray(parsed)) {
|
|
3098
|
+
throw new Error(
|
|
3099
|
+
"Evidence must be a JSON array of {turnIndex, speaker?, startTime?, text} objects."
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
return parsed.map((turn, i) => {
|
|
3103
|
+
if (typeof turn !== "object" || turn === null) {
|
|
3104
|
+
throw new Error(`Evidence[${i}] is not an object.`);
|
|
3105
|
+
}
|
|
3106
|
+
const t = turn;
|
|
3107
|
+
if (typeof t.turnIndex !== "number" || typeof t.text !== "string") {
|
|
3108
|
+
throw new Error(
|
|
3109
|
+
`Evidence[${i}] needs a numeric "turnIndex" and a string "text".`
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
return {
|
|
3113
|
+
turnIndex: t.turnIndex,
|
|
3114
|
+
speaker: typeof t.speaker === "string" ? t.speaker : null,
|
|
3115
|
+
startTime: typeof t.startTime === "number" ? t.startTime : null,
|
|
3116
|
+
text: t.text
|
|
3117
|
+
};
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
async function resolveProductFilter(client, workspaceId, value) {
|
|
3121
|
+
if (value === void 0) return void 0;
|
|
3122
|
+
if (value === "workspace") return "workspace";
|
|
3123
|
+
return await resolveProductId(client, workspaceId, value);
|
|
3124
|
+
}
|
|
3125
|
+
function parsePositiveInt(v, flag) {
|
|
3126
|
+
if (v === void 0) return void 0;
|
|
3127
|
+
const n = Number.parseInt(v, 10);
|
|
3128
|
+
if (Number.isNaN(n) || n < 1) {
|
|
3129
|
+
throw new Error(`${flag} must be a positive integer, got "${v}".`);
|
|
3130
|
+
}
|
|
3131
|
+
return n;
|
|
3132
|
+
}
|
|
3133
|
+
function parseNullableString(v) {
|
|
3134
|
+
if (v === void 0) return void 0;
|
|
3135
|
+
return v === "null" ? null : v;
|
|
3136
|
+
}
|
|
3137
|
+
function parseNullableInt(v) {
|
|
3138
|
+
if (v === void 0) return void 0;
|
|
3139
|
+
if (v === "null") return null;
|
|
3140
|
+
const n = Number.parseInt(v, 10);
|
|
3141
|
+
if (Number.isNaN(n)) throw new Error(`Expected a number, got "${v}".`);
|
|
3142
|
+
return n;
|
|
3143
|
+
}
|
|
3144
|
+
var BATCH_ITEM_KEYS = /* @__PURE__ */ new Set([
|
|
3145
|
+
"statement",
|
|
3146
|
+
"body",
|
|
3147
|
+
"status",
|
|
3148
|
+
"source",
|
|
3149
|
+
"meeting",
|
|
3150
|
+
"transcriptionSessionId",
|
|
3151
|
+
"productId",
|
|
3152
|
+
"projectId",
|
|
3153
|
+
"goalId",
|
|
3154
|
+
"keyResultId",
|
|
3155
|
+
"occurrenceId",
|
|
3156
|
+
"decidedAt",
|
|
3157
|
+
"ownerId",
|
|
3158
|
+
"deciders",
|
|
3159
|
+
"evidence"
|
|
3160
|
+
]);
|
|
3161
|
+
function parseBatchFile(raw) {
|
|
3162
|
+
let parsed;
|
|
3163
|
+
try {
|
|
3164
|
+
parsed = JSON.parse(raw);
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
throw new Error(
|
|
3167
|
+
`--from-file must be JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const items = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.decisions) ? parsed.decisions : null;
|
|
3171
|
+
if (!items) {
|
|
3172
|
+
throw new Error(
|
|
3173
|
+
'--from-file must contain a JSON array of decisions, or {"decisions": [...]}.'
|
|
3174
|
+
);
|
|
3175
|
+
}
|
|
3176
|
+
if (items.length === 0) {
|
|
3177
|
+
throw new Error("--from-file contained no decisions.");
|
|
3178
|
+
}
|
|
3179
|
+
return items.map((item, i) => {
|
|
3180
|
+
if (typeof item !== "object" || item === null) {
|
|
3181
|
+
throw new Error(`Decision[${i}] is not an object.`);
|
|
3182
|
+
}
|
|
3183
|
+
const entry = item;
|
|
3184
|
+
if (typeof entry.statement !== "string" || entry.statement.trim() === "") {
|
|
3185
|
+
throw new Error(`Decision[${i}] needs a non-empty "statement".`);
|
|
3186
|
+
}
|
|
3187
|
+
const unknown = Object.keys(entry).filter(
|
|
3188
|
+
(key) => !BATCH_ITEM_KEYS.has(key)
|
|
3189
|
+
);
|
|
3190
|
+
if (unknown.length > 0) {
|
|
3191
|
+
throw new Error(
|
|
3192
|
+
`Decision[${i}] has unknown field(s): ${unknown.join(", ")}. Accepted: ${[...BATCH_ITEM_KEYS].join(", ")}.`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
if (entry.goalId !== void 0 && typeof entry.goalId !== "number") {
|
|
3196
|
+
throw new Error(`Decision[${i}].goalId must be a number, not a string.`);
|
|
3197
|
+
}
|
|
3198
|
+
return entry;
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
function batchDeciders(value, index) {
|
|
3202
|
+
if (value === void 0 || value === null) return void 0;
|
|
3203
|
+
if (!Array.isArray(value)) {
|
|
3204
|
+
throw new Error(`Decision[${index}].deciders must be an array.`);
|
|
3205
|
+
}
|
|
3206
|
+
return value.map((d) => {
|
|
3207
|
+
if (typeof d === "string") return parseDecider(d);
|
|
3208
|
+
if (typeof d === "object" && d !== null && typeof d.name === "string") {
|
|
3209
|
+
const obj = d;
|
|
3210
|
+
return {
|
|
3211
|
+
name: obj.name,
|
|
3212
|
+
email: typeof obj.email === "string" ? obj.email : null,
|
|
3213
|
+
userId: typeof obj.userId === "string" ? obj.userId : null
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
throw new Error(
|
|
3217
|
+
`Decision[${index}].deciders entries must be "Name <email>" strings or {name, email?} objects.`
|
|
3218
|
+
);
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
function createDecisionsCommand() {
|
|
3222
|
+
const decisions = new Command4("decisions").description(
|
|
3223
|
+
[
|
|
3224
|
+
"Log and manage decisions and open questions (the workspace Decision Log).",
|
|
3225
|
+
"",
|
|
3226
|
+
OPEN_QUESTION_NOTE,
|
|
3227
|
+
"",
|
|
3228
|
+
"Statuses: OPEN (an open question) \xB7 PROPOSED \xB7 ACCEPTED \xB7 SUPERSEDED \xB7 DEPRECATED.",
|
|
3229
|
+
'SUPERSEDED and DEPRECATED are reached with "decisions status", never at creation.'
|
|
3230
|
+
].join("\n")
|
|
3231
|
+
);
|
|
3232
|
+
decisions.command("list").description(
|
|
3233
|
+
`List decisions, newest decided first. Pass --status OPEN for open questions only. ${OPEN_QUESTION_NOTE}`
|
|
3234
|
+
).option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").option(
|
|
3235
|
+
"--meeting <id>",
|
|
3236
|
+
"One meeting's decisions and open questions (includes drafts if you can edit it)"
|
|
3237
|
+
).option(
|
|
3238
|
+
"--status <status>",
|
|
3239
|
+
"Filter by status (repeatable): OPEN, PROPOSED, ACCEPTED, SUPERSEDED, DEPRECATED",
|
|
3240
|
+
(value, previous) => [...previous, parseStatus(value)],
|
|
3241
|
+
[]
|
|
3242
|
+
).option(
|
|
3243
|
+
"--source <source>",
|
|
3244
|
+
"Filter by source (repeatable): MEETING, MANUAL, AGENT",
|
|
3245
|
+
(value, previous) => [...previous, parseSource(value)],
|
|
3246
|
+
[]
|
|
3247
|
+
).option("--product <slug|id>", 'Filter by product, or "workspace" for decisions with no product').option("--include-workspace-wide", "With --product: also include decisions that have no product").option("--project <id>", "Filter by project CUID").option("--search <text>", "Free-text over statement and body").option("--number <n>", "One decision by its workspace sequence number (D-0003 is 3)").option("--limit <n>", "Cap the rows returned (1-500)").option("--adr <id>", "Decisions formalised as this ADR document").action(
|
|
3248
|
+
async (options, cmd) => {
|
|
3249
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3250
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3251
|
+
try {
|
|
3252
|
+
const client = getClient();
|
|
3253
|
+
if (options.meeting) {
|
|
3254
|
+
const result = await client.decisions.listForMeeting(options.meeting);
|
|
3255
|
+
let rows2 = result.decisions;
|
|
3256
|
+
if (options.status.length > 0) {
|
|
3257
|
+
rows2 = rows2.filter((d) => options.status.includes(d.status));
|
|
3258
|
+
}
|
|
3259
|
+
if (options.source.length > 0) {
|
|
3260
|
+
rows2 = rows2.filter((d) => options.source.includes(d.source));
|
|
3261
|
+
}
|
|
3262
|
+
if (useJson) {
|
|
3263
|
+
outputDecisionsJson(rows2, {
|
|
3264
|
+
meetingId: options.meeting,
|
|
3265
|
+
workspaceId: result.workspaceId,
|
|
3266
|
+
canLogDecision: result.canLogDecision
|
|
3267
|
+
});
|
|
3268
|
+
} else {
|
|
3269
|
+
outputDecisionsPretty(rows2);
|
|
3270
|
+
if (!result.canLogDecision) {
|
|
3271
|
+
console.log(
|
|
3272
|
+
chalk4.yellow(
|
|
3273
|
+
"You cannot log decisions on this meeting (no edit access, or it has no workspace)."
|
|
3274
|
+
)
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3281
|
+
const rows = options.adr ? await client.decisions.listForAdr(workspaceId, options.adr) : await client.decisions.list({
|
|
3282
|
+
workspaceId,
|
|
3283
|
+
statuses: options.status.length > 0 ? options.status : void 0,
|
|
3284
|
+
sources: options.source.length > 0 ? options.source : void 0,
|
|
3285
|
+
productId: await resolveProductFilter(client, workspaceId, options.product),
|
|
3286
|
+
includeWorkspaceWide: options.includeWorkspaceWide,
|
|
3287
|
+
projectId: options.project,
|
|
3288
|
+
search: options.search,
|
|
3289
|
+
number: parsePositiveInt(options.number, "--number"),
|
|
3290
|
+
limit: parsePositiveInt(options.limit, "--limit")
|
|
3291
|
+
});
|
|
3292
|
+
if (useJson) outputDecisionsJson(rows, { workspaceId });
|
|
3293
|
+
else outputDecisionsPretty(rows);
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
handleError(error, useJson);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
);
|
|
3299
|
+
decisions.command("get <id>").description("Show one decision in full: body, evidence, deciders, chain and links").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(async (id, options, cmd) => {
|
|
3300
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3301
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3302
|
+
try {
|
|
3303
|
+
const client = getClient();
|
|
3304
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3305
|
+
const decision = await client.decisions.get(workspaceId, id);
|
|
3306
|
+
if (useJson) outputDecisionJson(decision);
|
|
3307
|
+
else outputDecisionPretty(decision);
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
handleError(error, useJson);
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
decisions.command("create").description(
|
|
3313
|
+
[
|
|
3314
|
+
"Log a decision, or an open question with --status OPEN.",
|
|
3315
|
+
OPEN_QUESTION_NOTE,
|
|
3316
|
+
"Bodies are Markdown: use --body-file <path> (or - for stdin) rather than a shell argument.",
|
|
3317
|
+
"Bulk: --from-file <path|-> takes a JSON array of decisions; the other flags become its defaults."
|
|
3318
|
+
].join(" ")
|
|
3319
|
+
).option("-s, --statement <text>", "The decision, or the open question (max 500 chars)").option("-b, --body <markdown>", "Markdown detail: context, alternatives, consequences").option("--body-file <path>", 'Read the body from a file ("-" = stdin)').option("--status <status>", "OPEN (open question), PROPOSED (default) or ACCEPTED").option("--source <source>", "MEETING, MANUAL or AGENT").option("--meeting <id>", "Meeting (transcriptionSessionId) this came out of \u2014 needs edit access to it").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").option("--product <slug|id>", "Link to a product").option("--project <id>", "Link to a project CUID").option("--goal <id>", "Link to a goal (integer id)").option("--key-result <id>", "Link to a key result CUID").option("--occurrence <id>", "Link to a ceremony occurrence CUID").option("--decided-at <iso>", "When it was decided (e.g. 2026-09-10T14:00)").option("--owner <userId>", "Owner user ID").option(
|
|
3320
|
+
'--decider <"Name <email>">',
|
|
3321
|
+
"Someone who made the decision (repeatable); external people need no account",
|
|
3322
|
+
(value, previous) => [...previous, parseDecider(value)],
|
|
3323
|
+
[]
|
|
3324
|
+
).option(
|
|
3325
|
+
"--evidence-file <path>",
|
|
3326
|
+
'JSON array of {turnIndex, text} transcript quotes ("-" = stdin). Needs --meeting: the server checks every quote against that transcript and silently drops the ones that do not match.'
|
|
3327
|
+
).option("--from-file <path>", 'Bulk create from a JSON array of decisions ("-" = stdin)').action(
|
|
3328
|
+
async (options, cmd) => {
|
|
3329
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3330
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3331
|
+
try {
|
|
3332
|
+
if (!options.statement && !options.fromFile) {
|
|
3333
|
+
throw new Error(
|
|
3334
|
+
"Pass --statement <text>, or --from-file <path|-> for a bulk create."
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
if (options.statement && options.fromFile) {
|
|
3338
|
+
throw new Error(
|
|
3339
|
+
"--statement and --from-file are alternatives: the file carries the statements."
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
const client = getClient();
|
|
3343
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3344
|
+
const productId = options.product ? await resolveProductId(client, workspaceId, options.product) : void 0;
|
|
3345
|
+
const status = options.status ? parseStatus(options.status) : void 0;
|
|
3346
|
+
const source = options.source ? parseSource(options.source) : void 0;
|
|
3347
|
+
const decidedAt = parseDate(options.decidedAt);
|
|
3348
|
+
const goalId = options.goal ? Number.parseInt(options.goal, 10) : void 0;
|
|
3349
|
+
if (goalId !== void 0 && Number.isNaN(goalId)) {
|
|
3350
|
+
throw new Error(`--goal must be an integer id, got "${options.goal}".`);
|
|
3351
|
+
}
|
|
3352
|
+
const defaults = {
|
|
3353
|
+
workspaceId,
|
|
3354
|
+
status,
|
|
3355
|
+
source,
|
|
3356
|
+
transcriptionSessionId: options.meeting,
|
|
3357
|
+
productId,
|
|
3358
|
+
projectId: options.project,
|
|
3359
|
+
goalId,
|
|
3360
|
+
keyResultId: options.keyResult,
|
|
3361
|
+
occurrenceId: options.occurrence,
|
|
3362
|
+
decidedAt,
|
|
3363
|
+
ownerId: options.owner,
|
|
3364
|
+
deciders: options.decider.length > 0 ? options.decider : void 0
|
|
3365
|
+
};
|
|
3366
|
+
if (options.fromFile) {
|
|
3367
|
+
const perEntryOnly = [
|
|
3368
|
+
options.body !== void 0 ? "--body" : null,
|
|
3369
|
+
options.bodyFile !== void 0 ? "--body-file" : null,
|
|
3370
|
+
options.evidenceFile !== void 0 ? "--evidence-file" : null
|
|
3371
|
+
].filter((flag) => flag !== null);
|
|
3372
|
+
if (perEntryOnly.length > 0) {
|
|
3373
|
+
throw new Error(
|
|
3374
|
+
`${perEntryOnly.join(", ")} ${perEntryOnly.length === 1 ? "is" : "are"} per-decision content \u2014 put "body" and "evidence" on each entry in --from-file instead.`
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
const items = parseBatchFile(readText(void 0, options.fromFile));
|
|
3378
|
+
const results = [];
|
|
3379
|
+
for (const [index, item] of items.entries()) {
|
|
3380
|
+
const statement = String(item.statement);
|
|
3381
|
+
try {
|
|
3382
|
+
const input = {
|
|
3383
|
+
...defaults,
|
|
3384
|
+
statement,
|
|
3385
|
+
body: typeof item.body === "string" ? item.body : void 0,
|
|
3386
|
+
status: item.status !== void 0 ? parseStatus(String(item.status)) : defaults.status,
|
|
3387
|
+
source: item.source !== void 0 ? parseSource(String(item.source)) : defaults.source,
|
|
3388
|
+
transcriptionSessionId: typeof item.transcriptionSessionId === "string" ? item.transcriptionSessionId : typeof item.meeting === "string" ? item.meeting : defaults.transcriptionSessionId,
|
|
3389
|
+
productId: typeof item.productId === "string" ? item.productId : defaults.productId,
|
|
3390
|
+
projectId: typeof item.projectId === "string" ? item.projectId : defaults.projectId,
|
|
3391
|
+
goalId: typeof item.goalId === "number" ? item.goalId : defaults.goalId,
|
|
3392
|
+
keyResultId: typeof item.keyResultId === "string" ? item.keyResultId : defaults.keyResultId,
|
|
3393
|
+
occurrenceId: typeof item.occurrenceId === "string" ? item.occurrenceId : defaults.occurrenceId,
|
|
3394
|
+
decidedAt: typeof item.decidedAt === "string" ? parseDate(item.decidedAt) : defaults.decidedAt,
|
|
3395
|
+
ownerId: typeof item.ownerId === "string" ? item.ownerId : defaults.ownerId,
|
|
3396
|
+
deciders: batchDeciders(item.deciders, index) ?? defaults.deciders,
|
|
3397
|
+
evidence: item.evidence === void 0 ? void 0 : parseEvidence(JSON.stringify(item.evidence))
|
|
3398
|
+
};
|
|
3399
|
+
if (input.evidence && !input.transcriptionSessionId) {
|
|
3400
|
+
throw new Error(
|
|
3401
|
+
'Evidence is quotes from a meeting transcript, so this entry needs a "meeting" (or the --meeting flag).'
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3404
|
+
const decision2 = await client.decisions.create(input);
|
|
3405
|
+
results.push({ index, success: true, decision: decision2, statement });
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
results.push({
|
|
3408
|
+
index,
|
|
3409
|
+
success: false,
|
|
3410
|
+
statement,
|
|
3411
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (results.some((r) => !r.success)) process.exitCode = 1;
|
|
3416
|
+
if (useJson) outputDecisionBatchJson(results);
|
|
3417
|
+
else outputDecisionBatchPretty(results);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const evidenceRaw = readText(void 0, options.evidenceFile);
|
|
3421
|
+
if (evidenceRaw !== void 0 && !options.meeting) {
|
|
3422
|
+
throw new Error(
|
|
3423
|
+
"Evidence is quotes from a meeting transcript, so --evidence-file needs --meeting <id>."
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
const decision = await client.decisions.create({
|
|
3427
|
+
...defaults,
|
|
3428
|
+
statement: options.statement,
|
|
3429
|
+
body: readText(options.body, options.bodyFile),
|
|
3430
|
+
deciders: options.decider.length > 0 ? options.decider : void 0,
|
|
3431
|
+
evidence: evidenceRaw === void 0 ? void 0 : parseEvidence(evidenceRaw)
|
|
3432
|
+
});
|
|
3433
|
+
if (useJson) outputDecisionJson(decision);
|
|
3434
|
+
else {
|
|
3435
|
+
console.log(
|
|
3436
|
+
`
|
|
3437
|
+
\u2713 ${decision.status === "OPEN" ? "Open question" : "Decision"} logged`
|
|
3438
|
+
);
|
|
3439
|
+
outputDecisionPretty(decision);
|
|
3440
|
+
}
|
|
3441
|
+
} catch (error) {
|
|
3442
|
+
handleError(error, useJson);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
);
|
|
3446
|
+
decisions.command("update").description(
|
|
3447
|
+
`Update a decision's content and scope; only the fields you pass are written. Status changes go through "decisions status".`
|
|
3448
|
+
).requiredOption("--id <id>", "Decision CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").option("-s, --statement <text>", "New statement").option("-b, --body <markdown>", 'New Markdown body (or "null" to clear)').option("--body-file <path>", 'Read the new body from a file ("-" = stdin)').option("--decided-at <iso>", 'When it was decided (or "null" to clear)').option("--owner <userId>", 'Owner user ID (or "null" to clear)').option("--product <slug|id>", 'Product (or "null" to detach)').option("--project <id>", 'Project CUID (or "null" to detach)').option("--goal <id>", 'Goal integer id (or "null" to detach)').option("--key-result <id>", 'Key result CUID (or "null" to detach)').option("--adr <id>", 'ADR document CUID (or "null" to detach)').action(
|
|
3449
|
+
async (options, cmd) => {
|
|
3450
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3451
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3452
|
+
try {
|
|
3453
|
+
const body = options.body === "null" ? null : readText(options.body, options.bodyFile);
|
|
3454
|
+
const patch = {
|
|
3455
|
+
statement: options.statement,
|
|
3456
|
+
body,
|
|
3457
|
+
decidedAt: options.decidedAt === "null" ? null : parseDate(options.decidedAt),
|
|
3458
|
+
ownerId: parseNullableString(options.owner),
|
|
3459
|
+
projectId: parseNullableString(options.project),
|
|
3460
|
+
goalId: parseNullableInt(options.goal),
|
|
3461
|
+
keyResultId: parseNullableString(options.keyResult),
|
|
3462
|
+
adrDocumentId: parseNullableString(options.adr)
|
|
3463
|
+
};
|
|
3464
|
+
if (options.product === void 0 && Object.values(patch).every((v) => v === void 0)) {
|
|
3465
|
+
throw new Error(
|
|
3466
|
+
"Nothing to update. Pass at least one of --statement, --body, --body-file, --decided-at, --owner, --product, --project, --goal, --key-result, --adr."
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
const client = getClient();
|
|
3470
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3471
|
+
const productId = options.product === void 0 ? void 0 : options.product === "null" ? null : await resolveProductId(client, workspaceId, options.product);
|
|
3472
|
+
const decision = await client.decisions.update({
|
|
3473
|
+
workspaceId,
|
|
3474
|
+
decisionId: options.id,
|
|
3475
|
+
...patch,
|
|
3476
|
+
productId
|
|
3477
|
+
});
|
|
3478
|
+
if (useJson) outputDecisionJson(decision);
|
|
3479
|
+
else {
|
|
3480
|
+
console.log("\n\u2713 Decision updated");
|
|
3481
|
+
outputDecisionPretty(decision);
|
|
3482
|
+
}
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
handleError(error, useJson);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
);
|
|
3488
|
+
decisions.command("status").description(
|
|
3489
|
+
"Move a decision through its lifecycle \u2014 including answering an open question by moving it from OPEN to ACCEPTED."
|
|
3490
|
+
).requiredOption("--id <id>", "Decision CUID").requiredOption(
|
|
3491
|
+
"--status <status>",
|
|
3492
|
+
"OPEN, PROPOSED, ACCEPTED, SUPERSEDED or DEPRECATED"
|
|
3493
|
+
).option("--superseded-by <id>", "The decision that replaced this one (with --status SUPERSEDED)").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(
|
|
3494
|
+
async (options, cmd) => {
|
|
3495
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3496
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3497
|
+
try {
|
|
3498
|
+
const status = parseStatus(options.status);
|
|
3499
|
+
if (status === "SUPERSEDED" && !options.supersededBy) {
|
|
3500
|
+
throw new Error(
|
|
3501
|
+
"--status SUPERSEDED needs --superseded-by <id>: the decision that replaced this one."
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
const client = getClient();
|
|
3505
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3506
|
+
const decision = await client.decisions.setStatus({
|
|
3507
|
+
workspaceId,
|
|
3508
|
+
decisionId: options.id,
|
|
3509
|
+
status,
|
|
3510
|
+
supersededById: options.supersededBy
|
|
3511
|
+
});
|
|
3512
|
+
if (useJson) outputDecisionJson(decision);
|
|
3513
|
+
else {
|
|
3514
|
+
console.log(`
|
|
3515
|
+
\u2713 Decision is now ${decision.status}`);
|
|
3516
|
+
outputDecisionPretty(decision);
|
|
3517
|
+
}
|
|
3518
|
+
} catch (error) {
|
|
3519
|
+
handleError(error, useJson);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
);
|
|
3523
|
+
decisions.command("link").description("Record what implements a decision: a ticket or a feature. Idempotent.").requiredOption("--id <id>", "Decision CUID").option("--ticket <id>", "Ticket CUID").option("--feature <id>", "Feature CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(
|
|
3524
|
+
async (options, cmd) => {
|
|
3525
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3526
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3527
|
+
try {
|
|
3528
|
+
if (!options.ticket && !options.feature) {
|
|
3529
|
+
throw new Error("Pass --ticket <id> or --feature <id>.");
|
|
3530
|
+
}
|
|
3531
|
+
if (options.ticket && options.feature) {
|
|
3532
|
+
throw new Error("Pass one of --ticket or --feature, not both.");
|
|
3533
|
+
}
|
|
3534
|
+
const client = getClient();
|
|
3535
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3536
|
+
const link = options.ticket ? await client.decisions.linkTicket(workspaceId, options.id, options.ticket) : await client.decisions.linkFeature(workspaceId, options.id, options.feature);
|
|
3537
|
+
if (useJson) console.log(JSON.stringify(link, null, 2));
|
|
3538
|
+
else {
|
|
3539
|
+
console.log(`\u2713 Linked ${chalk4.gray(`(link id ${link.id})`)}`);
|
|
3540
|
+
console.log(
|
|
3541
|
+
chalk4.gray(" Remove it with: exponential decisions unlink --link " + link.id)
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3544
|
+
} catch (error) {
|
|
3545
|
+
handleError(error, useJson);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
);
|
|
3549
|
+
decisions.command("unlink").description(
|
|
3550
|
+
'Remove one "implemented by" link. Takes the LINK id (shown by "decisions get"), not the ticket or feature id.'
|
|
3551
|
+
).requiredOption("--link <id>", "DecisionLink CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(async (options, cmd) => {
|
|
3552
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3553
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3554
|
+
try {
|
|
3555
|
+
const client = getClient();
|
|
3556
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3557
|
+
await client.decisions.unlink(workspaceId, options.link);
|
|
3558
|
+
if (useJson) console.log(JSON.stringify({ deleted: true, linkId: options.link }, null, 2));
|
|
3559
|
+
else console.log("\u2713 Link removed");
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
handleError(error, useJson);
|
|
3562
|
+
}
|
|
3563
|
+
});
|
|
3564
|
+
const drafts = new Command4("draft").description(
|
|
3565
|
+
"Review decisions an agent proposed but nobody has confirmed yet (visible only on their source meeting)"
|
|
3566
|
+
);
|
|
3567
|
+
drafts.command("extract").description(
|
|
3568
|
+
"Propose draft decisions from a meeting's notes and transcript, for someone to confirm. Idempotent: existing drafts are returned rather than regenerated, and a meeting that already has confirmed decisions is left alone."
|
|
3569
|
+
).requiredOption("--meeting <id>", "Meeting CUID (transcriptionSessionId)").action(async (options, cmd) => {
|
|
3570
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3571
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3572
|
+
try {
|
|
3573
|
+
const client = getClient();
|
|
3574
|
+
const result = await client.decisions.extractDrafts(options.meeting);
|
|
3575
|
+
if (useJson) console.log(JSON.stringify(result, null, 2));
|
|
3576
|
+
else {
|
|
3577
|
+
if (result.alreadyPublished) {
|
|
3578
|
+
console.log(
|
|
3579
|
+
chalk4.gray("This meeting already has confirmed decisions \u2014 nothing was extracted.")
|
|
3580
|
+
);
|
|
3581
|
+
} else if (result.alreadyDrafted) {
|
|
3582
|
+
console.log(chalk4.gray("Returning the drafts this meeting already had."));
|
|
3583
|
+
}
|
|
3584
|
+
console.log(
|
|
3585
|
+
`\u2713 ${result.draftCount} draft${result.draftCount === 1 ? "" : "s"} awaiting review (${result.draftsCreated} new)`
|
|
3586
|
+
);
|
|
3587
|
+
if (result.discardedWithoutEvidence > 0) {
|
|
3588
|
+
console.log(
|
|
3589
|
+
chalk4.gray(
|
|
3590
|
+
` ${result.discardedWithoutEvidence} candidate(s) dropped \u2014 no transcript turn supported them.`
|
|
3591
|
+
)
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
for (const message of result.errors) console.log(chalk4.yellow(` ${message}`));
|
|
3595
|
+
}
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
handleError(error, useJson);
|
|
3598
|
+
}
|
|
3599
|
+
});
|
|
3600
|
+
drafts.command("confirm").description("Publish a draft decision into the log").requiredOption("--id <id>", "Decision CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(async (options, cmd) => {
|
|
2436
3601
|
const globalOpts = cmd.optsWithGlobals();
|
|
2437
3602
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2438
3603
|
try {
|
|
2439
3604
|
const client = getClient();
|
|
2440
|
-
await client.
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
console.log(
|
|
2445
|
-
|
|
3605
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3606
|
+
const decision = await client.decisions.confirmDraft(workspaceId, options.id);
|
|
3607
|
+
if (useJson) outputDecisionJson(decision);
|
|
3608
|
+
else {
|
|
3609
|
+
console.log("\n\u2713 Draft confirmed");
|
|
3610
|
+
outputDecisionPretty(decision);
|
|
2446
3611
|
}
|
|
2447
3612
|
} catch (error) {
|
|
2448
3613
|
handleError(error, useJson);
|
|
2449
3614
|
}
|
|
2450
3615
|
});
|
|
2451
|
-
|
|
3616
|
+
drafts.command("reject").description(
|
|
3617
|
+
'Reject a draft. A confirmed decision is never rejected \u2014 deprecate or supersede it with "decisions status".'
|
|
3618
|
+
).requiredOption("--id <id>", "Decision CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(async (options, cmd) => {
|
|
2452
3619
|
const globalOpts = cmd.optsWithGlobals();
|
|
2453
3620
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2454
3621
|
try {
|
|
2455
|
-
if (!VALID_INTERACTION_TYPES.includes(options.type)) {
|
|
2456
|
-
throw new Error(`Invalid interaction type "${options.type}". Valid: ${VALID_INTERACTION_TYPES.join(", ")}`);
|
|
2457
|
-
}
|
|
2458
|
-
if (!VALID_INTERACTION_DIRECTIONS.includes(options.direction)) {
|
|
2459
|
-
throw new Error(`Invalid direction "${options.direction}". Valid: ${VALID_INTERACTION_DIRECTIONS.join(", ")}`);
|
|
2460
|
-
}
|
|
2461
3622
|
const client = getClient();
|
|
2462
|
-
const
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
subject: options.subject,
|
|
2467
|
-
notes: options.notes
|
|
2468
|
-
});
|
|
2469
|
-
if (useJson) {
|
|
2470
|
-
outputInteractionJson(interaction);
|
|
2471
|
-
} else {
|
|
2472
|
-
console.log("\n\u2713 Interaction added");
|
|
2473
|
-
outputInteractionPretty(interaction);
|
|
2474
|
-
}
|
|
3623
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3624
|
+
const result = await client.decisions.rejectDraft(workspaceId, options.id);
|
|
3625
|
+
if (useJson) console.log(JSON.stringify(result, null, 2));
|
|
3626
|
+
else console.log("\u2713 Draft rejected");
|
|
2475
3627
|
} catch (error) {
|
|
2476
3628
|
handleError(error, useJson);
|
|
2477
3629
|
}
|
|
2478
3630
|
});
|
|
2479
|
-
|
|
3631
|
+
drafts.command("delete").description(
|
|
3632
|
+
"Permanently delete a draft or rejected decision. Confirmed decisions are never deleted."
|
|
3633
|
+
).requiredOption("--id <id>", "Decision CUID").option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").action(async (options, cmd) => {
|
|
2480
3634
|
const globalOpts = cmd.optsWithGlobals();
|
|
2481
3635
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2482
3636
|
try {
|
|
2483
3637
|
const client = getClient();
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
console.log(`
|
|
2489
|
-
\u2713 Enrichment queued for contact ${id} (job ${result.enrichmentId})`);
|
|
2490
|
-
} else if (result.reason === "already-in-flight") {
|
|
2491
|
-
console.log(`
|
|
2492
|
-
\u2022 Enrichment already in progress for contact ${id} (job ${result.enrichmentId})`);
|
|
2493
|
-
} else {
|
|
2494
|
-
console.log(`
|
|
2495
|
-
\u2022 Not queued for contact ${id}${result.reason ? ` (${result.reason})` : ""}`);
|
|
2496
|
-
}
|
|
3638
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
3639
|
+
const result = await client.decisions.deleteDraft(workspaceId, options.id);
|
|
3640
|
+
if (useJson) console.log(JSON.stringify({ deleted: true, ...result }, null, 2));
|
|
3641
|
+
else console.log("\u2713 Draft deleted");
|
|
2497
3642
|
} catch (error) {
|
|
2498
3643
|
handleError(error, useJson);
|
|
2499
3644
|
}
|
|
2500
3645
|
});
|
|
2501
|
-
|
|
3646
|
+
decisions.addCommand(drafts);
|
|
3647
|
+
return decisions;
|
|
2502
3648
|
}
|
|
2503
3649
|
|
|
2504
3650
|
// src/commands/deals.ts
|
|
2505
|
-
import { Command as
|
|
3651
|
+
import { Command as Command5 } from "commander";
|
|
2506
3652
|
function createDealsCommand() {
|
|
2507
|
-
const deals = new
|
|
3653
|
+
const deals = new Command5("deals").description("Manage pipeline deals");
|
|
2508
3654
|
deals.command("pipeline").description("Get pipeline overview with stages").requiredOption("--workspace <id>", "Workspace ID").action(async (options, cmd) => {
|
|
2509
3655
|
const globalOpts = cmd.optsWithGlobals();
|
|
2510
3656
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -2689,36 +3835,7 @@ function createDealsCommand() {
|
|
|
2689
3835
|
}
|
|
2690
3836
|
|
|
2691
3837
|
// src/commands/epics.ts
|
|
2692
|
-
import { Command as
|
|
2693
|
-
|
|
2694
|
-
// src/utils/resolve.ts
|
|
2695
|
-
async function resolveWorkspaceId(client, slugOrId) {
|
|
2696
|
-
return (await resolveWorkspace(client, slugOrId)).id;
|
|
2697
|
-
}
|
|
2698
|
-
async function resolveWorkspace(client, slugOrId) {
|
|
2699
|
-
const config = getConfig();
|
|
2700
|
-
const candidate = slugOrId ?? config.defaultWorkspaceSlug ?? config.defaultWorkspaceId;
|
|
2701
|
-
if (!candidate) {
|
|
2702
|
-
throw new Error(
|
|
2703
|
-
"No workspace specified and no default workspace set. Pass --workspace <slug> or run `exponential workspaces set-default <slug>`."
|
|
2704
|
-
);
|
|
2705
|
-
}
|
|
2706
|
-
const workspaces = await client.workspaces.list();
|
|
2707
|
-
const byId = workspaces.find((w) => w.id === candidate);
|
|
2708
|
-
if (byId) return byId;
|
|
2709
|
-
const bySlug = workspaces.find((w) => w.slug === candidate);
|
|
2710
|
-
if (bySlug) return bySlug;
|
|
2711
|
-
const known = workspaces.map((w) => w.slug).join(", ");
|
|
2712
|
-
throw new Error(
|
|
2713
|
-
`Workspace "${candidate}" not found. Available: ${known || "(none)"}`
|
|
2714
|
-
);
|
|
2715
|
-
}
|
|
2716
|
-
async function resolveProductId(client, workspaceId, slugOrId) {
|
|
2717
|
-
const product = await client.products.resolve(workspaceId, slugOrId);
|
|
2718
|
-
return product.id;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
// src/commands/epics.ts
|
|
3838
|
+
import { Command as Command6 } from "commander";
|
|
2722
3839
|
var EPIC_STATUSES = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
|
|
2723
3840
|
var EPIC_PRIORITIES = ["HIGH", "MEDIUM", "LOW", "NONE"];
|
|
2724
3841
|
function validateEpicStatus(value) {
|
|
@@ -2737,7 +3854,7 @@ function validateEpicPriority(value) {
|
|
|
2737
3854
|
}
|
|
2738
3855
|
return value;
|
|
2739
3856
|
}
|
|
2740
|
-
function
|
|
3857
|
+
function parseDate2(value) {
|
|
2741
3858
|
if (!value) return void 0;
|
|
2742
3859
|
const d = new Date(value);
|
|
2743
3860
|
if (isNaN(d.getTime())) {
|
|
@@ -2746,7 +3863,7 @@ function parseDate(value) {
|
|
|
2746
3863
|
return d;
|
|
2747
3864
|
}
|
|
2748
3865
|
function createEpicsCommand() {
|
|
2749
|
-
const epics = new
|
|
3866
|
+
const epics = new Command6("epics").description(
|
|
2750
3867
|
"Manage epics. Epics are workspace-scoped and group actions and tickets across products."
|
|
2751
3868
|
);
|
|
2752
3869
|
epics.command("list").description("List epics in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option("--status <status>", `Filter by status: ${EPIC_STATUSES.join(", ")}`).action(
|
|
@@ -2783,8 +3900,8 @@ function createEpicsCommand() {
|
|
|
2783
3900
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2784
3901
|
try {
|
|
2785
3902
|
const priority = validateEpicPriority(options.priority);
|
|
2786
|
-
const startDate =
|
|
2787
|
-
const targetDate =
|
|
3903
|
+
const startDate = parseDate2(options.start);
|
|
3904
|
+
const targetDate = parseDate2(options.target);
|
|
2788
3905
|
const client = getClient();
|
|
2789
3906
|
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
2790
3907
|
const epic = await client.epics.create({
|
|
@@ -2846,10 +3963,10 @@ function createEpicsCommand() {
|
|
|
2846
3963
|
}
|
|
2847
3964
|
|
|
2848
3965
|
// src/commands/features.ts
|
|
2849
|
-
import { Command as
|
|
3966
|
+
import { Command as Command12 } from "commander";
|
|
2850
3967
|
|
|
2851
3968
|
// src/commands/stories.ts
|
|
2852
|
-
import { Command as
|
|
3969
|
+
import { Command as Command7 } from "commander";
|
|
2853
3970
|
async function readStdin() {
|
|
2854
3971
|
const chunks = [];
|
|
2855
3972
|
for await (const chunk of process.stdin) {
|
|
@@ -2858,7 +3975,7 @@ async function readStdin() {
|
|
|
2858
3975
|
return Buffer.concat(chunks).toString("utf-8");
|
|
2859
3976
|
}
|
|
2860
3977
|
function createStoriesCommand() {
|
|
2861
|
-
const stories = new
|
|
3978
|
+
const stories = new Command7("stories").description(
|
|
2862
3979
|
"Manage a feature's native, structured user stories (As a / I want / So that)."
|
|
2863
3980
|
);
|
|
2864
3981
|
stories.command("add").description(
|
|
@@ -2956,7 +4073,7 @@ function createStoriesCommand() {
|
|
|
2956
4073
|
}
|
|
2957
4074
|
|
|
2958
4075
|
// src/commands/scopes.ts
|
|
2959
|
-
import { Command as
|
|
4076
|
+
import { Command as Command8 } from "commander";
|
|
2960
4077
|
var SCOPE_STATUSES = [
|
|
2961
4078
|
"PLANNED",
|
|
2962
4079
|
"IN_PROGRESS",
|
|
@@ -2973,7 +4090,7 @@ function validateScopeStatus(value) {
|
|
|
2973
4090
|
return value;
|
|
2974
4091
|
}
|
|
2975
4092
|
function createScopesCommand() {
|
|
2976
|
-
const scopes = new
|
|
4093
|
+
const scopes = new Command8("scopes").description(
|
|
2977
4094
|
`Manage a feature's scopes: shippable increments ("V1", "V2"), each with its own lifecycle.`
|
|
2978
4095
|
);
|
|
2979
4096
|
scopes.command("list").description("List a feature's scopes").requiredOption("--feature <id>", "Feature CUID").action(
|
|
@@ -3052,7 +4169,7 @@ function createScopesCommand() {
|
|
|
3052
4169
|
}
|
|
3053
4170
|
|
|
3054
4171
|
// src/commands/requirements.ts
|
|
3055
|
-
import { Command as
|
|
4172
|
+
import { Command as Command9 } from "commander";
|
|
3056
4173
|
var REQUIREMENT_KINDS = [
|
|
3057
4174
|
"FUNCTIONAL",
|
|
3058
4175
|
"NON_FUNCTIONAL",
|
|
@@ -3075,7 +4192,7 @@ async function readStdin2() {
|
|
|
3075
4192
|
return Buffer.concat(chunks).toString("utf-8");
|
|
3076
4193
|
}
|
|
3077
4194
|
function createRequirementsCommand() {
|
|
3078
|
-
const requirements = new
|
|
4195
|
+
const requirements = new Command9("requirements").description(
|
|
3079
4196
|
`Manage a feature's requirements: atomic, testable EARS-style "shall" statements, checkable met/unmet.`
|
|
3080
4197
|
);
|
|
3081
4198
|
requirements.command("add").description(
|
|
@@ -3206,25 +4323,25 @@ function createRequirementsCommand() {
|
|
|
3206
4323
|
}
|
|
3207
4324
|
|
|
3208
4325
|
// src/commands/areas.ts
|
|
3209
|
-
import { Command as
|
|
3210
|
-
import
|
|
4326
|
+
import { Command as Command10 } from "commander";
|
|
4327
|
+
import chalk5 from "chalk";
|
|
3211
4328
|
function outputAreasPretty(areas) {
|
|
3212
4329
|
if (areas.length === 0) {
|
|
3213
|
-
console.log(
|
|
4330
|
+
console.log(chalk5.gray("No areas found."));
|
|
3214
4331
|
return;
|
|
3215
4332
|
}
|
|
3216
|
-
console.log(
|
|
4333
|
+
console.log(chalk5.bold(`
|
|
3217
4334
|
Areas (${areas.length} total)`));
|
|
3218
|
-
console.log(
|
|
4335
|
+
console.log(chalk5.gray("\u2500".repeat(50)));
|
|
3219
4336
|
for (const a of areas) {
|
|
3220
|
-
const count = a._count?.features != null ?
|
|
3221
|
-
console.log(` ${
|
|
3222
|
-
console.log(
|
|
4337
|
+
const count = a._count?.features != null ? chalk5.gray(` (${a._count.features} features)`) : "";
|
|
4338
|
+
console.log(` ${chalk5.bold(a.name)}${count}`);
|
|
4339
|
+
console.log(chalk5.gray(` ID: ${a.id}`));
|
|
3223
4340
|
}
|
|
3224
4341
|
console.log();
|
|
3225
4342
|
}
|
|
3226
4343
|
function createAreasCommand() {
|
|
3227
|
-
const areas = new
|
|
4344
|
+
const areas = new Command10("areas").description(
|
|
3228
4345
|
"Manage a product's areas: the buckets features are filed under (a feature has exactly one or none)."
|
|
3229
4346
|
);
|
|
3230
4347
|
areas.command("list").description("List a product's areas").requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").action(
|
|
@@ -3268,7 +4385,7 @@ function createAreasCommand() {
|
|
|
3268
4385
|
}
|
|
3269
4386
|
|
|
3270
4387
|
// src/commands/featureComments.ts
|
|
3271
|
-
import { Command as
|
|
4388
|
+
import { Command as Command11 } from "commander";
|
|
3272
4389
|
|
|
3273
4390
|
// src/utils/mentions.ts
|
|
3274
4391
|
function candidateLabels(member) {
|
|
@@ -3341,7 +4458,7 @@ function collectMention(value, previous) {
|
|
|
3341
4458
|
|
|
3342
4459
|
// src/commands/featureComments.ts
|
|
3343
4460
|
function createFeatureCommentsCommand() {
|
|
3344
|
-
const comment = new
|
|
4461
|
+
const comment = new Command11("comment").description(
|
|
3345
4462
|
"Read and post comments on a feature. Mention teammates with --mention."
|
|
3346
4463
|
);
|
|
3347
4464
|
comment.command("list").description("List comments on a feature").requiredOption("--feature <id>", "Feature ID").action(async (options, cmd) => {
|
|
@@ -3542,7 +4659,7 @@ function validateFeatureStatus(value) {
|
|
|
3542
4659
|
return value;
|
|
3543
4660
|
}
|
|
3544
4661
|
function createFeaturesCommand() {
|
|
3545
|
-
const features = new
|
|
4662
|
+
const features = new Command12("features").description(
|
|
3546
4663
|
"Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
|
|
3547
4664
|
);
|
|
3548
4665
|
features.command("list").description("List features in a product").requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${FEATURE_STATUSES.join(", ")}`).action(
|
|
@@ -3799,8 +4916,8 @@ Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
|
|
|
3799
4916
|
}
|
|
3800
4917
|
|
|
3801
4918
|
// src/commands/labels.ts
|
|
3802
|
-
import { Command as
|
|
3803
|
-
import
|
|
4919
|
+
import { Command as Command13 } from "commander";
|
|
4920
|
+
import chalk6 from "chalk";
|
|
3804
4921
|
var TRIAGE_LABELS = [
|
|
3805
4922
|
{ slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
|
|
3806
4923
|
{ slug: "needs-info", name: "needs-info", color: "avatar-yellow" },
|
|
@@ -3814,22 +4931,22 @@ function looksLikeCuid(value) {
|
|
|
3814
4931
|
}
|
|
3815
4932
|
function outputTagsPretty(tags) {
|
|
3816
4933
|
if (tags.length === 0) {
|
|
3817
|
-
console.log(
|
|
4934
|
+
console.log(chalk6.dim("No labels found."));
|
|
3818
4935
|
return;
|
|
3819
4936
|
}
|
|
3820
4937
|
console.log();
|
|
3821
4938
|
for (const tag of tags) {
|
|
3822
|
-
const scope = tag.workspaceId === null ?
|
|
3823
|
-
const category = tag.category === null ?
|
|
4939
|
+
const scope = tag.workspaceId === null ? chalk6.cyan("global") : chalk6.magenta("workspace");
|
|
4940
|
+
const category = tag.category === null ? chalk6.dim("(label)") : chalk6.dim(`(${tag.category})`);
|
|
3824
4941
|
console.log(
|
|
3825
|
-
` ${
|
|
4942
|
+
` ${chalk6.bold(tag.slug)} ${tag.name} ${chalk6.gray(tag.color)} ${scope} ${category}`
|
|
3826
4943
|
);
|
|
3827
4944
|
}
|
|
3828
4945
|
console.log();
|
|
3829
4946
|
}
|
|
3830
4947
|
function outputTagPretty(tag, prefix) {
|
|
3831
4948
|
if (prefix) console.log(prefix);
|
|
3832
|
-
console.log(` ${
|
|
4949
|
+
console.log(` ${chalk6.bold(tag.slug)} ${tag.name} ${chalk6.gray(tag.color)}`);
|
|
3833
4950
|
}
|
|
3834
4951
|
async function resolveLabelIds(client, workspaceId, values) {
|
|
3835
4952
|
if (values.length === 0) return [];
|
|
@@ -3853,7 +4970,7 @@ async function resolveLabelIds(client, workspaceId, values) {
|
|
|
3853
4970
|
return [...byId, ...resolvedFromSlugs];
|
|
3854
4971
|
}
|
|
3855
4972
|
function createLabelsCommand() {
|
|
3856
|
-
const labels = new
|
|
4973
|
+
const labels = new Command13("labels").description(
|
|
3857
4974
|
"Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
|
|
3858
4975
|
);
|
|
3859
4976
|
labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
|
|
@@ -3901,7 +5018,7 @@ function createLabelsCommand() {
|
|
|
3901
5018
|
if (useJson) {
|
|
3902
5019
|
console.log(JSON.stringify(tag, null, 2));
|
|
3903
5020
|
} else {
|
|
3904
|
-
outputTagPretty(tag,
|
|
5021
|
+
outputTagPretty(tag, chalk6.green("\n\u2713 Label created"));
|
|
3905
5022
|
}
|
|
3906
5023
|
} catch (error) {
|
|
3907
5024
|
handleError(error, useJson);
|
|
@@ -3939,10 +5056,10 @@ function createLabelsCommand() {
|
|
|
3939
5056
|
console.log(JSON.stringify(results, null, 2));
|
|
3940
5057
|
} else {
|
|
3941
5058
|
console.log();
|
|
3942
|
-
console.log(
|
|
5059
|
+
console.log(chalk6.bold("Triage labels:"));
|
|
3943
5060
|
for (const r of results) {
|
|
3944
|
-
const tag =
|
|
3945
|
-
const status = r.status === "created" ?
|
|
5061
|
+
const tag = chalk6.bold(r.slug);
|
|
5062
|
+
const status = r.status === "created" ? chalk6.green("created") : chalk6.dim("existed");
|
|
3946
5063
|
console.log(` ${tag} ${status}`);
|
|
3947
5064
|
}
|
|
3948
5065
|
console.log();
|
|
@@ -3956,38 +5073,10 @@ function createLabelsCommand() {
|
|
|
3956
5073
|
}
|
|
3957
5074
|
|
|
3958
5075
|
// src/commands/meetings.ts
|
|
3959
|
-
import { Command as
|
|
3960
|
-
import
|
|
3961
|
-
|
|
3962
|
-
// src/utils/input.ts
|
|
3963
|
-
import { readFileSync } from "fs";
|
|
3964
|
-
var stdinConsumed = false;
|
|
3965
|
-
function readText(inline, file) {
|
|
3966
|
-
if (inline !== void 0) return inline;
|
|
3967
|
-
if (file === void 0) return void 0;
|
|
3968
|
-
if (file === "-") {
|
|
3969
|
-
if (stdinConsumed) {
|
|
3970
|
-
throw new Error('stdin ("-") can only back one option per invocation.');
|
|
3971
|
-
}
|
|
3972
|
-
stdinConsumed = true;
|
|
3973
|
-
return readFileSync(0, "utf-8");
|
|
3974
|
-
}
|
|
3975
|
-
return readFileSync(file, "utf-8");
|
|
3976
|
-
}
|
|
3977
|
-
function parseDate2(value) {
|
|
3978
|
-
if (value === void 0 || value === "") return void 0;
|
|
3979
|
-
const date = /^\d{4}-\d{2}-\d{2}$/.test(value) ? /* @__PURE__ */ new Date(`${value}T00:00:00`) : new Date(value);
|
|
3980
|
-
if (Number.isNaN(date.getTime())) {
|
|
3981
|
-
throw new Error(
|
|
3982
|
-
`Invalid date: "${value}". Use ISO format, e.g. 2026-08-25 or 2026-08-25T14:00.`
|
|
3983
|
-
);
|
|
3984
|
-
}
|
|
3985
|
-
return date;
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
// src/commands/meetings.ts
|
|
5076
|
+
import { Command as Command14 } from "commander";
|
|
5077
|
+
import chalk7 from "chalk";
|
|
3989
5078
|
function createMeetingsCommand() {
|
|
3990
|
-
const meetings = new
|
|
5079
|
+
const meetings = new Command14("meetings").description(
|
|
3991
5080
|
"Manage meetings and their notes (recorded, imported, or manually created)"
|
|
3992
5081
|
);
|
|
3993
5082
|
meetings.command("list").description(
|
|
@@ -4048,7 +5137,7 @@ function createMeetingsCommand() {
|
|
|
4048
5137
|
transcription,
|
|
4049
5138
|
notes: readText(options.notes, options.notesFile),
|
|
4050
5139
|
description: options.description,
|
|
4051
|
-
meetingDate:
|
|
5140
|
+
meetingDate: parseDate(options.date),
|
|
4052
5141
|
projectId: options.project,
|
|
4053
5142
|
workspaceId
|
|
4054
5143
|
});
|
|
@@ -4082,7 +5171,7 @@ function createMeetingsCommand() {
|
|
|
4082
5171
|
notes: notes2,
|
|
4083
5172
|
summary: options.summary,
|
|
4084
5173
|
description: options.description,
|
|
4085
|
-
meetingDate:
|
|
5174
|
+
meetingDate: parseDate(options.date)
|
|
4086
5175
|
});
|
|
4087
5176
|
if (useJson) outputMeetingJson(meeting);
|
|
4088
5177
|
else {
|
|
@@ -4148,7 +5237,7 @@ function createMeetingsCommand() {
|
|
|
4148
5237
|
console.log(`\u2713 Deleted ${count} of ${ids.length} meeting${ids.length === 1 ? "" : "s"}`);
|
|
4149
5238
|
if (skipped > 0) {
|
|
4150
5239
|
console.log(
|
|
4151
|
-
|
|
5240
|
+
chalk7.yellow(
|
|
4152
5241
|
`${skipped} id(s) were skipped \u2014 not found, or not owned by you.`
|
|
4153
5242
|
)
|
|
4154
5243
|
);
|
|
@@ -4159,7 +5248,7 @@ function createMeetingsCommand() {
|
|
|
4159
5248
|
}
|
|
4160
5249
|
}
|
|
4161
5250
|
);
|
|
4162
|
-
const notes = new
|
|
5251
|
+
const notes = new Command14("notes").description(
|
|
4163
5252
|
"Read and write a meeting's notes"
|
|
4164
5253
|
);
|
|
4165
5254
|
notes.command("get <id>").description(
|
|
@@ -4175,7 +5264,7 @@ function createMeetingsCommand() {
|
|
|
4175
5264
|
process.stdout.write(body.endsWith("\n") ? body : `${body}
|
|
4176
5265
|
`);
|
|
4177
5266
|
} else if (process.stdout.isTTY) {
|
|
4178
|
-
console.log(
|
|
5267
|
+
console.log(chalk7.gray("No notes yet."));
|
|
4179
5268
|
}
|
|
4180
5269
|
} catch (error) {
|
|
4181
5270
|
handleError(error, shouldUseJson(globalOpts.json, globalOpts.pretty));
|
|
@@ -4224,7 +5313,7 @@ function createMeetingsCommand() {
|
|
|
4224
5313
|
}
|
|
4225
5314
|
|
|
4226
5315
|
// src/commands/organizations.ts
|
|
4227
|
-
import { Command as
|
|
5316
|
+
import { Command as Command15 } from "commander";
|
|
4228
5317
|
var VALID_SIZES = [
|
|
4229
5318
|
"1-10",
|
|
4230
5319
|
"11-50",
|
|
@@ -4234,7 +5323,7 @@ var VALID_SIZES = [
|
|
|
4234
5323
|
"1000+"
|
|
4235
5324
|
];
|
|
4236
5325
|
function createOrganizationsCommand() {
|
|
4237
|
-
const organizations = new
|
|
5326
|
+
const organizations = new Command15("organizations").alias("orgs").description("Manage CRM organizations");
|
|
4238
5327
|
organizations.command("list").description("List organizations").requiredOption("--workspace <id>", "Workspace ID").option("--search <query>", "Search by name or description").option("--industry <industry>", "Filter by industry").option("--limit <n>", "Max results to return", parseInt).option("--cursor <cursor>", "Pagination cursor").action(async (options, cmd) => {
|
|
4239
5328
|
const globalOpts = cmd.optsWithGlobals();
|
|
4240
5329
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -4302,12 +5391,12 @@ function createOrganizationsCommand() {
|
|
|
4302
5391
|
}
|
|
4303
5392
|
|
|
4304
5393
|
// src/commands/pages.ts
|
|
4305
|
-
import { Command as
|
|
5394
|
+
import { Command as Command17 } from "commander";
|
|
4306
5395
|
|
|
4307
5396
|
// src/commands/pageComments.ts
|
|
4308
|
-
import { Command as
|
|
5397
|
+
import { Command as Command16 } from "commander";
|
|
4309
5398
|
function createPageCommentsCommand() {
|
|
4310
|
-
const comment = new
|
|
5399
|
+
const comment = new Command16("comment").description(
|
|
4311
5400
|
"Read and post comments on a page. Mention teammates with --mention."
|
|
4312
5401
|
);
|
|
4313
5402
|
comment.command("list").description("List comments on a page").requiredOption("--page <id>", "Page ID").action(async (options, cmd) => {
|
|
@@ -4402,7 +5491,7 @@ function createPageCommentsCommand() {
|
|
|
4402
5491
|
|
|
4403
5492
|
// src/commands/pages.ts
|
|
4404
5493
|
function createPagesCommand() {
|
|
4405
|
-
const pages = new
|
|
5494
|
+
const pages = new Command17("pages").description(
|
|
4406
5495
|
'Manage Knowledge pages (PRDs, research, technical specs). Link one to a feature with "features link-page".'
|
|
4407
5496
|
);
|
|
4408
5497
|
pages.command("list").description("List pages in a workspace").option("--workspace <slug|id>", "Workspace (defaults to the configured default)").option("--search <text>", "Filter by title/content").action(
|
|
@@ -4485,9 +5574,9 @@ function createPagesCommand() {
|
|
|
4485
5574
|
}
|
|
4486
5575
|
|
|
4487
5576
|
// src/commands/products.ts
|
|
4488
|
-
import { Command as
|
|
5577
|
+
import { Command as Command18 } from "commander";
|
|
4489
5578
|
function createProductsCommand() {
|
|
4490
|
-
const products = new
|
|
5579
|
+
const products = new Command18("products").description(
|
|
4491
5580
|
"Manage products. A workspace contains products; a product holds features and tickets."
|
|
4492
5581
|
);
|
|
4493
5582
|
products.command("list").description("List products in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID (defaults to configured workspace)").action(async (options, cmd) => {
|
|
@@ -4550,7 +5639,7 @@ function createProductsCommand() {
|
|
|
4550
5639
|
}
|
|
4551
5640
|
|
|
4552
5641
|
// src/commands/projects.ts
|
|
4553
|
-
import { Command as
|
|
5642
|
+
import { Command as Command19 } from "commander";
|
|
4554
5643
|
var PROJECT_STATUSES = [
|
|
4555
5644
|
"ACTIVE",
|
|
4556
5645
|
"ON_HOLD",
|
|
@@ -4577,7 +5666,7 @@ function validateProjectPriority(value) {
|
|
|
4577
5666
|
return value;
|
|
4578
5667
|
}
|
|
4579
5668
|
function createProjectsCommand() {
|
|
4580
|
-
const projects = new
|
|
5669
|
+
const projects = new Command19("projects").description("Manage projects");
|
|
4581
5670
|
projects.command("list").description("List all projects").option("--workspace <slug|id>", "Workspace slug or CUID").option("--include-actions", "Include actions in output").action(async (options, cmd) => {
|
|
4582
5671
|
const globalOpts = cmd.optsWithGlobals();
|
|
4583
5672
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -4696,8 +5785,8 @@ Re-run with --force to delete it anyway.`
|
|
|
4696
5785
|
}
|
|
4697
5786
|
|
|
4698
5787
|
// src/commands/search.ts
|
|
4699
|
-
import { Command as
|
|
4700
|
-
import
|
|
5788
|
+
import { Command as Command20 } from "commander";
|
|
5789
|
+
import chalk8 from "chalk";
|
|
4701
5790
|
var TYPE_LABELS = {
|
|
4702
5791
|
workspace: "Workspaces",
|
|
4703
5792
|
project: "Projects",
|
|
@@ -4715,7 +5804,7 @@ var TYPE_LABELS = {
|
|
|
4715
5804
|
product: "Products"
|
|
4716
5805
|
};
|
|
4717
5806
|
function createSearchCommand() {
|
|
4718
|
-
const search = new
|
|
5807
|
+
const search = new Command20("search").description(
|
|
4719
5808
|
"Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
|
|
4720
5809
|
).argument("<query>", "Text to search for").option("-w, --workspace <slug>", "Restrict results to one workspace (slug or ID)").option("-l, --limit <n>", "Max results per entity type (1-25, default 10)").action(async (query, options, cmd) => {
|
|
4721
5810
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -4729,10 +5818,10 @@ function createSearchCommand() {
|
|
|
4729
5818
|
(w) => w.slug === options.workspace || w.id === options.workspace
|
|
4730
5819
|
);
|
|
4731
5820
|
if (!match) {
|
|
4732
|
-
console.error(
|
|
4733
|
-
console.log(
|
|
5821
|
+
console.error(chalk8.red(`Workspace "${options.workspace}" not found.`));
|
|
5822
|
+
console.log(chalk8.gray("Available workspaces:"));
|
|
4734
5823
|
workspaces.forEach((w) => {
|
|
4735
|
-
console.log(
|
|
5824
|
+
console.log(chalk8.gray(` - ${w.slug} (${w.name})`));
|
|
4736
5825
|
});
|
|
4737
5826
|
process.exit(1);
|
|
4738
5827
|
}
|
|
@@ -4745,7 +5834,7 @@ function createSearchCommand() {
|
|
|
4745
5834
|
return;
|
|
4746
5835
|
}
|
|
4747
5836
|
if (result.results.length === 0) {
|
|
4748
|
-
console.log(
|
|
5837
|
+
console.log(chalk8.gray(`No matches for "${result.query}".`));
|
|
4749
5838
|
return;
|
|
4750
5839
|
}
|
|
4751
5840
|
const byType = /* @__PURE__ */ new Map();
|
|
@@ -4755,14 +5844,14 @@ function createSearchCommand() {
|
|
|
4755
5844
|
byType.set(r.type, bucket);
|
|
4756
5845
|
}
|
|
4757
5846
|
for (const [type, rows] of byType) {
|
|
4758
|
-
console.log(
|
|
5847
|
+
console.log(chalk8.bold(`
|
|
4759
5848
|
${TYPE_LABELS[type] ?? type}`));
|
|
4760
5849
|
for (const row of rows) {
|
|
4761
|
-
const parts = [
|
|
4762
|
-
if (row.subtitle) parts.push(
|
|
4763
|
-
if (row.workspace) parts.push(
|
|
4764
|
-
console.log(` ${parts.join(
|
|
4765
|
-
console.log(
|
|
5850
|
+
const parts = [chalk8.white(row.title)];
|
|
5851
|
+
if (row.subtitle) parts.push(chalk8.gray(row.subtitle));
|
|
5852
|
+
if (row.workspace) parts.push(chalk8.cyan(`@${row.workspace.slug}`));
|
|
5853
|
+
console.log(` ${parts.join(chalk8.gray(" \xB7 "))}`);
|
|
5854
|
+
console.log(chalk8.gray(` ${row.id}${row.url ? ` ${row.url}` : ""}`));
|
|
4766
5855
|
}
|
|
4767
5856
|
}
|
|
4768
5857
|
console.log();
|
|
@@ -4774,7 +5863,7 @@ ${TYPE_LABELS[type] ?? type}`));
|
|
|
4774
5863
|
}
|
|
4775
5864
|
|
|
4776
5865
|
// src/commands/tickets.ts
|
|
4777
|
-
import { Command as
|
|
5866
|
+
import { Command as Command21 } from "commander";
|
|
4778
5867
|
function collectRepeatable(value, previous) {
|
|
4779
5868
|
return [...previous, value];
|
|
4780
5869
|
}
|
|
@@ -4817,7 +5906,7 @@ function validateTicketType(value) {
|
|
|
4817
5906
|
return value;
|
|
4818
5907
|
}
|
|
4819
5908
|
function createTicketsCommand() {
|
|
4820
|
-
const tickets = new
|
|
5909
|
+
const tickets = new Command21("tickets").description(
|
|
4821
5910
|
"Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
|
|
4822
5911
|
);
|
|
4823
5912
|
tickets.command("list").description(
|
|
@@ -4998,7 +6087,7 @@ function createTicketsCommand() {
|
|
|
4998
6087
|
const globalOpts = cmd.optsWithGlobals();
|
|
4999
6088
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
5000
6089
|
try {
|
|
5001
|
-
const
|
|
6090
|
+
const parseNullableInt2 = (v) => {
|
|
5002
6091
|
if (v === void 0) return void 0;
|
|
5003
6092
|
if (v === "null") return null;
|
|
5004
6093
|
const n = parseInt(v, 10);
|
|
@@ -5012,7 +6101,7 @@ function createTicketsCommand() {
|
|
|
5012
6101
|
if (isNaN(n)) throw new Error(`Invalid number: ${v}`);
|
|
5013
6102
|
return n;
|
|
5014
6103
|
};
|
|
5015
|
-
const
|
|
6104
|
+
const parseNullableString2 = (v) => {
|
|
5016
6105
|
if (v === void 0) return void 0;
|
|
5017
6106
|
if (v === "null") return null;
|
|
5018
6107
|
return v;
|
|
@@ -5026,14 +6115,14 @@ function createTicketsCommand() {
|
|
|
5026
6115
|
body: options.body,
|
|
5027
6116
|
type,
|
|
5028
6117
|
status,
|
|
5029
|
-
priority:
|
|
6118
|
+
priority: parseNullableInt2(options.priority),
|
|
5030
6119
|
points: parseNullableFloat(options.points),
|
|
5031
|
-
featureId:
|
|
5032
|
-
epicId:
|
|
5033
|
-
cycleId:
|
|
5034
|
-
assigneeId:
|
|
5035
|
-
branchName:
|
|
5036
|
-
prUrl:
|
|
6120
|
+
featureId: parseNullableString2(options.feature),
|
|
6121
|
+
epicId: parseNullableString2(options.epic),
|
|
6122
|
+
cycleId: parseNullableString2(options.cycle),
|
|
6123
|
+
assigneeId: parseNullableString2(options.assignee),
|
|
6124
|
+
branchName: parseNullableString2(options.branch),
|
|
6125
|
+
prUrl: parseNullableString2(options.pr)
|
|
5037
6126
|
});
|
|
5038
6127
|
if (options.addLabel.length > 0 || options.removeLabel.length > 0) {
|
|
5039
6128
|
const current = await client.labels.listForEntity({
|
|
@@ -5143,7 +6232,7 @@ function createTicketsCommand() {
|
|
|
5143
6232
|
handleError(error, useJson);
|
|
5144
6233
|
}
|
|
5145
6234
|
});
|
|
5146
|
-
const comment = new
|
|
6235
|
+
const comment = new Command21("comment").description("Manage comments on a ticket");
|
|
5147
6236
|
comment.command("list").description("List comments on a ticket").requiredOption("--id <id>", "Ticket CUID").action(async (options, cmd) => {
|
|
5148
6237
|
const globalOpts = cmd.optsWithGlobals();
|
|
5149
6238
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -5234,11 +6323,271 @@ function createTicketsCommand() {
|
|
|
5234
6323
|
return tickets;
|
|
5235
6324
|
}
|
|
5236
6325
|
|
|
6326
|
+
// src/commands/time.ts
|
|
6327
|
+
import { Command as Command22 } from "commander";
|
|
6328
|
+
import { segmentConversation } from "exponential-sdk";
|
|
6329
|
+
var SOURCES2 = ["manual", "claude-desktop", "agent-run"];
|
|
6330
|
+
var STATUSES2 = ["PROPOSED", "CONFIRMED"];
|
|
6331
|
+
var OWNERSHIP_NOTE = "Run with an external-agent key, the entry belongs to the agent's OWNER and is always PROPOSED; a personal token logs your own time, CONFIRMED by default.";
|
|
6332
|
+
function parseSource2(value) {
|
|
6333
|
+
const lower = value.toLowerCase();
|
|
6334
|
+
if (!SOURCES2.includes(lower)) {
|
|
6335
|
+
throw new Error(`Invalid source "${value}". Use one of: ${SOURCES2.join(", ")}.`);
|
|
6336
|
+
}
|
|
6337
|
+
return lower;
|
|
6338
|
+
}
|
|
6339
|
+
function parseStatus2(value) {
|
|
6340
|
+
const upper = value.toUpperCase();
|
|
6341
|
+
if (!STATUSES2.includes(upper)) {
|
|
6342
|
+
throw new Error(`Invalid status "${value}". Use one of: ${STATUSES2.join(", ")}.`);
|
|
6343
|
+
}
|
|
6344
|
+
return upper;
|
|
6345
|
+
}
|
|
6346
|
+
var BATCH_ITEM_KEYS2 = /* @__PURE__ */ new Set([
|
|
6347
|
+
"actionId",
|
|
6348
|
+
"action",
|
|
6349
|
+
"startedAt",
|
|
6350
|
+
"from",
|
|
6351
|
+
"endedAt",
|
|
6352
|
+
"to",
|
|
6353
|
+
"source",
|
|
6354
|
+
"status",
|
|
6355
|
+
"sourceRef",
|
|
6356
|
+
"ref",
|
|
6357
|
+
"note"
|
|
6358
|
+
]);
|
|
6359
|
+
function parseBatchFile2(raw) {
|
|
6360
|
+
let parsed;
|
|
6361
|
+
try {
|
|
6362
|
+
parsed = JSON.parse(raw);
|
|
6363
|
+
} catch (error) {
|
|
6364
|
+
throw new Error(
|
|
6365
|
+
`--from-file must be JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
6366
|
+
);
|
|
6367
|
+
}
|
|
6368
|
+
const items = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.entries) ? parsed.entries : null;
|
|
6369
|
+
if (!items) {
|
|
6370
|
+
throw new Error('--from-file must contain a JSON array of entries, or {"entries": [...]}.');
|
|
6371
|
+
}
|
|
6372
|
+
if (items.length === 0) {
|
|
6373
|
+
throw new Error("--from-file contained no entries.");
|
|
6374
|
+
}
|
|
6375
|
+
return items.map((item, i) => {
|
|
6376
|
+
if (typeof item !== "object" || item === null) {
|
|
6377
|
+
throw new Error(`Entry[${i}] is not an object.`);
|
|
6378
|
+
}
|
|
6379
|
+
const entry = item;
|
|
6380
|
+
const unknown = Object.keys(entry).filter((key) => !BATCH_ITEM_KEYS2.has(key));
|
|
6381
|
+
if (unknown.length > 0) {
|
|
6382
|
+
throw new Error(
|
|
6383
|
+
`Entry[${i}] has unknown field(s): ${unknown.join(", ")}. Accepted: ${[...BATCH_ITEM_KEYS2].join(", ")}.`
|
|
6384
|
+
);
|
|
6385
|
+
}
|
|
6386
|
+
return entry;
|
|
6387
|
+
});
|
|
6388
|
+
}
|
|
6389
|
+
function optionalString(value, label, index) {
|
|
6390
|
+
if (value === void 0 || value === null) return void 0;
|
|
6391
|
+
if (typeof value !== "string") throw new Error(`Entry[${index}].${label} must be a string.`);
|
|
6392
|
+
return value;
|
|
6393
|
+
}
|
|
6394
|
+
function batchItemToInput(item, index, defaults) {
|
|
6395
|
+
const actionId = optionalString(item.actionId ?? item.action, "actionId", index) ?? defaults.actionId;
|
|
6396
|
+
const from = optionalString(item.startedAt ?? item.from, "startedAt", index);
|
|
6397
|
+
const to = optionalString(item.endedAt ?? item.to, "endedAt", index);
|
|
6398
|
+
const startedAt = from !== void 0 ? parseDate(from) : defaults.startedAt;
|
|
6399
|
+
const endedAt = to !== void 0 ? parseDate(to) : defaults.endedAt;
|
|
6400
|
+
if (!actionId) throw new Error(`Entry[${index}] needs an "actionId" (or the --action flag).`);
|
|
6401
|
+
if (!startedAt || !endedAt) {
|
|
6402
|
+
throw new Error(`Entry[${index}] needs "startedAt" and "endedAt" (or --from and --to).`);
|
|
6403
|
+
}
|
|
6404
|
+
const source = optionalString(item.source, "source", index);
|
|
6405
|
+
const status = optionalString(item.status, "status", index);
|
|
6406
|
+
return {
|
|
6407
|
+
actionId,
|
|
6408
|
+
startedAt,
|
|
6409
|
+
endedAt,
|
|
6410
|
+
source: source !== void 0 ? parseSource2(source) : defaults.source,
|
|
6411
|
+
status: status !== void 0 ? parseStatus2(status) : defaults.status,
|
|
6412
|
+
sourceRef: optionalString(item.sourceRef ?? item.ref, "sourceRef", index) ?? defaults.sourceRef,
|
|
6413
|
+
note: optionalString(item.note, "note", index) ?? defaults.note
|
|
6414
|
+
};
|
|
6415
|
+
}
|
|
6416
|
+
function createTimeCommand() {
|
|
6417
|
+
const time = new Command22("time").description(
|
|
6418
|
+
`Time entries \u2014 log the day's work and list it. ${OWNERSHIP_NOTE}`
|
|
6419
|
+
);
|
|
6420
|
+
time.command("log").description(
|
|
6421
|
+
[
|
|
6422
|
+
"Log one completed entry with explicit bounds; the running Timer is never touched.",
|
|
6423
|
+
"With --ref the write is idempotent per owner: a re-run updates a PROPOSED entry and leaves a CONFIRMED one alone.",
|
|
6424
|
+
"Bulk: --from-file <path|-> takes a JSON array of entries; the other flags become its defaults.",
|
|
6425
|
+
OWNERSHIP_NOTE
|
|
6426
|
+
].join(" ")
|
|
6427
|
+
).option("-a, --action <id>", "Action CUID the time was spent on").option("--from <iso>", "Start, e.g. 2026-09-11T09:22").option("--to <iso>", "End, e.g. 2026-09-11T10:30").option("--source <source>", `${SOURCES2.join(", ")} (default: manual)`).option("--status <status>", "PROPOSED or CONFIRMED (ignored under an agent key: always PROPOSED)").option("--ref <sourceRef>", "Idempotency key, e.g. claude-session:<sessionId>#<segment>").option("--note <text>", "One line shown in the day view and list row").option("--from-file <path>", 'Bulk log from a JSON array of entries ("-" = stdin)').action(
|
|
6428
|
+
async (options, cmd) => {
|
|
6429
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6430
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
6431
|
+
try {
|
|
6432
|
+
const defaults = {
|
|
6433
|
+
actionId: options.action,
|
|
6434
|
+
startedAt: parseDate(options.from),
|
|
6435
|
+
endedAt: parseDate(options.to),
|
|
6436
|
+
source: options.source ? parseSource2(options.source) : void 0,
|
|
6437
|
+
status: options.status ? parseStatus2(options.status) : void 0,
|
|
6438
|
+
sourceRef: options.ref,
|
|
6439
|
+
note: options.note
|
|
6440
|
+
};
|
|
6441
|
+
const client = getClient();
|
|
6442
|
+
if (options.fromFile) {
|
|
6443
|
+
if (options.ref !== void 0) {
|
|
6444
|
+
throw new Error(
|
|
6445
|
+
'--ref is per-entry: a shared sourceRef would make every entry overwrite the last. Put "sourceRef" on each entry in --from-file instead.'
|
|
6446
|
+
);
|
|
6447
|
+
}
|
|
6448
|
+
const items = parseBatchFile2(readText(void 0, options.fromFile));
|
|
6449
|
+
const inputs = items.map((item, i) => {
|
|
6450
|
+
try {
|
|
6451
|
+
return batchItemToInput(item, i, defaults);
|
|
6452
|
+
} catch (error) {
|
|
6453
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
6454
|
+
}
|
|
6455
|
+
});
|
|
6456
|
+
const results = [];
|
|
6457
|
+
for (const [index, input] of inputs.entries()) {
|
|
6458
|
+
if (input instanceof Error) {
|
|
6459
|
+
results.push({ index, success: false, error: input.message });
|
|
6460
|
+
continue;
|
|
6461
|
+
}
|
|
6462
|
+
const [result2] = await client.time.logBatch([input]);
|
|
6463
|
+
results.push({ ...result2, index });
|
|
6464
|
+
}
|
|
6465
|
+
if (results.some((r) => !r.success)) process.exitCode = 1;
|
|
6466
|
+
if (useJson) outputTimeBatchJson(results);
|
|
6467
|
+
else outputTimeBatchPretty(results);
|
|
6468
|
+
return;
|
|
6469
|
+
}
|
|
6470
|
+
if (!defaults.actionId) throw new Error("Pass --action <id>, or --from-file <path|-> for a batch.");
|
|
6471
|
+
if (!defaults.startedAt || !defaults.endedAt) throw new Error("Pass both --from <iso> and --to <iso>.");
|
|
6472
|
+
if (defaults.endedAt.getTime() <= defaults.startedAt.getTime()) {
|
|
6473
|
+
throw new Error("--to must be after --from.");
|
|
6474
|
+
}
|
|
6475
|
+
const result = await client.time.log(defaults);
|
|
6476
|
+
if (useJson) outputTimeLogJson(result);
|
|
6477
|
+
else outputTimeLogPretty(result);
|
|
6478
|
+
} catch (error) {
|
|
6479
|
+
handleError(error);
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
);
|
|
6483
|
+
time.command("list").description("One day's entries for the signed-in user, oldest first, each with its PROPOSED/CONFIRMED status").requiredOption("-d, --date <YYYY-MM-DD>", "The day (local time)").option("-w, --workspace <slug|id>", "Only entries in this workspace (default: all)").option("--proposed", "Only PROPOSED entries").action(async (options, cmd) => {
|
|
6484
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6485
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
6486
|
+
try {
|
|
6487
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
|
|
6488
|
+
throw new Error(`--date must be YYYY-MM-DD, got "${options.date}".`);
|
|
6489
|
+
}
|
|
6490
|
+
const client = getClient();
|
|
6491
|
+
const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
|
|
6492
|
+
let entries = await client.time.list(options.date, workspaceId);
|
|
6493
|
+
if (options.proposed) entries = entries.filter((e) => e.status === "PROPOSED");
|
|
6494
|
+
if (useJson) outputTimeEntriesJson(entries, options.date);
|
|
6495
|
+
else outputTimeEntriesPretty(entries, options.date);
|
|
6496
|
+
} catch (error) {
|
|
6497
|
+
handleError(error);
|
|
6498
|
+
}
|
|
6499
|
+
});
|
|
6500
|
+
time.command("confirm").description(
|
|
6501
|
+
"Confirm a day: every PROPOSED entry of yours that starts that day becomes CONFIRMED and the Actions' spent time moves. Human only \u2014 run it with your personal credentials; an agent key is refused."
|
|
6502
|
+
).requiredOption("-d, --date <YYYY-MM-DD>", "The day (local time)").option("-w, --workspace <slug|id>", "Only entries in this workspace (default: all)").action(async (options, cmd) => {
|
|
6503
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6504
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
6505
|
+
try {
|
|
6506
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
|
|
6507
|
+
throw new Error(`--date must be YYYY-MM-DD, got "${options.date}".`);
|
|
6508
|
+
}
|
|
6509
|
+
const client = getClient();
|
|
6510
|
+
const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
|
|
6511
|
+
const result = await client.time.confirmDay(options.date, workspaceId);
|
|
6512
|
+
if (useJson) outputTimeConfirmJson(result, options.date);
|
|
6513
|
+
else outputTimeConfirmPretty(result, options.date);
|
|
6514
|
+
} catch (error) {
|
|
6515
|
+
handleError(error);
|
|
6516
|
+
}
|
|
6517
|
+
});
|
|
6518
|
+
time.command("report").description(
|
|
6519
|
+
"One day's report \u2014 the numbers the /time Day tab shows: attention hours (each covered minute once), session hours (the plain sum), agent-run time on its own, hours by Product and by Action with overlapping minutes split evenly, and the unassigned and proposed counts. JSON when piped or with --json."
|
|
6520
|
+
).requiredOption("-d, --date <YYYY-MM-DD>", "The day (local time)").option("-w, --workspace <slug|id>", "Only entries in this workspace (default: all)").action(async (options, cmd) => {
|
|
6521
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6522
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
6523
|
+
try {
|
|
6524
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
|
|
6525
|
+
throw new Error(`--date must be YYYY-MM-DD, got "${options.date}".`);
|
|
6526
|
+
}
|
|
6527
|
+
const client = getClient();
|
|
6528
|
+
const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
|
|
6529
|
+
const report = await client.time.dayReport(options.date, workspaceId);
|
|
6530
|
+
if (useJson) outputDayReportJson(report, options.date);
|
|
6531
|
+
else outputDayReportPretty(report, options.date);
|
|
6532
|
+
} catch (error) {
|
|
6533
|
+
handleError(error);
|
|
6534
|
+
}
|
|
6535
|
+
});
|
|
6536
|
+
time.command("segment").description(
|
|
6537
|
+
[
|
|
6538
|
+
"Cut one conversation's messages into Worklog segments and print the batch `time log --from-file -` consumes.",
|
|
6539
|
+
'Input: a JSON array of {at, role} (role user|assistant), or {"messages": [...]}.',
|
|
6540
|
+
"A gap over 30 minutes between the person's messages ends a segment; bounds round outward to 5 minutes; assistant-only runs longer than the gap become agent-run entries."
|
|
6541
|
+
].join(" ")
|
|
6542
|
+
).requiredOption("--from-file <path>", 'Messages JSON ("-" = stdin)').requiredOption("-a, --action <id>", "Action CUID every entry is logged on").requiredOption("--ref-prefix <prefix>", "sourceRef prefix, e.g. claude-session:<sessionId>; segments get #0, #1, \u2026").option("--gap <minutes>", "Silence between the person's messages that ends a segment", "30").option("--note <text>", "Note copied onto every entry").action(
|
|
6543
|
+
async (options) => {
|
|
6544
|
+
try {
|
|
6545
|
+
const gap = Number.parseInt(options.gap, 10);
|
|
6546
|
+
if (Number.isNaN(gap) || gap <= 0) throw new Error(`--gap must be a positive integer, got "${options.gap}".`);
|
|
6547
|
+
const messages = parseMessagesFile(readText(void 0, options.fromFile));
|
|
6548
|
+
const segments = segmentConversation(messages, gap);
|
|
6549
|
+
const entries = segments.map((segment, index) => ({
|
|
6550
|
+
actionId: options.action,
|
|
6551
|
+
startedAt: segment.start.toISOString(),
|
|
6552
|
+
endedAt: segment.end.toISOString(),
|
|
6553
|
+
source: segment.humanTurns === 0 ? "agent-run" : "claude-desktop",
|
|
6554
|
+
sourceRef: `${options.refPrefix}#${index}`,
|
|
6555
|
+
...options.note ? { note: options.note } : {}
|
|
6556
|
+
}));
|
|
6557
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
6558
|
+
} catch (error) {
|
|
6559
|
+
handleError(error);
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
);
|
|
6563
|
+
return time;
|
|
6564
|
+
}
|
|
6565
|
+
function parseMessagesFile(raw) {
|
|
6566
|
+
let parsed;
|
|
6567
|
+
try {
|
|
6568
|
+
parsed = JSON.parse(raw);
|
|
6569
|
+
} catch (error) {
|
|
6570
|
+
throw new Error(
|
|
6571
|
+
`--from-file must be JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
6572
|
+
);
|
|
6573
|
+
}
|
|
6574
|
+
const items = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.messages) ? parsed.messages : null;
|
|
6575
|
+
if (!items) throw new Error('--from-file must contain a JSON array of {at, role} messages, or {"messages": [...]}.');
|
|
6576
|
+
return items.map((item, i) => {
|
|
6577
|
+
if (typeof item !== "object" || item === null) throw new Error(`Message[${i}] is not an object.`);
|
|
6578
|
+
const { at, role } = item;
|
|
6579
|
+
if (typeof at !== "string" && !(at instanceof Date)) throw new Error(`Message[${i}].at must be an ISO timestamp.`);
|
|
6580
|
+
if (Number.isNaN(new Date(at).getTime())) throw new Error(`Message[${i}].at is not a valid timestamp: "${String(at)}".`);
|
|
6581
|
+
if (role !== "user" && role !== "assistant") throw new Error(`Message[${i}].role must be "user" or "assistant".`);
|
|
6582
|
+
return { at, role };
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
|
|
5237
6586
|
// src/commands/workspaces.ts
|
|
5238
|
-
import { Command as
|
|
5239
|
-
import
|
|
6587
|
+
import { Command as Command23 } from "commander";
|
|
6588
|
+
import chalk9 from "chalk";
|
|
5240
6589
|
function createWorkspacesCommand() {
|
|
5241
|
-
const workspaces = new
|
|
6590
|
+
const workspaces = new Command23("workspaces").description("Manage workspaces");
|
|
5242
6591
|
workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
|
|
5243
6592
|
const globalOpts = cmd.optsWithGlobals();
|
|
5244
6593
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -5251,7 +6600,7 @@ function createWorkspacesCommand() {
|
|
|
5251
6600
|
const config = getConfig();
|
|
5252
6601
|
outputWorkspacesPretty(workspaces2);
|
|
5253
6602
|
if (config.defaultWorkspaceSlug) {
|
|
5254
|
-
console.log(
|
|
6603
|
+
console.log(chalk9.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
|
|
5255
6604
|
}
|
|
5256
6605
|
}
|
|
5257
6606
|
} catch (error) {
|
|
@@ -5264,10 +6613,10 @@ function createWorkspacesCommand() {
|
|
|
5264
6613
|
const workspaces2 = await client.workspaces.list();
|
|
5265
6614
|
const workspace = workspaces2.find((w) => w.slug === slug);
|
|
5266
6615
|
if (!workspace) {
|
|
5267
|
-
console.error(
|
|
5268
|
-
console.log(
|
|
6616
|
+
console.error(chalk9.red(`Workspace with slug "${slug}" not found.`));
|
|
6617
|
+
console.log(chalk9.gray("Available workspaces:"));
|
|
5269
6618
|
workspaces2.forEach((w) => {
|
|
5270
|
-
console.log(
|
|
6619
|
+
console.log(chalk9.gray(` - ${w.slug} (${w.name})`));
|
|
5271
6620
|
});
|
|
5272
6621
|
process.exit(1);
|
|
5273
6622
|
}
|
|
@@ -5275,7 +6624,7 @@ function createWorkspacesCommand() {
|
|
|
5275
6624
|
defaultWorkspaceId: workspace.id,
|
|
5276
6625
|
defaultWorkspaceSlug: workspace.slug
|
|
5277
6626
|
});
|
|
5278
|
-
console.log(
|
|
6627
|
+
console.log(chalk9.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
|
|
5279
6628
|
} catch (error) {
|
|
5280
6629
|
handleError(error);
|
|
5281
6630
|
}
|
|
@@ -5303,20 +6652,20 @@ function createWorkspacesCommand() {
|
|
|
5303
6652
|
return;
|
|
5304
6653
|
}
|
|
5305
6654
|
if (members.length === 0) {
|
|
5306
|
-
console.log(
|
|
6655
|
+
console.log(chalk9.gray("No members found."));
|
|
5307
6656
|
return;
|
|
5308
6657
|
}
|
|
5309
|
-
console.log(
|
|
6658
|
+
console.log(chalk9.bold(`
|
|
5310
6659
|
Members (${members.length})
|
|
5311
6660
|
`));
|
|
5312
6661
|
for (const m of members) {
|
|
5313
6662
|
const label = m.name ?? m.email ?? m.id;
|
|
5314
|
-
const via = m.source === "team" ?
|
|
6663
|
+
const via = m.source === "team" ? chalk9.gray(
|
|
5315
6664
|
` via team${m.teams.length ? `: ${m.teams.map((t) => t.name).join(", ")}` : ""}` + (m.teamRole ? ` [team ${m.teamRole}]` : "")
|
|
5316
6665
|
) : "";
|
|
5317
|
-
console.log(`${
|
|
5318
|
-
if (m.email) console.log(
|
|
5319
|
-
console.log(
|
|
6666
|
+
console.log(`${chalk9.bold(label)} ${chalk9.gray(`(${m.role})`)}${via}`);
|
|
6667
|
+
if (m.email) console.log(chalk9.gray(` ${m.email}`));
|
|
6668
|
+
console.log(chalk9.gray(` mention: ${m.mentionSyntax}`));
|
|
5320
6669
|
console.log();
|
|
5321
6670
|
}
|
|
5322
6671
|
} catch (error) {
|
|
@@ -5328,7 +6677,7 @@ Members (${members.length})
|
|
|
5328
6677
|
}
|
|
5329
6678
|
|
|
5330
6679
|
// src/commands/goals.ts
|
|
5331
|
-
import { Command as
|
|
6680
|
+
import { Command as Command24 } from "commander";
|
|
5332
6681
|
var GOAL_STATUSES = [
|
|
5333
6682
|
"planned",
|
|
5334
6683
|
"active",
|
|
@@ -5440,7 +6789,7 @@ function validateKeyResultUnit(value) {
|
|
|
5440
6789
|
return value;
|
|
5441
6790
|
}
|
|
5442
6791
|
function createKeyResultsCommand() {
|
|
5443
|
-
const kr = new
|
|
6792
|
+
const kr = new Command24("kr").description(
|
|
5444
6793
|
"Key results on an objective. Ids are CUIDs; --goal takes the objective's integer id."
|
|
5445
6794
|
);
|
|
5446
6795
|
kr.command("list").description("List key results").option("--goal <id>", "Objective ID (a number)").option("-w, --workspace <slug|id>", "Workspace (workspace-wide: every member's KRs)").option("--period <period>", "Period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${KEY_RESULT_STATUSES.join(", ")}`).option("--mine", "Narrow a workspace list to key results you own").action(
|
|
@@ -5643,7 +6992,7 @@ function createKeyResultsCommand() {
|
|
|
5643
6992
|
return kr;
|
|
5644
6993
|
}
|
|
5645
6994
|
function createGoalCommentsCommand() {
|
|
5646
|
-
const comment = new
|
|
6995
|
+
const comment = new Command24("comment").description(
|
|
5647
6996
|
"Read and post comments on a goal. Mention teammates with --mention."
|
|
5648
6997
|
);
|
|
5649
6998
|
comment.command("list").description("List comments on a goal").requiredOption("--goal <id>", "Goal ID (a number)").action(async (options, cmd) => {
|
|
@@ -5738,7 +7087,7 @@ function createGoalCommentsCommand() {
|
|
|
5738
7087
|
return comment;
|
|
5739
7088
|
}
|
|
5740
7089
|
function createGoalsCommand() {
|
|
5741
|
-
const goals = new
|
|
7090
|
+
const goals = new Command24("goals").description(
|
|
5742
7091
|
"Manage goals (objectives) and their key results. Objectives have integer ids; key results are CUIDs."
|
|
5743
7092
|
);
|
|
5744
7093
|
goals.command("list").description(
|
|
@@ -6004,7 +7353,7 @@ Re-run with --with-key-results to go ahead, or move them with \`goals kr update
|
|
|
6004
7353
|
return goals;
|
|
6005
7354
|
}
|
|
6006
7355
|
function createOkrsCommand() {
|
|
6007
|
-
const okrs = new
|
|
7356
|
+
const okrs = new Command24("okrs").description(
|
|
6008
7357
|
"OKRs \u2014 objectives with their key results nested. Manage the parts with `goals` and `goals kr`."
|
|
6009
7358
|
);
|
|
6010
7359
|
okrs.command("list").description(
|
|
@@ -6081,39 +7430,54 @@ function createOkrsCommand() {
|
|
|
6081
7430
|
return okrs;
|
|
6082
7431
|
}
|
|
6083
7432
|
|
|
6084
|
-
// src/
|
|
7433
|
+
// src/program.ts
|
|
6085
7434
|
import { readFileSync as readFileSync2 } from "fs";
|
|
6086
7435
|
import { dirname, join } from "path";
|
|
6087
7436
|
import { fileURLToPath } from "url";
|
|
6088
7437
|
var PKG_VERSION = JSON.parse(
|
|
6089
7438
|
readFileSync2(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
|
|
6090
7439
|
).version;
|
|
6091
|
-
var
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
program.addCommand(
|
|
6107
|
-
program.addCommand(
|
|
6108
|
-
program.addCommand(
|
|
6109
|
-
program.addCommand(
|
|
6110
|
-
program.addCommand(
|
|
6111
|
-
program.addCommand(
|
|
6112
|
-
program.addCommand(
|
|
6113
|
-
program.addCommand(
|
|
6114
|
-
program.addCommand(
|
|
6115
|
-
program.addCommand(
|
|
6116
|
-
program.addCommand(
|
|
6117
|
-
program.addCommand(
|
|
6118
|
-
program.
|
|
7440
|
+
var CLI_VERSION_FLAGS = "-V, --cli-version";
|
|
7441
|
+
function isBareVersionRequest(args) {
|
|
7442
|
+
return args.includes("--version") && args.every((arg) => arg.startsWith("-"));
|
|
7443
|
+
}
|
|
7444
|
+
function buildProgram() {
|
|
7445
|
+
const program = new Command25();
|
|
7446
|
+
program.name("exponential").description(
|
|
7447
|
+
[
|
|
7448
|
+
"CLI to interact with Exponential productivity app.",
|
|
7449
|
+
"",
|
|
7450
|
+
"Work-tracking hierarchy:",
|
|
7451
|
+
" workspace -> product -> feature -> ticket -> action",
|
|
7452
|
+
" epics are workspace-scoped and can group tickets and actions across products."
|
|
7453
|
+
].join("\n")
|
|
7454
|
+
).version(PKG_VERSION, CLI_VERSION_FLAGS, "Output the CLI version").option("--json", "Output as JSON (default when piped)").option("--pretty", "Force pretty-printed output");
|
|
7455
|
+
program.addCommand(createAuthCommand());
|
|
7456
|
+
program.addCommand(createActionsCommand());
|
|
7457
|
+
program.addCommand(createContactsCommand());
|
|
7458
|
+
program.addCommand(createDecisionsCommand());
|
|
7459
|
+
program.addCommand(createDealsCommand());
|
|
7460
|
+
program.addCommand(createEpicsCommand());
|
|
7461
|
+
program.addCommand(createFeaturesCommand());
|
|
7462
|
+
program.addCommand(createLabelsCommand());
|
|
7463
|
+
program.addCommand(createMeetingsCommand());
|
|
7464
|
+
program.addCommand(createOrganizationsCommand());
|
|
7465
|
+
program.addCommand(createPagesCommand());
|
|
7466
|
+
program.addCommand(createProductsCommand());
|
|
7467
|
+
program.addCommand(createProjectsCommand());
|
|
7468
|
+
program.addCommand(createSearchCommand());
|
|
7469
|
+
program.addCommand(createTicketsCommand());
|
|
7470
|
+
program.addCommand(createTimeCommand());
|
|
7471
|
+
program.addCommand(createWorkspacesCommand());
|
|
7472
|
+
program.addCommand(createGoalsCommand());
|
|
7473
|
+
program.addCommand(createOkrsCommand());
|
|
7474
|
+
return program;
|
|
7475
|
+
}
|
|
7476
|
+
|
|
7477
|
+
// src/index.ts
|
|
7478
|
+
if (isBareVersionRequest(process.argv.slice(2))) {
|
|
7479
|
+
console.log(PKG_VERSION);
|
|
7480
|
+
} else {
|
|
7481
|
+
buildProgram().parse();
|
|
7482
|
+
}
|
|
6119
7483
|
//# sourceMappingURL=index.js.map
|