@bussolabs/closeyourit-cli 0.14.0 → 0.16.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.
Files changed (39) hide show
  1. package/README.md +17 -0
  2. package/dist/commands/matrix/categories/create.d.ts +13 -0
  3. package/dist/commands/matrix/categories/create.js +33 -0
  4. package/dist/commands/matrix/categories/delete.d.ts +13 -0
  5. package/dist/commands/matrix/categories/delete.js +27 -0
  6. package/dist/commands/matrix/categories/list.d.ts +12 -0
  7. package/dist/commands/matrix/categories/list.js +27 -0
  8. package/dist/commands/matrix/categories/update.d.ts +14 -0
  9. package/dist/commands/matrix/categories/update.js +37 -0
  10. package/dist/commands/matrix/cells/clear.d.ts +18 -0
  11. package/dist/commands/matrix/cells/clear.js +34 -0
  12. package/dist/commands/matrix/cells/set.d.ts +15 -0
  13. package/dist/commands/matrix/cells/set.js +42 -0
  14. package/dist/commands/matrix/features/create.d.ts +16 -0
  15. package/dist/commands/matrix/features/create.js +40 -0
  16. package/dist/commands/matrix/features/delete.d.ts +13 -0
  17. package/dist/commands/matrix/features/delete.js +27 -0
  18. package/dist/commands/matrix/features/list.d.ts +13 -0
  19. package/dist/commands/matrix/features/list.js +35 -0
  20. package/dist/commands/matrix/features/show.d.ts +10 -0
  21. package/dist/commands/matrix/features/show.js +33 -0
  22. package/dist/commands/matrix/features/update.d.ts +17 -0
  23. package/dist/commands/matrix/features/update.js +45 -0
  24. package/dist/commands/matrix/list.d.ts +9 -0
  25. package/dist/commands/matrix/list.js +18 -0
  26. package/dist/commands/matrix/show.d.ts +16 -0
  27. package/dist/commands/matrix/show.js +63 -0
  28. package/dist/commands/matrix/statuses.d.ts +6 -0
  29. package/dist/commands/matrix/statuses.js +22 -0
  30. package/dist/commands/tickets/lease/claim.d.ts +14 -0
  31. package/dist/commands/tickets/lease/claim.js +71 -0
  32. package/dist/commands/tickets/lease/release.d.ts +13 -0
  33. package/dist/commands/tickets/lease/release.js +52 -0
  34. package/dist/commands/tickets/lease/renew.d.ts +14 -0
  35. package/dist/commands/tickets/lease/renew.js +71 -0
  36. package/dist/lib/lease.d.ts +44 -0
  37. package/dist/lib/lease.js +61 -0
  38. package/oclif.manifest.json +3929 -2997
  39. package/package.json +5 -2
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class MatrixShow extends BaseCommand {
3
+ static args: {
4
+ product: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<unknown>;
9
+ /**
10
+ * The first column holds `Category/Feature` — the very reference `matrix cells set` and
11
+ * `matrix features` take, so a row can be acted on without a second lookup.
12
+ */
13
+ private renderMatrix;
14
+ /** `-` = not set (different from an explicit "not applicable" status, which has its own code). */
15
+ private renderCell;
16
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ class MatrixShow extends base_1.BaseCommand {
7
+ static args = {
8
+ product: core_1.Args.string({ description: 'Product id (UUID) or group name', required: true }),
9
+ };
10
+ static description = 'Show a product feature matrix: features in rows, platforms in columns';
11
+ static examples = [
12
+ '<%= config.bin %> matrix show CloseYourIt',
13
+ '<%= config.bin %> matrix show CloseYourIt --json',
14
+ ];
15
+ async run() {
16
+ const { args } = await this.parse(MatrixShow);
17
+ const productId = await this.resolveGroupId(args.product);
18
+ const res = await this.api.get(`/cli/v1/product/matrix/${encodeURIComponent(productId)}`);
19
+ if (!this.jsonEnabled())
20
+ this.renderMatrix(res.data ?? {});
21
+ return res;
22
+ }
23
+ /**
24
+ * The first column holds `Category/Feature` — the very reference `matrix cells set` and
25
+ * `matrix features` take, so a row can be acted on without a second lookup.
26
+ */
27
+ renderMatrix(matrix) {
28
+ const platforms = matrix.platforms ?? [];
29
+ if (platforms.length === 0) {
30
+ this.log('No platforms yet: declare them on the products projects, or write a cell to open a column.');
31
+ }
32
+ const byCell = new Map();
33
+ for (const cell of matrix.cells ?? [])
34
+ byCell.set(`${cell.feature_id}|${cell.platform_id}`, cell);
35
+ const rows = [];
36
+ for (const category of matrix.categories ?? []) {
37
+ for (const feature of category.features ?? []) {
38
+ rows.push([
39
+ `${category.name ?? ''}/${feature.name ?? ''}`,
40
+ ...platforms.map((platform) => this.renderCell(byCell.get(`${feature.id}|${platform.id}`))),
41
+ ]);
42
+ }
43
+ }
44
+ if (rows.length === 0) {
45
+ this.log('No features yet. Add one with: matrix features create <product> --category <name> --name <name>');
46
+ return;
47
+ }
48
+ this.log((0, output_1.renderTable)(['FEATURE', ...platforms.map((platform) => String(platform.code ?? '').toUpperCase())], rows));
49
+ const missing = Number(matrix.missing_release_count ?? 0);
50
+ if (missing > 0) {
51
+ this.log(`\n${missing} released cell(s) without a version (marked "?"). Set one with matrix cells set --release.`);
52
+ }
53
+ }
54
+ /** `-` = not set (different from an explicit "not applicable" status, which has its own code). */
55
+ renderCell(cell) {
56
+ if (!cell?.status?.code)
57
+ return '-';
58
+ if (cell.release?.version)
59
+ return `${cell.status.code} ${cell.release.version}`;
60
+ return cell.release_missing ? `${cell.status.code} ?` : String(cell.status.code);
61
+ }
62
+ }
63
+ exports.default = MatrixShow;
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class MatrixStatuses extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<unknown>;
6
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const base_1 = require("../../base");
4
+ const output_1 = require("../../lib/output");
5
+ class MatrixStatuses extends base_1.BaseCommand {
6
+ static description = 'List the cell statuses of your organization (the codes accepted by matrix cells set)';
7
+ static examples = ['<%= config.bin %> matrix statuses', '<%= config.bin %> matrix statuses --json'];
8
+ async run() {
9
+ await this.parse(MatrixStatuses);
10
+ const res = await this.api.get('/cli/v1/types/feature_statuses');
11
+ if (!this.jsonEnabled()) {
12
+ this.log((0, output_1.renderTable)(['CODE', 'LABEL', 'LIFECYCLE', 'ID'], (res.data ?? []).map((status) => [
13
+ String(status.code ?? ''),
14
+ String(status.label ?? ''),
15
+ String(status.category ?? ''),
16
+ String(status.id ?? ''),
17
+ ])));
18
+ }
19
+ return res;
20
+ }
21
+ }
22
+ exports.default = MatrixStatuses;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseClaim extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ ttl: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseClaim extends base_1.BaseCommand {
8
+ static description = 'Claim the working lease on a ticket, so nobody else (person or agent) starts it at the same time';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease claim <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease claim DRFL-3 -p driverone-flutter --ttl 3600 --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ ttl: core_1.Flags.integer({ description: 'Lease duration in seconds (server default: 8h; range 1s..30 days)' }),
19
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(TicketsLeaseClaim);
23
+ const projectId = await this.resolveProjectId(flags.project);
24
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
25
+ const body = {};
26
+ if (flags.ttl !== undefined)
27
+ body.ttl_seconds = flags.ttl;
28
+ if (flags['run-id'] !== undefined)
29
+ body.run_id = flags['run-id'];
30
+ let res;
31
+ try {
32
+ res = await this.api.post(path, body);
33
+ }
34
+ catch (error) {
35
+ // 409 = somebody else already holds it. --json prints the full backend envelope (details
36
+ // included) itself and returns, so BaseCommand.catch never trims it down to {code,message};
37
+ // human mode gets the holder spelled out in the message instead (requisito CYCL-14).
38
+ if (this.jsonEnabled()) {
39
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
40
+ if (!envelope)
41
+ throw error;
42
+ this.logJson(envelope);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ throw (0, lease_1.describeLeaseConflict)(error);
47
+ }
48
+ if (!this.jsonEnabled()) {
49
+ const lease = res.data ?? {};
50
+ this.log(`Ticket ${args.id} leased until ${lease.expires_at ?? '?'}:`);
51
+ this.log((0, output_1.renderRecord)(leaseSummary(lease)));
52
+ }
53
+ return res;
54
+ }
55
+ }
56
+ exports.default = TicketsLeaseClaim;
57
+ /** Flatten the lease payload for `renderRecord`: `held_by` is a nested object, so print it as one line. */
58
+ function leaseSummary(lease) {
59
+ const heldBy = lease.held_by;
60
+ return {
61
+ ticket: lease.ticket,
62
+ run_id: lease.run_id,
63
+ held_by: heldBy ? `${heldBy.kind} ${heldBy.name ?? heldBy.id}` : null,
64
+ expires_at: lease.expires_at,
65
+ host_id: lease.host_id,
66
+ account_id: lease.account_id,
67
+ agent: lease.agent,
68
+ execution_phase: lease.execution_phase,
69
+ profile_digest: lease.profile_digest,
70
+ };
71
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseRelease extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseRelease extends base_1.BaseCommand {
8
+ static description = 'Release your own working lease on a ticket, so someone else can claim it';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease release <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease release DRFL-3 -p driverone-flutter --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(TicketsLeaseRelease);
22
+ const projectId = await this.resolveProjectId(flags.project);
23
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
24
+ const body = {};
25
+ if (flags['run-id'] !== undefined)
26
+ body.run_id = flags['run-id'];
27
+ let res;
28
+ try {
29
+ res = await this.api.delete(path, { body });
30
+ }
31
+ catch (error) {
32
+ // 409 = somebody else already holds it, 404 = no lease to release. --json prints the full
33
+ // backend envelope (details included) itself and returns, so BaseCommand.catch never trims
34
+ // it down to {code,message}; human mode gets the holder spelled out instead (CYCL-14).
35
+ if (this.jsonEnabled()) {
36
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
37
+ if (!envelope)
38
+ throw error;
39
+ this.logJson(envelope);
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ throw (0, lease_1.describeLeaseConflict)(error);
44
+ }
45
+ if (!this.jsonEnabled()) {
46
+ this.log(`Ticket ${args.id} lease released:`);
47
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
48
+ }
49
+ return res;
50
+ }
51
+ }
52
+ exports.default = TicketsLeaseRelease;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base';
2
+ export default class TicketsLeaseRenew extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ ttl: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'run-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../../base");
5
+ const lease_1 = require("../../../lib/lease");
6
+ const output_1 = require("../../../lib/output");
7
+ class TicketsLeaseRenew extends base_1.BaseCommand {
8
+ static description = 'Extend your own working lease on a ticket before it expires';
9
+ static examples = [
10
+ '<%= config.bin %> tickets lease renew <ticket-id> --project acme-api',
11
+ '<%= config.bin %> tickets lease renew DRFL-3 -p driverone-flutter --ttl 3600 --run-id run-42',
12
+ ];
13
+ static args = {
14
+ id: core_1.Args.string({ description: 'Ticket id or code (e.g. DRFL-3)', required: true }),
15
+ };
16
+ static flags = {
17
+ ...base_1.projectFlag,
18
+ ttl: core_1.Flags.integer({ description: 'New lease duration in seconds (server default: 8h; range 1s..30 days)' }),
19
+ 'run-id': core_1.Flags.string({ description: 'Automation run id, for tokens that juggle several concurrent runs' }),
20
+ };
21
+ async run() {
22
+ const { args, flags } = await this.parse(TicketsLeaseRenew);
23
+ const projectId = await this.resolveProjectId(flags.project);
24
+ const path = `/cli/v1/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(args.id)}/lease`;
25
+ const body = {};
26
+ if (flags.ttl !== undefined)
27
+ body.ttl_seconds = flags.ttl;
28
+ if (flags['run-id'] !== undefined)
29
+ body.run_id = flags['run-id'];
30
+ let res;
31
+ try {
32
+ res = await this.api.put(path, body);
33
+ }
34
+ catch (error) {
35
+ // 409 = somebody else already holds it, 404 = no lease to renew. --json prints the full
36
+ // backend envelope (details included) itself and returns, so BaseCommand.catch never trims
37
+ // it down to {code,message}; human mode gets the holder spelled out instead (CYCL-14).
38
+ if (this.jsonEnabled()) {
39
+ const envelope = (0, lease_1.leaseErrorEnvelope)(error);
40
+ if (!envelope)
41
+ throw error;
42
+ this.logJson(envelope);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ throw (0, lease_1.describeLeaseConflict)(error);
47
+ }
48
+ if (!this.jsonEnabled()) {
49
+ const lease = res.data ?? {};
50
+ this.log(`Ticket ${args.id} lease renewed until ${lease.expires_at ?? '?'}:`);
51
+ this.log((0, output_1.renderRecord)(leaseSummary(lease)));
52
+ }
53
+ return res;
54
+ }
55
+ }
56
+ exports.default = TicketsLeaseRenew;
57
+ /** Flatten the lease payload for `renderRecord`: `held_by` is a nested object, so print it as one line. */
58
+ function leaseSummary(lease) {
59
+ const heldBy = lease.held_by;
60
+ return {
61
+ ticket: lease.ticket,
62
+ run_id: lease.run_id,
63
+ held_by: heldBy ? `${heldBy.kind} ${heldBy.name ?? heldBy.id}` : null,
64
+ expires_at: lease.expires_at,
65
+ host_id: lease.host_id,
66
+ account_id: lease.account_id,
67
+ agent: lease.agent,
68
+ execution_phase: lease.execution_phase,
69
+ profile_digest: lease.profile_digest,
70
+ };
71
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * On a 409 lease conflict, rewrite the error message to name who holds the ticket and until when.
3
+ * An automation reading "R409-LEASE-001: Ticket is already leased" alone cannot decide whether to
4
+ * skip to the next ticket — it needs the holder. Every other status/code (and a 409 without the
5
+ * expected `details.holder` shape) passes through untouched, so unrelated errors keep the server's
6
+ * own wording. Human-mode only: `--json` prints its own full envelope instead (see
7
+ * `leaseErrorEnvelope`), so it never goes through this rewrite.
8
+ */
9
+ export declare function describeLeaseConflict(error: unknown): unknown;
10
+ /** Allow-listed `details` for the --json envelope: only the fields a lease conflict is documented to carry. */
11
+ interface LeaseErrorDetails {
12
+ holder: {
13
+ held_by: {
14
+ kind: string;
15
+ id: string;
16
+ name?: string;
17
+ };
18
+ expires_at?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Full `{error:{code,message,details}}` envelope for a lease command failure in `--json` mode.
23
+ * `BaseCommand.catch` — shared by every command in the CLI — only ever emits `{code,message}`; a
24
+ * script reading `--json` on a 409 needs `details.holder` too (who holds the ticket, until when),
25
+ * so lease commands print this themselves and return before the error reaches `catch()`, instead of
26
+ * changing that shared, cross-cutting behaviour (out of scope for CYCL-14).
27
+ *
28
+ * `details` is rebuilt field-by-field from an explicit allow-list (`holder.held_by.{kind,id,name}`,
29
+ * `holder.expires_at`) instead of forwarding `error.details` verbatim — same reasoning as
30
+ * `BaseCommand.catch`'s own allow-listed envelope (see `base.ts`): never serialize a server-shaped
31
+ * object of arbitrary provenance straight into `--json` output. Any other field the backend happens
32
+ * to send inside `details` (e.g. a stray `holder.ticket`) is dropped, not forwarded.
33
+ *
34
+ * Returns undefined for anything that isn't an `ApiRequestError`: callers should let those propagate
35
+ * to `catch()` as usual.
36
+ */
37
+ export declare function leaseErrorEnvelope(error: unknown): {
38
+ error: {
39
+ code: string;
40
+ message: string;
41
+ details?: LeaseErrorDetails;
42
+ };
43
+ } | undefined;
44
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeLeaseConflict = describeLeaseConflict;
4
+ exports.leaseErrorEnvelope = leaseErrorEnvelope;
5
+ const api_1 = require("./api");
6
+ /** Extract `{kind,id,name?,expiresAt?}` from a 409's `details`, or undefined if the shape doesn't match. */
7
+ function parseLeaseHolder(details) {
8
+ const holder = details?.holder;
9
+ const heldBy = holder?.held_by;
10
+ if (!heldBy?.kind || !heldBy.id)
11
+ return undefined;
12
+ return { kind: heldBy.kind, id: heldBy.id, name: heldBy.name, expiresAt: holder?.expires_at };
13
+ }
14
+ /**
15
+ * On a 409 lease conflict, rewrite the error message to name who holds the ticket and until when.
16
+ * An automation reading "R409-LEASE-001: Ticket is already leased" alone cannot decide whether to
17
+ * skip to the next ticket — it needs the holder. Every other status/code (and a 409 without the
18
+ * expected `details.holder` shape) passes through untouched, so unrelated errors keep the server's
19
+ * own wording. Human-mode only: `--json` prints its own full envelope instead (see
20
+ * `leaseErrorEnvelope`), so it never goes through this rewrite.
21
+ */
22
+ function describeLeaseConflict(error) {
23
+ if (!(error instanceof api_1.ApiRequestError) || error.status !== 409)
24
+ return error;
25
+ const holder = parseLeaseHolder(error.details);
26
+ if (!holder)
27
+ return error;
28
+ const who = holder.name ? `${holder.kind} "${holder.name}"` : `${holder.kind} ${holder.id}`;
29
+ const until = holder.expiresAt ? ` until ${holder.expiresAt}` : '';
30
+ return new api_1.ApiRequestError(error.status, error.code, `Ticket already leased by ${who}${until}.`, error.details);
31
+ }
32
+ /**
33
+ * Full `{error:{code,message,details}}` envelope for a lease command failure in `--json` mode.
34
+ * `BaseCommand.catch` — shared by every command in the CLI — only ever emits `{code,message}`; a
35
+ * script reading `--json` on a 409 needs `details.holder` too (who holds the ticket, until when),
36
+ * so lease commands print this themselves and return before the error reaches `catch()`, instead of
37
+ * changing that shared, cross-cutting behaviour (out of scope for CYCL-14).
38
+ *
39
+ * `details` is rebuilt field-by-field from an explicit allow-list (`holder.held_by.{kind,id,name}`,
40
+ * `holder.expires_at`) instead of forwarding `error.details` verbatim — same reasoning as
41
+ * `BaseCommand.catch`'s own allow-listed envelope (see `base.ts`): never serialize a server-shaped
42
+ * object of arbitrary provenance straight into `--json` output. Any other field the backend happens
43
+ * to send inside `details` (e.g. a stray `holder.ticket`) is dropped, not forwarded.
44
+ *
45
+ * Returns undefined for anything that isn't an `ApiRequestError`: callers should let those propagate
46
+ * to `catch()` as usual.
47
+ */
48
+ function leaseErrorEnvelope(error) {
49
+ if (!(error instanceof api_1.ApiRequestError))
50
+ return undefined;
51
+ const holder = parseLeaseHolder(error.details);
52
+ const details = holder
53
+ ? {
54
+ holder: {
55
+ held_by: holder.name ? { kind: holder.kind, id: holder.id, name: holder.name } : { kind: holder.kind, id: holder.id },
56
+ ...(holder.expiresAt ? { expires_at: holder.expiresAt } : {}),
57
+ },
58
+ }
59
+ : undefined;
60
+ return { error: { code: error.code, message: error.message, details } };
61
+ }