exponential-cli 1.11.1 → 1.12.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 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
@@ -899,6 +899,17 @@ ${chalk3[getFeatureStatusColor(feature.status)](`[${feature.status}]`)} ${chalk3
899
899
  if (feature.priority != null) {
900
900
  console.log(` ${chalk3.magenta("Priority:")} ${feature.priority}`);
901
901
  }
902
+ if (feature.goal) {
903
+ console.log(` ${chalk3.blue("Objective:")} ${feature.goal.title} ${chalk3.gray(`(${feature.goal.id})`)}`);
904
+ }
905
+ if (feature.keyResultLinks?.length) {
906
+ console.log(` ${chalk3.blue("Key results:")}`);
907
+ for (const link of feature.keyResultLinks) {
908
+ const kr = link.keyResult;
909
+ const progress = kr.currentValue != null && kr.targetValue != null ? `${kr.currentValue}/${kr.targetValue}${kr.unit && kr.unit !== "custom" ? ` ${kr.unit}` : ""}, ` : "";
910
+ console.log(` \xB7 ${kr.title} ${chalk3.gray(`(${progress}${kr.period}, ${kr.id})`)}`);
911
+ }
912
+ }
902
913
  if (feature._count?.tickets != null) {
903
914
  console.log(` ${chalk3.cyan("Tickets:")} ${feature._count.tickets}`);
904
915
  }
@@ -923,9 +934,26 @@ Features (${features.length} total)`));
923
934
  const ticketCount = f._count?.tickets != null ? chalk3.gray(` \u2014 ${f._count.tickets} tickets`) : "";
924
935
  console.log(` ${statusBadge} ${chalk3.bold(f.name)}${ticketCount}`);
925
936
  console.log(chalk3.gray(` ID: ${f.id}`));
937
+ if (f.keyResultLinks?.length) {
938
+ const titles = f.keyResultLinks.map((l) => l.keyResult.title).join("; ");
939
+ console.log(chalk3.gray(` KRs: ${titles}`));
940
+ }
926
941
  }
927
942
  console.log();
928
943
  }
944
+ function summarizeFeatureKeyResult(link) {
945
+ const kr = link.keyResult;
946
+ return {
947
+ keyResultId: kr.id,
948
+ title: kr.title,
949
+ period: kr.period,
950
+ goalId: kr.goalId,
951
+ status: kr.status ?? null,
952
+ currentValue: kr.currentValue ?? null,
953
+ targetValue: kr.targetValue ?? null,
954
+ unit: kr.unit ?? null
955
+ };
956
+ }
929
957
  function transformFeature(feature) {
930
958
  return {
931
959
  id: feature.id,
@@ -938,6 +966,12 @@ function transformFeature(feature) {
938
966
  priority: feature.priority,
939
967
  goalId: feature.goalId,
940
968
  goal: feature.goal ?? null,
969
+ // Flattened out of the join row (ADR-0050): callers building a per-feature
970
+ // "which key results does this move" column want the key results, not the
971
+ // edge rows. `goal` above is the coarser Objective alignment and is a
972
+ // different question - the two are allowed to disagree, so both ride along.
973
+ // Progress values are present on `features get` and absent on `list`.
974
+ keyResults: feature.keyResultLinks?.map(summarizeFeatureKeyResult) ?? null,
941
975
  counts: feature._count ?? null,
942
976
  createdAt: new Date(feature.createdAt).toISOString(),
943
977
  updatedAt: new Date(feature.updatedAt).toISOString()
@@ -1748,6 +1782,63 @@ OKRs (${objectives.length} objectives, ${totalKeyResults} key results)`)
1748
1782
  }
1749
1783
  console.log();
1750
1784
  }
1785
+ function outputProjectJson(project) {
1786
+ console.log(JSON.stringify({
1787
+ ...transformProject(project),
1788
+ // The detail read has no `workspace` relation to expand, unlike the list
1789
+ // read — so `transformProject` alone would emit `workspace: null` and no id
1790
+ // at all, which reads as "personal project" rather than "not fetched".
1791
+ workspaceId: project.workspaceId,
1792
+ slug: project.slug ?? null,
1793
+ productId: project.productId ?? null,
1794
+ driId: project.driId ?? null,
1795
+ dri: project.dri ?? null,
1796
+ team: project.team ?? null,
1797
+ goals: project.goals ?? [],
1798
+ keyResults: project.keyResults?.map((k) => ({
1799
+ id: k.keyResultId,
1800
+ title: k.keyResult?.title ?? null,
1801
+ goal: k.keyResult?.goal ?? null
1802
+ })) ?? [],
1803
+ outcomes: project.outcomes ?? [],
1804
+ actionCount: project.actions?.length ?? null,
1805
+ createdAt: project.createdAt ? new Date(project.createdAt).toISOString() : null
1806
+ }, null, 2));
1807
+ }
1808
+ function outputProjectPretty(project) {
1809
+ console.log(chalk3.gray("\u2500".repeat(50)));
1810
+ console.log(`
1811
+ ${chalk3.bold(project.name)}`);
1812
+ console.log(chalk3.gray(` ID: ${project.id}`));
1813
+ if (project.status) console.log(` ${chalk3.cyan("Status:")} ${project.status}`);
1814
+ if (project.priority) console.log(` ${chalk3.magenta("Priority:")} ${project.priority}`);
1815
+ if (project.workspace) {
1816
+ console.log(` ${chalk3.yellow("Workspace:")} ${project.workspace.name} (${project.workspace.slug})`);
1817
+ }
1818
+ if (project.description) {
1819
+ console.log(` ${chalk3.gray("Description:")} ${project.description.substring(0, 200)}${project.description.length > 200 ? "..." : ""}`);
1820
+ }
1821
+ if (project.dri) {
1822
+ console.log(` ${chalk3.magenta("DRI:")} ${project.dri.name ?? project.dri.email ?? project.dri.id}`);
1823
+ }
1824
+ if (project.goals?.length) {
1825
+ console.log(` ${chalk3.cyan("Objectives:")}`);
1826
+ for (const g of project.goals) {
1827
+ console.log(` - #${g.id} ${g.title}`);
1828
+ }
1829
+ }
1830
+ if (project.keyResults?.length) {
1831
+ console.log(` ${chalk3.cyan("Key results:")}`);
1832
+ for (const k of project.keyResults) {
1833
+ const objective = k.keyResult?.goal ? chalk3.gray(` (objective #${k.keyResult.goal.id})`) : "";
1834
+ console.log(` - ${k.keyResult?.title ?? k.keyResultId} ${chalk3.gray(`[${k.keyResultId}]`)}${objective}`);
1835
+ }
1836
+ }
1837
+ if (project.actions?.length) {
1838
+ console.log(` ${chalk3.cyan("Actions:")} ${project.actions.length}`);
1839
+ }
1840
+ console.log();
1841
+ }
1751
1842
 
1752
1843
  // src/commands/actions.ts
1753
1844
  var VALID_PRIORITIES = [
@@ -3279,6 +3370,17 @@ function summarizeTicket(ticket) {
3279
3370
  function ticketLabel(ticket) {
3280
3371
  return ticket.shortId ?? (ticket.number != null ? `#${ticket.number}` : ticket.id);
3281
3372
  }
3373
+ function parseFeatureGoalId(value) {
3374
+ if (value === void 0) return void 0;
3375
+ if (value === "none") return null;
3376
+ const id = Number(value);
3377
+ if (!Number.isInteger(id)) {
3378
+ throw new Error(
3379
+ `--goal must be a whole number or "none", got "${value}". Find one with \`exponential goals list\`.`
3380
+ );
3381
+ }
3382
+ return id;
3383
+ }
3282
3384
  function validateFeatureStatus(value) {
3283
3385
  if (!value) return void 0;
3284
3386
  if (!FEATURE_STATUSES.includes(value)) {
@@ -3358,7 +3460,10 @@ function createFeaturesCommand() {
3358
3460
  }
3359
3461
  }
3360
3462
  );
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(
3463
+ 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(
3464
+ "--goal <id|none>",
3465
+ 'Objective (goal) id this feature serves ("none" to clear). For progress rollup, link the finer Feature\u2192key-result edge with `goals kr link --feature`.'
3466
+ ).action(
3362
3467
  async (options, cmd) => {
3363
3468
  const globalOpts = cmd.optsWithGlobals();
3364
3469
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3373,7 +3478,8 @@ function createFeaturesCommand() {
3373
3478
  status,
3374
3479
  priority: options.priority,
3375
3480
  effort: options.effort,
3376
- areaId: options.area === "none" ? null : options.area
3481
+ areaId: options.area === "none" ? null : options.area,
3482
+ goalId: parseFeatureGoalId(options.goal)
3377
3483
  });
3378
3484
  if (useJson) outputFeatureJson(feature);
3379
3485
  else {
@@ -4033,15 +4139,41 @@ function createProductsCommand() {
4033
4139
 
4034
4140
  // src/commands/projects.ts
4035
4141
  import { Command as Command17 } from "commander";
4142
+ var PROJECT_STATUSES = [
4143
+ "ACTIVE",
4144
+ "ON_HOLD",
4145
+ "COMPLETED",
4146
+ "CANCELLED"
4147
+ ];
4148
+ var PROJECT_PRIORITIES = ["HIGH", "MEDIUM", "LOW", "NONE"];
4149
+ function validateProjectStatus(value) {
4150
+ if (!value) return void 0;
4151
+ if (!PROJECT_STATUSES.includes(value)) {
4152
+ throw new Error(
4153
+ `Invalid project status "${value}". Valid: ${PROJECT_STATUSES.join(", ")}`
4154
+ );
4155
+ }
4156
+ return value;
4157
+ }
4158
+ function validateProjectPriority(value) {
4159
+ if (!value) return void 0;
4160
+ if (!PROJECT_PRIORITIES.includes(value)) {
4161
+ throw new Error(
4162
+ `Invalid project priority "${value}". Valid: ${PROJECT_PRIORITIES.join(", ")}`
4163
+ );
4164
+ }
4165
+ return value;
4166
+ }
4036
4167
  function createProjectsCommand() {
4037
4168
  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) => {
4169
+ 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
4170
  const globalOpts = cmd.optsWithGlobals();
4040
4171
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4041
4172
  try {
4042
4173
  const client = getClient();
4174
+ const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
4043
4175
  const projects2 = await client.projects.list({
4044
- workspaceId: options.workspace,
4176
+ workspaceId,
4045
4177
  includeActions: options.includeActions
4046
4178
  });
4047
4179
  if (useJson) {
@@ -4053,6 +4185,101 @@ function createProjectsCommand() {
4053
4185
  handleError(error, useJson);
4054
4186
  }
4055
4187
  });
4188
+ projects.command("get <id>").description(
4189
+ "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."
4190
+ ).action(async (id, _options, cmd) => {
4191
+ const globalOpts = cmd.optsWithGlobals();
4192
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4193
+ try {
4194
+ const client = getClient();
4195
+ const project = await client.projects.get(id);
4196
+ if (useJson) outputProjectJson(project);
4197
+ else outputProjectPretty(project);
4198
+ } catch (error) {
4199
+ handleError(error, useJson);
4200
+ }
4201
+ });
4202
+ projects.command("update").description(
4203
+ "Update a project. Only the fields you pass are written; the rest are read back from the project and re-sent unchanged."
4204
+ ).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(
4205
+ async (options, cmd) => {
4206
+ const globalOpts = cmd.optsWithGlobals();
4207
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4208
+ try {
4209
+ const status = validateProjectStatus(options.status);
4210
+ const priority = validateProjectPriority(options.priority);
4211
+ const client = getClient();
4212
+ const updated = await client.projects.update({
4213
+ id: options.id,
4214
+ name: options.name,
4215
+ description: options.description,
4216
+ status,
4217
+ priority,
4218
+ productId: options.product === void 0 ? void 0 : options.product === "none" ? null : options.product,
4219
+ driId: options.dri === void 0 ? void 0 : options.dri === "none" ? null : options.dri
4220
+ });
4221
+ if (useJson) outputProjectJson(updated);
4222
+ else {
4223
+ console.log("\n\u2713 Project updated");
4224
+ outputProjectPretty(updated);
4225
+ }
4226
+ } catch (error) {
4227
+ handleError(error, useJson);
4228
+ }
4229
+ }
4230
+ );
4231
+ projects.command("delete").description(
4232
+ "Delete a project. Refuses while it still has actions or is linked to an objective or key result, unless --force."
4233
+ ).requiredOption("--id <cuid>", "Project CUID").option("--force", "Delete anyway, taking the actions and links with it").action(async (options, cmd) => {
4234
+ const globalOpts = cmd.optsWithGlobals();
4235
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4236
+ try {
4237
+ const client = getClient();
4238
+ const project = await client.projects.get(options.id);
4239
+ const actions = project.actions ?? [];
4240
+ const goals = project.goals ?? [];
4241
+ const keyResults = project.keyResults ?? [];
4242
+ const blockers = [
4243
+ actions.length ? `${actions.length} action(s)` : null,
4244
+ goals.length ? `${goals.length} objective link(s)` : null,
4245
+ keyResults.length ? `${keyResults.length} key result link(s)` : null
4246
+ ].filter(Boolean);
4247
+ if (blockers.length > 0 && !options.force) {
4248
+ const payload2 = {
4249
+ deleted: false,
4250
+ id: project.id,
4251
+ reason: `has ${blockers.join(", ")}`,
4252
+ actionCount: actions.length,
4253
+ goals: goals.map((g) => ({ id: g.id, title: g.title })),
4254
+ keyResults: keyResults.map((k) => ({ id: k.keyResultId }))
4255
+ };
4256
+ if (useJson) console.log(JSON.stringify(payload2, null, 2));
4257
+ else {
4258
+ console.error(
4259
+ `Project "${project.name}" has ${blockers.join(", ")}.
4260
+ Re-run with --force to delete it anyway.`
4261
+ );
4262
+ }
4263
+ process.exitCode = 1;
4264
+ return;
4265
+ }
4266
+ await client.projects.delete(project.id);
4267
+ const payload = {
4268
+ deleted: true,
4269
+ id: project.id,
4270
+ actionsDeleted: actions.length
4271
+ };
4272
+ if (useJson) console.log(JSON.stringify(payload, null, 2));
4273
+ else {
4274
+ console.log("\u2713 Project deleted");
4275
+ if (actions.length) {
4276
+ console.log(` ${actions.length} action(s) went with it`);
4277
+ }
4278
+ }
4279
+ } catch (error) {
4280
+ handleError(error, useJson);
4281
+ }
4282
+ });
4056
4283
  return projects;
4057
4284
  }
4058
4285
 
@@ -4809,10 +5036,17 @@ function createKeyResultsCommand() {
4809
5036
  try {
4810
5037
  const status = validateKeyResultStatus(options.status);
4811
5038
  const client = getClient();
4812
- const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
5039
+ const goalId = options.goal ? parseGoalId(options.goal) : void 0;
5040
+ let workspaceId;
5041
+ if (options.workspace) {
5042
+ workspaceId = await resolveWorkspaceId(client, options.workspace);
5043
+ } else if (goalId !== void 0) {
5044
+ const goal = await client.goals.get(goalId);
5045
+ workspaceId = goal.workspaceId ?? void 0;
5046
+ }
4813
5047
  const list = await client.goals.keyResults.list({
4814
5048
  workspaceId,
4815
- goalId: options.goal ? parseGoalId(options.goal) : void 0,
5049
+ goalId,
4816
5050
  period: options.period,
4817
5051
  status,
4818
5052
  onlyMine: options.mine
@@ -5170,15 +5404,25 @@ function createGoalsCommand() {
5170
5404
  );
5171
5405
  goals.command("update").description(
5172
5406
  "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(
5407
+ ).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(
5408
+ "-w, --workspace <slug|id|none>",
5409
+ 'Re-home the objective; "none" makes it personal'
5410
+ ).action(
5174
5411
  async (options, cmd) => {
5175
5412
  const globalOpts = cmd.optsWithGlobals();
5176
5413
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5177
5414
  try {
5178
5415
  const status = validateWritableGoalStatus(options.status);
5179
5416
  const clearable = (value) => value === void 0 ? void 0 : value === "none" ? null : value;
5180
- const goal = await getClient().goals.update({
5417
+ const client = getClient();
5418
+ let workspaceId;
5419
+ if (options.workspace === "none") workspaceId = null;
5420
+ else if (options.workspace !== void 0) {
5421
+ workspaceId = await resolveWorkspaceId(client, options.workspace);
5422
+ }
5423
+ const goal = await client.goals.update({
5181
5424
  id: parseGoalId(options.id),
5425
+ workspaceId,
5182
5426
  title: options.title,
5183
5427
  description: clearable(options.description),
5184
5428
  whyThisGoal: clearable(options.why),
@@ -5258,21 +5502,60 @@ function createGoalsCommand() {
5258
5502
  handleError(error, useJson);
5259
5503
  }
5260
5504
  });
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");
5505
+ goals.command("delete").description(
5506
+ "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."
5507
+ ).requiredOption("--id <n>", "Objective ID (a number)").option(
5508
+ "--with-key-results",
5509
+ "Delete the objective and cascade-delete its key results (destructive)"
5510
+ ).action(
5511
+ async (options, cmd) => {
5512
+ const globalOpts = cmd.optsWithGlobals();
5513
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5514
+ try {
5515
+ const id = parseGoalId(options.id);
5516
+ const client = getClient();
5517
+ const goal = await client.goals.get(id);
5518
+ const keyResults = goal.keyResults ?? [];
5519
+ const children = goal.childGoals ?? [];
5520
+ if (keyResults.length > 0 && !options.withKeyResults) {
5521
+ const payload2 = {
5522
+ deleted: false,
5523
+ id,
5524
+ reason: `has ${keyResults.length} key result(s)`,
5525
+ keyResults: keyResults.map((kr) => ({ id: kr.id }))
5526
+ };
5527
+ if (useJson) console.log(JSON.stringify(payload2, null, 2));
5528
+ else {
5529
+ console.error(
5530
+ `Objective #${id} has ${keyResults.length} key result(s). Deleting it deletes them too.
5531
+ Re-run with --with-key-results to go ahead, or move them with \`goals kr update --id <cuid> --goal <other>\`.`
5532
+ );
5533
+ }
5534
+ process.exitCode = 1;
5535
+ return;
5536
+ }
5537
+ await client.goals.delete(id);
5538
+ const payload = {
5539
+ deleted: true,
5540
+ id,
5541
+ keyResultsDeleted: keyResults.map((kr) => ({ id: kr.id })),
5542
+ childGoalsDetached: children.map((c) => ({ id: c.id, title: c.title }))
5543
+ };
5544
+ if (useJson) console.log(JSON.stringify(payload, null, 2));
5545
+ else {
5546
+ console.log("\u2713 Goal deleted");
5547
+ if (keyResults.length) {
5548
+ console.log(` ${keyResults.length} key result(s) deleted with it`);
5549
+ }
5550
+ if (children.length) {
5551
+ console.log(` ${children.length} sub-goal(s) left detached`);
5552
+ }
5553
+ }
5554
+ } catch (error) {
5555
+ handleError(error, useJson);
5271
5556
  }
5272
- } catch (error) {
5273
- handleError(error, useJson);
5274
5557
  }
5275
- });
5558
+ );
5276
5559
  goals.command("periods").description("List the conventional OKR period strings").action(async (_options, cmd) => {
5277
5560
  const globalOpts = cmd.optsWithGlobals();
5278
5561
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);