@sanity/workflow-cli 0.8.1 → 0.10.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 (79) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +145 -63
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +8 -12
  5. package/dist/commands/abort.js +24 -34
  6. package/dist/commands/definition/delete.d.ts +6 -6
  7. package/dist/commands/definition/delete.js +17 -33
  8. package/dist/commands/definition/diff.d.ts +10 -3
  9. package/dist/commands/definition/diff.js +21 -29
  10. package/dist/commands/definition/list.d.ts +5 -6
  11. package/dist/commands/definition/list.js +44 -50
  12. package/dist/commands/definition/show.d.ts +22 -13
  13. package/dist/commands/definition/show.js +25 -36
  14. package/dist/commands/deploy.d.ts +67 -8
  15. package/dist/commands/deploy.js +136 -32
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +45 -61
  18. package/dist/commands/fire-action.d.ts +7 -30
  19. package/dist/commands/fire-action.js +42 -115
  20. package/dist/commands/list.d.ts +9 -10
  21. package/dist/commands/list.js +49 -62
  22. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  23. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
  24. package/dist/commands/set-stage.d.ts +27 -3
  25. package/dist/commands/set-stage.js +63 -12
  26. package/dist/commands/show.d.ts +3 -2
  27. package/dist/commands/show.js +15 -21
  28. package/dist/commands/start.d.ts +56 -0
  29. package/dist/commands/start.js +133 -0
  30. package/dist/commands/tail.d.ts +5 -2
  31. package/dist/commands/tail.js +25 -26
  32. package/dist/hooks/finally/telemetry.d.ts +8 -0
  33. package/dist/hooks/finally/telemetry.js +13 -0
  34. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  35. package/dist/hooks/prerun/telemetry.js +5 -0
  36. package/dist/index.js +0 -3
  37. package/dist/lib/base-command.d.ts +14 -0
  38. package/dist/lib/base-command.js +10 -0
  39. package/dist/lib/client.d.ts +11 -14
  40. package/dist/lib/client.js +29 -34
  41. package/dist/lib/context.d.ts +98 -0
  42. package/dist/lib/context.js +83 -0
  43. package/dist/lib/definitions.d.ts +38 -29
  44. package/dist/lib/definitions.js +34 -93
  45. package/dist/lib/diff.d.ts +5 -2
  46. package/dist/lib/diff.js +62 -20
  47. package/dist/lib/env.d.ts +4 -6
  48. package/dist/lib/env.js +4 -9
  49. package/dist/lib/fail.d.ts +12 -0
  50. package/dist/lib/fail.js +25 -16
  51. package/dist/lib/flags.d.ts +7 -2
  52. package/dist/lib/flags.js +7 -3
  53. package/dist/lib/load-config.d.ts +15 -0
  54. package/dist/lib/load-config.js +89 -0
  55. package/dist/lib/operation-args.d.ts +20 -15
  56. package/dist/lib/operation-args.js +9 -40
  57. package/dist/lib/ops-report.d.ts +29 -13
  58. package/dist/lib/ops-report.js +20 -17
  59. package/dist/lib/params.d.ts +7 -0
  60. package/dist/lib/params.js +18 -0
  61. package/dist/lib/read-fanout.d.ts +22 -0
  62. package/dist/lib/read-fanout.js +28 -0
  63. package/dist/lib/select-deployment.d.ts +31 -0
  64. package/dist/lib/select-deployment.js +37 -0
  65. package/dist/lib/share-definitions.d.ts +86 -0
  66. package/dist/lib/share-definitions.js +106 -0
  67. package/dist/lib/stub.js +0 -7
  68. package/dist/lib/telemetry-setup.d.ts +66 -0
  69. package/dist/lib/telemetry-setup.js +92 -0
  70. package/dist/lib/telemetry.d.ts +100 -0
  71. package/dist/lib/telemetry.js +89 -0
  72. package/dist/lib/ui.d.ts +33 -1
  73. package/dist/lib/ui.js +32 -21
  74. package/oclif.manifest.json +133 -113
  75. package/package.json +16 -8
  76. package/dist/commands/move-stage.d.ts +0 -52
  77. package/dist/commands/move-stage.js +0 -86
  78. package/dist/lib/config.d.ts +0 -18
  79. package/dist/lib/config.js +0 -50
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The CLI's telemetry shell. A standalone oclif binary is its own app
3
+ * shell — no host to ride — so this module owns the store, the standard
4
+ * Sanity-intake consent/transport recipe (shared via
5
+ * `@sanity/workflow-engine`'s `createTelemetryIntake`), and the one-time
6
+ * disclosure. The `prerun` hook assembles it once per invocation and the
7
+ * `finally` hook completes the command trace and flushes.
8
+ *
9
+ * Consent recipe: CI / `DO_NOT_TRACK` → denied; not logged in → no store
10
+ * (undetermined, nothing sent); otherwise the account-level status the
11
+ * intake recipe fetches. Consent is managed account-wide by
12
+ * `sanity telemetry enable|disable|status` — the CLI inherits it and
13
+ * ships no consent UX of its own.
14
+ *
15
+ * Provided-implementation caveat: a `telemetry` logger on the workflow
16
+ * config replaces this shell wholesale — the built-in store is never
17
+ * constructed and none of its policy (consent, environment suppression,
18
+ * transport) applies; every event flows to the provided logger
19
+ * unconditionally. Gates live in the store, not at emission sites.
20
+ *
21
+ * Payload policy: no customer strings, ever. The command trace carries
22
+ * the command id, the NAMES of declared flags used, and a success flag —
23
+ * never flag values, arguments, or error text. `Editorial Workflows
24
+ * Definition Shared` is a content-free marker (hash + structural counts); the shared
25
+ * document itself rides the first-party feedback endpoint, never
26
+ * telemetry (see `share-definitions.ts`).
27
+ */
28
+ import { type DefinedTelemetryTrace, type TelemetryStore } from '@sanity/telemetry';
29
+ import { type TelemetryIntakeClient, type WorkflowDefinitionDeployedData, type WorkflowTelemetryLogger } from '@sanity/workflow-engine';
30
+ export interface WorkflowCliCommandData {
31
+ /** The oclif command id — ours, never user free-text. */
32
+ command: string;
33
+ /** Names of DECLARED flags present on the invocation — never values,
34
+ * never undeclared tokens. */
35
+ flags: string[];
36
+ success: boolean;
37
+ }
38
+ export declare const WorkflowCliCommandExecuted: DefinedTelemetryTrace<WorkflowCliCommandData, void>;
39
+ /** {@link WorkflowDefinitionDeployedData} minus the deploy status — the marker
40
+ * is projected from the engine's own structural derivation so the two events
41
+ * can never disagree about one definition, and only newly created versions
42
+ * are ever shared, so status carries nothing. */
43
+ export type WorkflowDefinitionSharedData = Omit<WorkflowDefinitionDeployedData, 'status'>;
44
+ export declare const WorkflowDefinitionShared: import("@sanity/telemetry").DefinedTelemetryLog<WorkflowDefinitionSharedData>;
45
+ /**
46
+ * The shell for one CLI invocation. `builtin` is the Sanity-intake store;
47
+ * `provided` is a config-supplied logger that replaces the shell and its
48
+ * policy wholesale; `off` (the default) emits nothing — no config-derived
49
+ * project, no auth token, or telemetry setup failed.
50
+ */
51
+ export type CliTelemetry = {
52
+ kind: 'builtin';
53
+ logger: WorkflowTelemetryLogger;
54
+ store: TelemetryStore<unknown>;
55
+ envDenied: boolean;
56
+ } | {
57
+ kind: 'provided';
58
+ logger: WorkflowTelemetryLogger;
59
+ } | {
60
+ kind: 'off';
61
+ logger: WorkflowTelemetryLogger;
62
+ };
63
+ export declare function cliTelemetry(): CliTelemetry;
64
+ /** Install the invocation's shell and, for the built-in store, start the
65
+ * command trace (completed by {@link finishCliTelemetry}). */
66
+ export declare function setCliTelemetry(telemetry: CliTelemetry): void;
67
+ export declare function clearCliTelemetry(): void;
68
+ /** Build the built-in Sanity-intake shell over an authenticated,
69
+ * project-bound client. */
70
+ export declare function createBuiltinTelemetry(args: {
71
+ client: TelemetryIntakeClient;
72
+ projectId: string;
73
+ env: Record<string, string | undefined>;
74
+ }): Extract<CliTelemetry, {
75
+ kind: 'builtin';
76
+ }>;
77
+ /**
78
+ * Race `work` against a deadline, resolving `undefined` when the deadline
79
+ * wins. The timer is always cleared once the race settles — oclif never
80
+ * force-exits on success, so a pending timer would hold a finished process
81
+ * open. The losing promise's eventual settlement is absorbed by the race.
82
+ */
83
+ export declare function raceWithDeadline<T>(work: Promise<T>, deadlineMs: number): Promise<T | undefined>;
84
+ /**
85
+ * Complete the command trace and flush the final batch — the `finally`
86
+ * hook's whole job. On the built-in store the trace completes with the
87
+ * payload and the store flushes (deadline-capped); a provided logger gets
88
+ * the same payload through its `log` seam and owns its own delivery.
89
+ */
90
+ export declare function finishCliTelemetry(data: WorkflowCliCommandData): Promise<void>;
91
+ /**
92
+ * The names of declared flags present in `argv` — long form (`--name`,
93
+ * `--name=value`), negated booleans (`--no-name`), and short chars
94
+ * (`-c`). Matching against the command's declared flags is what keeps
95
+ * values and free-text out of the payload; tokens after the `--`
96
+ * terminator are never inspected.
97
+ */
98
+ export declare function usedFlagNames(argv: string[], flagDefs: Record<string, {
99
+ char?: string;
100
+ }> | undefined): string[];
@@ -0,0 +1,89 @@
1
+ import { createBatchedStore, createSessionId, defineEvent, defineTrace, } from '@sanity/telemetry';
2
+ import { createTelemetryIntake, isTelemetryEnvDenied, noopTelemetry, } from '@sanity/workflow-engine';
3
+ export const WorkflowCliCommandExecuted = defineTrace({
4
+ name: 'Editorial Workflows CLI Command Executed',
5
+ version: 1,
6
+ description: 'A workflow CLI command was executed — payload is the command id, the names of declared flags used (never their values), and a success flag',
7
+ });
8
+ export const WorkflowDefinitionShared = defineEvent({
9
+ name: 'Editorial Workflows Definition Shared',
10
+ version: 1,
11
+ description: 'A newly created workflow definition version was donated through the explicit definition-sharing opt-in. Content-free marker: the content hash plus the same structural counts Editorial Workflows Definition Deployed carries — the document itself goes to the first-party definition-feedback endpoint, never through telemetry.',
12
+ });
13
+ const OFF = { kind: 'off', logger: noopTelemetry };
14
+ let current = OFF;
15
+ let activeTrace;
16
+ export function cliTelemetry() {
17
+ return current;
18
+ }
19
+ export function setCliTelemetry(telemetry) {
20
+ current = telemetry;
21
+ if (telemetry.kind === 'builtin') {
22
+ activeTrace = telemetry.store.logger.trace(WorkflowCliCommandExecuted);
23
+ activeTrace.start();
24
+ }
25
+ }
26
+ export function clearCliTelemetry() {
27
+ current = OFF;
28
+ activeTrace = undefined;
29
+ }
30
+ export function createBuiltinTelemetry(args) {
31
+ const { client, projectId, env } = args;
32
+ const envDenied = isTelemetryEnvDenied(env);
33
+ const store = createBatchedStore(createSessionId(), createTelemetryIntake({ client, projectId, denied: envDenied }));
34
+ return { kind: 'builtin', logger: store.logger, store, envDenied };
35
+ }
36
+ function traceAsLogEvent(trace) {
37
+ const { name, version, description } = trace;
38
+ return { type: 'log', name, version, description };
39
+ }
40
+ const FLUSH_DEADLINE_MS = 3_000;
41
+ export async function raceWithDeadline(work, deadlineMs) {
42
+ let deadline;
43
+ try {
44
+ return await Promise.race([
45
+ work,
46
+ new Promise((resolve) => {
47
+ deadline = setTimeout(() => resolve(undefined), deadlineMs);
48
+ }),
49
+ ]);
50
+ }
51
+ finally {
52
+ clearTimeout(deadline);
53
+ }
54
+ }
55
+ export async function finishCliTelemetry(data) {
56
+ if (current.kind === 'provided') {
57
+ current.logger.log(traceAsLogEvent(WorkflowCliCommandExecuted), data);
58
+ return;
59
+ }
60
+ if (current.kind !== 'builtin')
61
+ return;
62
+ if (activeTrace !== undefined) {
63
+ activeTrace.log(data);
64
+ activeTrace.complete();
65
+ activeTrace = undefined;
66
+ }
67
+ const { store } = current;
68
+ try {
69
+ await raceWithDeadline(store.flush(), FLUSH_DEADLINE_MS);
70
+ }
71
+ finally {
72
+ store.end();
73
+ }
74
+ }
75
+ export function usedFlagNames(argv, flagDefs) {
76
+ const terminator = argv.indexOf('--');
77
+ const tokens = argv.slice(0, terminator === -1 ? argv.length : terminator);
78
+ return Object.entries(flagDefs ?? {})
79
+ .filter(([name, def]) => tokens.some((token) => matchesFlag({ token, name, char: def.char })))
80
+ .map(([name]) => name)
81
+ .sort();
82
+ }
83
+ function matchesFlag(args) {
84
+ const { token, name, char } = args;
85
+ if (token === `--${name}` || token.startsWith(`--${name}=`) || token === `--no-${name}`) {
86
+ return true;
87
+ }
88
+ return char !== undefined && (token === `-${char}` || token.startsWith(`-${char}=`));
89
+ }
package/dist/lib/ui.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ActivityStatus } from '@sanity/workflow-engine';
1
+ import type { ActivityStatus, WorkflowResource } from '@sanity/workflow-engine';
2
2
  /**
3
3
  * A bold `Title:` section heading. Mirrors the Sanity CLI's `sectionHeader`
4
4
  * (name + shape) so it folds into the shared helper when this CLI merges in.
@@ -15,6 +15,38 @@ export declare function formatKeyValue({ key, value, indent, padTo, }: {
15
15
  indent?: number;
16
16
  padTo?: number;
17
17
  }): string;
18
+ /**
19
+ * A borderless column table as printable lines: cyan header row, every
20
+ * column padded to its widest cell, columns joined by three spaces.
21
+ * Mirrors the Sanity CLI's list-table shape (see `projects list`) so it
22
+ * folds into the shared helper when this CLI merges in. Widths count
23
+ * visible characters, so a pre-styled (ANSI) cell still pads correctly.
24
+ */
25
+ export declare function formatTable(headers: string[], rows: string[][]): string[];
26
+ /**
27
+ * Clip a probe-fetched result back to `limit`. The list query builders
28
+ * fetch one row past the display limit; when that probe row comes back,
29
+ * `note` carries the dim "showing the first N" hint to print under the table.
30
+ */
31
+ export declare function clipToLimit<T>(rows: T[], limit: number): {
32
+ rows: T[];
33
+ note?: string;
34
+ };
35
+ /**
36
+ * The `▸ label` banner printed above a group's output when a run spans
37
+ * several groups (deployments for deploy, resources for fanned-out reads),
38
+ * so each block is attributable. `undefined` for a lone group — it keeps its
39
+ * original unprefixed output. A leading blank line separates every group
40
+ * after the first.
41
+ */
42
+ export declare function groupBanner({ label, index, total, }: {
43
+ label: string;
44
+ index: number;
45
+ total: number;
46
+ }): string | undefined;
47
+ /** A workflow resource as a display label: a dataset's `project.dataset` id
48
+ * reads naturally bare; other resource types keep their type for clarity. */
49
+ export declare function resourceLabel(resource: WorkflowResource): string;
18
50
  /** Status glyph per activity state. log-symbols ship pre-colored (green ✔, red ✖),
19
51
  * so those need no extra wrap; the rest carry their own state color. */
20
52
  export declare const activityIcon: Record<ActivityStatus, string>;
package/dist/lib/ui.js CHANGED
@@ -1,26 +1,46 @@
1
- import { styleText } from 'node:util';
1
+ import { stripVTControlCharacters, styleText } from 'node:util';
2
2
  import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
3
3
  import { lightFormat } from 'date-fns/lightFormat';
4
4
  import { parseISO } from 'date-fns/parseISO';
5
5
  import logSymbols from 'log-symbols';
6
- /**
7
- * A bold `Title:` section heading. Mirrors the Sanity CLI's `sectionHeader`
8
- * (name + shape) so it folds into the shared helper when this CLI merges in.
9
- */
10
6
  export function sectionHeader(title) {
11
7
  return styleText('bold', `${title}:`);
12
8
  }
13
- /**
14
- * A ` key: value` detail row — a dim, padded key then the value. Mirrors
15
- * the Sanity CLI's `formatKeyValue` so it folds into the shared helper on
16
- * merge; pass `padTo` (the longest key's length) to align a block of rows.
17
- */
18
9
  export function formatKeyValue({ key, value, indent = 2, padTo = 0, }) {
19
10
  const paddedKey = `${key}:`.padEnd(padTo > 0 ? padTo + 1 : key.length + 1);
20
11
  return `${' '.repeat(indent)}${styleText('dim', paddedKey)} ${value}`;
21
12
  }
22
- /** Status glyph per activity state. log-symbols ship pre-colored (green ✔, red ✖),
23
- * so those need no extra wrap; the rest carry their own state color. */
13
+ export function formatTable(headers, rows) {
14
+ const width = (cell) => stripVTControlCharacters(cell).length;
15
+ const widths = headers.map((header, i) => Math.max(width(header), ...rows.map((row) => width(row[i] ?? ''))));
16
+ const formatRow = (row) => widths
17
+ .map((columnWidth, i) => {
18
+ const cell = row[i] ?? '';
19
+ return cell + ' '.repeat(columnWidth - width(cell));
20
+ })
21
+ .join(' ')
22
+ .trimEnd();
23
+ return [styleText('cyan', formatRow(headers)), ...rows.map(formatRow)];
24
+ }
25
+ export function clipToLimit(rows, limit) {
26
+ if (rows.length <= limit) {
27
+ return { rows };
28
+ }
29
+ return {
30
+ rows: rows.slice(0, limit),
31
+ note: styleText('dim', `showing the first ${limit} — raise --limit to see more`),
32
+ };
33
+ }
34
+ export function groupBanner({ label, index, total, }) {
35
+ if (total <= 1) {
36
+ return undefined;
37
+ }
38
+ const gap = index > 0 ? '\n' : '';
39
+ return `${gap}${styleText('bold', `▸ ${label}`)}`;
40
+ }
41
+ export function resourceLabel(resource) {
42
+ return resource.type === 'dataset' ? resource.id : `${resource.type}:${resource.id}`;
43
+ }
24
44
  export const activityIcon = {
25
45
  done: logSymbols.success,
26
46
  active: styleText('cyan', '●'),
@@ -28,25 +48,16 @@ export const activityIcon = {
28
48
  failed: logSymbols.error,
29
49
  skipped: styleText('dim', '⊘'),
30
50
  };
31
- /** Parse an engine ISO-8601 timestamp, or `undefined` when it's missing or
32
- * unparseable. The engine writes valid ISO, but the lake is the only
33
- * enforcement boundary — a legacy or raw-client-written doc can carry a bad
34
- * or absent timestamp, and `parseISO`/`lightFormat` throw on those. Callers
35
- * fall back to the raw value so one bad field never crashes a whole command. */
36
51
  function parseTimestamp(iso) {
37
52
  if (!iso)
38
53
  return undefined;
39
54
  const date = parseISO(iso);
40
55
  return Number.isNaN(date.getTime()) ? undefined : date;
41
56
  }
42
- /** An engine ISO-8601 timestamp as an absolute `yyyy-MM-dd HH:mm:ss` — the
43
- * Sanity CLI's audit-log format (see `backups/list`). For detail views. */
44
57
  export function formatTimestamp(iso) {
45
58
  const date = parseTimestamp(iso);
46
59
  return date ? lightFormat(date, 'yyyy-MM-dd HH:mm:ss') : iso;
47
60
  }
48
- /** An engine ISO-8601 timestamp as a relative `… ago` — the Sanity CLI's
49
- * job-list format (see `datasets/copy`). For scannable overview tables. */
50
61
  export function formatAge(iso) {
51
62
  const date = parseTimestamp(iso);
52
63
  return date ? `${formatDistanceToNow(date)} ago` : iso;