exponential-cli 1.11.1 → 1.11.2

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 CHANGED
@@ -130,14 +130,69 @@ exponential actions reschedule --ids id1,id2 --to 2026-08-10
130
130
  > would report 6 actions while 43 sat overdue. Pass `--due-only` for the
131
131
  > previous output shape.
132
132
 
133
+ ### Goals and OKRs
134
+
135
+ Objectives are goals; they carry **integer** ids. Their key results carry CUIDs
136
+ and live under `goals kr`.
137
+
138
+ ```bash
139
+ # List a workspace's objectives, or the annual → quarterly cascade
140
+ exponential goals list --workspace clear --period Q3-2026
141
+ exponential goals list --workspace clear --tree
142
+ exponential goals list --all-workspaces # "what am I neglecting" is cross-workspace
143
+
144
+ exponential goals get 46
145
+ exponential goals create --workspace clear --title "Ship the CLI" --period Q3-2026
146
+
147
+ # Close a quarter. set-status/close write ONLY the status column.
148
+ exponential goals close --id 46 # → completed
149
+ exponential goals set-status --id 46 --status on-hold
150
+
151
+ # Everything else is a partial update: fields you don't pass are left alone,
152
+ # and "none" is how you clear one.
153
+ exponential goals update --id 46 --title "Ship the CLI, properly"
154
+ exponential goals update --id 46 --workspace none # make it personal
155
+ exponential goals reparent --id 47 --parent 46 # or --parent none
156
+ exponential goals delete --id 46 # refuses if it has key results
157
+
158
+ exponential goals periods
159
+ exponential goals stats --workspace clear --period Q3-2026
160
+
161
+ # The quarter at a glance: objectives with their key results nested
162
+ exponential okrs list --workspace clear
163
+ ```
164
+
165
+ Key results:
166
+
167
+ ```bash
168
+ exponential goals kr list --goal 46
169
+ exponential goals kr list --workspace clear --status at-risk
170
+ exponential goals kr create --goal 46 --title "Weekly active agents" --target 500 --unit count
171
+ exponential goals kr checkin --id <cuid> --value 120 --note "post-launch bump"
172
+ exponential goals kr update --id <cuid> --target 600
173
+ exponential goals kr link --id <cuid> --feature <feature-cuid> # or --project <cuid>
174
+ exponential goals kr unlink --id <cuid> --project <cuid>
175
+ exponential goals kr delete --id <cuid>
176
+ ```
177
+
133
178
  ### Projects
134
179
 
135
180
  ```bash
136
181
  # List all projects
137
182
  exponential projects list
138
183
 
139
- # List projects in a workspace
140
- exponential projects list --workspace <workspace-id>
184
+ # List projects in a workspace (slug or CUID)
185
+ exponential projects list --workspace clear
186
+
187
+ # One project, with its objectives, key results, DRI and actions
188
+ exponential projects get <cuid|slug>
189
+
190
+ # Partial update — unnamed fields are preserved
191
+ exponential projects update --id <cuid> --name "KR support" --status COMPLETED
192
+ exponential projects update --id <cuid> --product none
193
+
194
+ # Refuses while the project still has actions or OKR links
195
+ exponential projects delete --id <cuid> [--force]
141
196
  ```
142
197
 
143
198
  ### Workspaces
package/dist/index.js CHANGED
@@ -1748,6 +1748,63 @@ OKRs (${objectives.length} objectives, ${totalKeyResults} key results)`)
1748
1748
  }
1749
1749
  console.log();
1750
1750
  }
1751
+ function outputProjectJson(project) {
1752
+ console.log(JSON.stringify({
1753
+ ...transformProject(project),
1754
+ // The detail read has no `workspace` relation to expand, unlike the list
1755
+ // read — so `transformProject` alone would emit `workspace: null` and no id
1756
+ // at all, which reads as "personal project" rather than "not fetched".
1757
+ workspaceId: project.workspaceId,
1758
+ slug: project.slug ?? null,
1759
+ productId: project.productId ?? null,
1760
+ driId: project.driId ?? null,
1761
+ dri: project.dri ?? null,
1762
+ team: project.team ?? null,
1763
+ goals: project.goals ?? [],
1764
+ keyResults: project.keyResults?.map((k) => ({
1765
+ id: k.keyResultId,
1766
+ title: k.keyResult?.title ?? null,
1767
+ goal: k.keyResult?.goal ?? null
1768
+ })) ?? [],
1769
+ outcomes: project.outcomes ?? [],
1770
+ actionCount: project.actions?.length ?? null,
1771
+ createdAt: project.createdAt ? new Date(project.createdAt).toISOString() : null
1772
+ }, null, 2));
1773
+ }
1774
+ function outputProjectPretty(project) {
1775
+ console.log(chalk3.gray("\u2500".repeat(50)));
1776
+ console.log(`
1777
+ ${chalk3.bold(project.name)}`);
1778
+ console.log(chalk3.gray(` ID: ${project.id}`));
1779
+ if (project.status) console.log(` ${chalk3.cyan("Status:")} ${project.status}`);
1780
+ if (project.priority) console.log(` ${chalk3.magenta("Priority:")} ${project.priority}`);
1781
+ if (project.workspace) {
1782
+ console.log(` ${chalk3.yellow("Workspace:")} ${project.workspace.name} (${project.workspace.slug})`);
1783
+ }
1784
+ if (project.description) {
1785
+ console.log(` ${chalk3.gray("Description:")} ${project.description.substring(0, 200)}${project.description.length > 200 ? "..." : ""}`);
1786
+ }
1787
+ if (project.dri) {
1788
+ console.log(` ${chalk3.magenta("DRI:")} ${project.dri.name ?? project.dri.email ?? project.dri.id}`);
1789
+ }
1790
+ if (project.goals?.length) {
1791
+ console.log(` ${chalk3.cyan("Objectives:")}`);
1792
+ for (const g of project.goals) {
1793
+ console.log(` - #${g.id} ${g.title}`);
1794
+ }
1795
+ }
1796
+ if (project.keyResults?.length) {
1797
+ console.log(` ${chalk3.cyan("Key results:")}`);
1798
+ for (const k of project.keyResults) {
1799
+ const objective = k.keyResult?.goal ? chalk3.gray(` (objective #${k.keyResult.goal.id})`) : "";
1800
+ console.log(` - ${k.keyResult?.title ?? k.keyResultId} ${chalk3.gray(`[${k.keyResultId}]`)}${objective}`);
1801
+ }
1802
+ }
1803
+ if (project.actions?.length) {
1804
+ console.log(` ${chalk3.cyan("Actions:")} ${project.actions.length}`);
1805
+ }
1806
+ console.log();
1807
+ }
1751
1808
 
1752
1809
  // src/commands/actions.ts
1753
1810
  var VALID_PRIORITIES = [
@@ -3279,6 +3336,17 @@ function summarizeTicket(ticket) {
3279
3336
  function ticketLabel(ticket) {
3280
3337
  return ticket.shortId ?? (ticket.number != null ? `#${ticket.number}` : ticket.id);
3281
3338
  }
3339
+ function parseFeatureGoalId(value) {
3340
+ if (value === void 0) return void 0;
3341
+ if (value === "none") return null;
3342
+ const id = Number(value);
3343
+ if (!Number.isInteger(id)) {
3344
+ throw new Error(
3345
+ `--goal must be a whole number or "none", got "${value}". Find one with \`exponential goals list\`.`
3346
+ );
3347
+ }
3348
+ return id;
3349
+ }
3282
3350
  function validateFeatureStatus(value) {
3283
3351
  if (!value) return void 0;
3284
3352
  if (!FEATURE_STATUSES.includes(value)) {
@@ -3358,7 +3426,10 @@ function createFeaturesCommand() {
3358
3426
  }
3359
3427
  }
3360
3428
  );
3361
- features.command("update").description("Update an existing feature").requiredOption("--id <id>", "Feature CUID").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("--vision <text>", "New vision text").option("--status <status>", `Status: ${FEATURE_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4", parseInt).option("--effort <n>", "Effort estimate", parseFloat).option("--area <id>", 'Area CUID ("none" to clear)').action(
3429
+ features.command("update").description("Update an existing feature").requiredOption("--id <id>", "Feature CUID").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("--vision <text>", "New vision text").option("--status <status>", `Status: ${FEATURE_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4", parseInt).option("--effort <n>", "Effort estimate", parseFloat).option("--area <id>", 'Area CUID ("none" to clear)').option(
3430
+ "--goal <id|none>",
3431
+ 'Objective (goal) id this feature serves ("none" to clear). For progress rollup, link the finer Feature\u2192key-result edge with `goals kr link --feature`.'
3432
+ ).action(
3362
3433
  async (options, cmd) => {
3363
3434
  const globalOpts = cmd.optsWithGlobals();
3364
3435
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3373,7 +3444,8 @@ function createFeaturesCommand() {
3373
3444
  status,
3374
3445
  priority: options.priority,
3375
3446
  effort: options.effort,
3376
- areaId: options.area === "none" ? null : options.area
3447
+ areaId: options.area === "none" ? null : options.area,
3448
+ goalId: parseFeatureGoalId(options.goal)
3377
3449
  });
3378
3450
  if (useJson) outputFeatureJson(feature);
3379
3451
  else {
@@ -4033,15 +4105,41 @@ function createProductsCommand() {
4033
4105
 
4034
4106
  // src/commands/projects.ts
4035
4107
  import { Command as Command17 } from "commander";
4108
+ var PROJECT_STATUSES = [
4109
+ "ACTIVE",
4110
+ "ON_HOLD",
4111
+ "COMPLETED",
4112
+ "CANCELLED"
4113
+ ];
4114
+ var PROJECT_PRIORITIES = ["HIGH", "MEDIUM", "LOW", "NONE"];
4115
+ function validateProjectStatus(value) {
4116
+ if (!value) return void 0;
4117
+ if (!PROJECT_STATUSES.includes(value)) {
4118
+ throw new Error(
4119
+ `Invalid project status "${value}". Valid: ${PROJECT_STATUSES.join(", ")}`
4120
+ );
4121
+ }
4122
+ return value;
4123
+ }
4124
+ function validateProjectPriority(value) {
4125
+ if (!value) return void 0;
4126
+ if (!PROJECT_PRIORITIES.includes(value)) {
4127
+ throw new Error(
4128
+ `Invalid project priority "${value}". Valid: ${PROJECT_PRIORITIES.join(", ")}`
4129
+ );
4130
+ }
4131
+ return value;
4132
+ }
4036
4133
  function createProjectsCommand() {
4037
4134
  const projects = new Command17("projects").description("Manage projects");
4038
- projects.command("list").description("List all projects").option("--workspace <id>", "Filter by workspace ID").option("--include-actions", "Include actions in output").action(async (options, cmd) => {
4135
+ 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) => {
4039
4136
  const globalOpts = cmd.optsWithGlobals();
4040
4137
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4041
4138
  try {
4042
4139
  const client = getClient();
4140
+ const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
4043
4141
  const projects2 = await client.projects.list({
4044
- workspaceId: options.workspace,
4142
+ workspaceId,
4045
4143
  includeActions: options.includeActions
4046
4144
  });
4047
4145
  if (useJson) {
@@ -4053,6 +4151,101 @@ function createProjectsCommand() {
4053
4151
  handleError(error, useJson);
4054
4152
  }
4055
4153
  });
4154
+ projects.command("get <id>").description(
4155
+ "Get one project with its objectives, key results, DRI and actions \u2014 none of which `projects list` carries. Accepts a CUID, a slug, or the slug-cuid form from app URLs."
4156
+ ).action(async (id, _options, cmd) => {
4157
+ const globalOpts = cmd.optsWithGlobals();
4158
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4159
+ try {
4160
+ const client = getClient();
4161
+ const project = await client.projects.get(id);
4162
+ if (useJson) outputProjectJson(project);
4163
+ else outputProjectPretty(project);
4164
+ } catch (error) {
4165
+ handleError(error, useJson);
4166
+ }
4167
+ });
4168
+ projects.command("update").description(
4169
+ "Update a project. Only the fields you pass are written; the rest are read back from the project and re-sent unchanged."
4170
+ ).requiredOption("--id <cuid>", "Project CUID").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("--status <status>", `Status: ${PROJECT_STATUSES.join(", ")}`).option("--priority <priority>", `Priority: ${PROJECT_PRIORITIES.join(", ")}`).option("--product <cuid|none>", 'Product to file the project under ("none" to unlink)').option("--dri <userId|none>", 'Directly responsible individual ("none" to clear)').action(
4171
+ async (options, cmd) => {
4172
+ const globalOpts = cmd.optsWithGlobals();
4173
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4174
+ try {
4175
+ const status = validateProjectStatus(options.status);
4176
+ const priority = validateProjectPriority(options.priority);
4177
+ const client = getClient();
4178
+ const updated = await client.projects.update({
4179
+ id: options.id,
4180
+ name: options.name,
4181
+ description: options.description,
4182
+ status,
4183
+ priority,
4184
+ productId: options.product === void 0 ? void 0 : options.product === "none" ? null : options.product,
4185
+ driId: options.dri === void 0 ? void 0 : options.dri === "none" ? null : options.dri
4186
+ });
4187
+ if (useJson) outputProjectJson(updated);
4188
+ else {
4189
+ console.log("\n\u2713 Project updated");
4190
+ outputProjectPretty(updated);
4191
+ }
4192
+ } catch (error) {
4193
+ handleError(error, useJson);
4194
+ }
4195
+ }
4196
+ );
4197
+ projects.command("delete").description(
4198
+ "Delete a project. Refuses while it still has actions or is linked to an objective or key result, unless --force."
4199
+ ).requiredOption("--id <cuid>", "Project CUID").option("--force", "Delete anyway, taking the actions and links with it").action(async (options, cmd) => {
4200
+ const globalOpts = cmd.optsWithGlobals();
4201
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4202
+ try {
4203
+ const client = getClient();
4204
+ const project = await client.projects.get(options.id);
4205
+ const actions = project.actions ?? [];
4206
+ const goals = project.goals ?? [];
4207
+ const keyResults = project.keyResults ?? [];
4208
+ const blockers = [
4209
+ actions.length ? `${actions.length} action(s)` : null,
4210
+ goals.length ? `${goals.length} objective link(s)` : null,
4211
+ keyResults.length ? `${keyResults.length} key result link(s)` : null
4212
+ ].filter(Boolean);
4213
+ if (blockers.length > 0 && !options.force) {
4214
+ const payload2 = {
4215
+ deleted: false,
4216
+ id: project.id,
4217
+ reason: `has ${blockers.join(", ")}`,
4218
+ actionCount: actions.length,
4219
+ goals: goals.map((g) => ({ id: g.id, title: g.title })),
4220
+ keyResults: keyResults.map((k) => ({ id: k.keyResultId }))
4221
+ };
4222
+ if (useJson) console.log(JSON.stringify(payload2, null, 2));
4223
+ else {
4224
+ console.error(
4225
+ `Project "${project.name}" has ${blockers.join(", ")}.
4226
+ Re-run with --force to delete it anyway.`
4227
+ );
4228
+ }
4229
+ process.exitCode = 1;
4230
+ return;
4231
+ }
4232
+ await client.projects.delete(project.id);
4233
+ const payload = {
4234
+ deleted: true,
4235
+ id: project.id,
4236
+ actionsDeleted: actions.length
4237
+ };
4238
+ if (useJson) console.log(JSON.stringify(payload, null, 2));
4239
+ else {
4240
+ console.log("\u2713 Project deleted");
4241
+ if (actions.length) {
4242
+ console.log(` ${actions.length} action(s) went with it`);
4243
+ }
4244
+ }
4245
+ } catch (error) {
4246
+ handleError(error, useJson);
4247
+ }
4248
+ });
4056
4249
  return projects;
4057
4250
  }
4058
4251
 
@@ -4809,10 +5002,17 @@ function createKeyResultsCommand() {
4809
5002
  try {
4810
5003
  const status = validateKeyResultStatus(options.status);
4811
5004
  const client = getClient();
4812
- const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
5005
+ const goalId = options.goal ? parseGoalId(options.goal) : void 0;
5006
+ let workspaceId;
5007
+ if (options.workspace) {
5008
+ workspaceId = await resolveWorkspaceId(client, options.workspace);
5009
+ } else if (goalId !== void 0) {
5010
+ const goal = await client.goals.get(goalId);
5011
+ workspaceId = goal.workspaceId ?? void 0;
5012
+ }
4813
5013
  const list = await client.goals.keyResults.list({
4814
5014
  workspaceId,
4815
- goalId: options.goal ? parseGoalId(options.goal) : void 0,
5015
+ goalId,
4816
5016
  period: options.period,
4817
5017
  status,
4818
5018
  onlyMine: options.mine
@@ -5170,15 +5370,25 @@ function createGoalsCommand() {
5170
5370
  );
5171
5371
  goals.command("update").description(
5172
5372
  "Update an objective. Only the flags you pass are written \u2014 everything else is left exactly as it is. For a status change use `set-status`; to re-parent use `reparent`."
5173
- ).requiredOption("--id <n>", "Objective ID (a number)").option("-t, --title <text>", "New title").option("-d, --description <text>", 'New description ("none" clears it)').option("--why <text>", 'Why this goal matters ("none" clears it)').option("--period <period>", 'New period ("none" clears it)').option("--dri <userId>", "Directly responsible individual (user ID)").option("--status <status>", `Status: ${GOAL_WRITABLE_STATUSES.join(", ")}`).option("--project <cuid>", 'Replace the project links with this project ("none" clears them)').action(
5373
+ ).requiredOption("--id <n>", "Objective ID (a number)").option("-t, --title <text>", "New title").option("-d, --description <text>", 'New description ("none" clears it)').option("--why <text>", 'Why this goal matters ("none" clears it)').option("--period <period>", 'New period ("none" clears it)').option("--dri <userId>", "Directly responsible individual (user ID)").option("--status <status>", `Status: ${GOAL_WRITABLE_STATUSES.join(", ")}`).option("--project <cuid>", 'Replace the project links with this project ("none" clears them)').option(
5374
+ "-w, --workspace <slug|id|none>",
5375
+ 'Re-home the objective; "none" makes it personal'
5376
+ ).action(
5174
5377
  async (options, cmd) => {
5175
5378
  const globalOpts = cmd.optsWithGlobals();
5176
5379
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5177
5380
  try {
5178
5381
  const status = validateWritableGoalStatus(options.status);
5179
5382
  const clearable = (value) => value === void 0 ? void 0 : value === "none" ? null : value;
5180
- const goal = await getClient().goals.update({
5383
+ const client = getClient();
5384
+ let workspaceId;
5385
+ if (options.workspace === "none") workspaceId = null;
5386
+ else if (options.workspace !== void 0) {
5387
+ workspaceId = await resolveWorkspaceId(client, options.workspace);
5388
+ }
5389
+ const goal = await client.goals.update({
5181
5390
  id: parseGoalId(options.id),
5391
+ workspaceId,
5182
5392
  title: options.title,
5183
5393
  description: clearable(options.description),
5184
5394
  whyThisGoal: clearable(options.why),
@@ -5258,21 +5468,60 @@ function createGoalsCommand() {
5258
5468
  handleError(error, useJson);
5259
5469
  }
5260
5470
  });
5261
- goals.command("delete").description("Delete an objective").requiredOption("--id <n>", "Objective ID (a number)").action(async (options, cmd) => {
5262
- const globalOpts = cmd.optsWithGlobals();
5263
- const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5264
- try {
5265
- const id = parseGoalId(options.id);
5266
- await getClient().goals.delete(id);
5267
- if (useJson) {
5268
- console.log(JSON.stringify({ deleted: true, id }, null, 2));
5269
- } else {
5270
- console.log("\u2713 Goal deleted");
5471
+ goals.command("delete").description(
5472
+ "Delete an objective. Refuses while it still has key results \u2014 deleting an objective deletes them too (KeyResult.goalId cascades), taking their check-in history with them."
5473
+ ).requiredOption("--id <n>", "Objective ID (a number)").option(
5474
+ "--with-key-results",
5475
+ "Delete the objective and cascade-delete its key results (destructive)"
5476
+ ).action(
5477
+ async (options, cmd) => {
5478
+ const globalOpts = cmd.optsWithGlobals();
5479
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5480
+ try {
5481
+ const id = parseGoalId(options.id);
5482
+ const client = getClient();
5483
+ const goal = await client.goals.get(id);
5484
+ const keyResults = goal.keyResults ?? [];
5485
+ const children = goal.childGoals ?? [];
5486
+ if (keyResults.length > 0 && !options.withKeyResults) {
5487
+ const payload2 = {
5488
+ deleted: false,
5489
+ id,
5490
+ reason: `has ${keyResults.length} key result(s)`,
5491
+ keyResults: keyResults.map((kr) => ({ id: kr.id }))
5492
+ };
5493
+ if (useJson) console.log(JSON.stringify(payload2, null, 2));
5494
+ else {
5495
+ console.error(
5496
+ `Objective #${id} has ${keyResults.length} key result(s). Deleting it deletes them too.
5497
+ Re-run with --with-key-results to go ahead, or move them with \`goals kr update --id <cuid> --goal <other>\`.`
5498
+ );
5499
+ }
5500
+ process.exitCode = 1;
5501
+ return;
5502
+ }
5503
+ await client.goals.delete(id);
5504
+ const payload = {
5505
+ deleted: true,
5506
+ id,
5507
+ keyResultsDeleted: keyResults.map((kr) => ({ id: kr.id })),
5508
+ childGoalsDetached: children.map((c) => ({ id: c.id, title: c.title }))
5509
+ };
5510
+ if (useJson) console.log(JSON.stringify(payload, null, 2));
5511
+ else {
5512
+ console.log("\u2713 Goal deleted");
5513
+ if (keyResults.length) {
5514
+ console.log(` ${keyResults.length} key result(s) deleted with it`);
5515
+ }
5516
+ if (children.length) {
5517
+ console.log(` ${children.length} sub-goal(s) left detached`);
5518
+ }
5519
+ }
5520
+ } catch (error) {
5521
+ handleError(error, useJson);
5271
5522
  }
5272
- } catch (error) {
5273
- handleError(error, useJson);
5274
5523
  }
5275
- });
5524
+ );
5276
5525
  goals.command("periods").description("List the conventional OKR period strings").action(async (_options, cmd) => {
5277
5526
  const globalOpts = cmd.optsWithGlobals();
5278
5527
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);