@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.
- package/CHANGELOG.md +485 -0
- package/README.md +145 -63
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +8 -12
- package/dist/commands/abort.js +24 -34
- package/dist/commands/definition/delete.d.ts +6 -6
- package/dist/commands/definition/delete.js +17 -33
- package/dist/commands/definition/diff.d.ts +10 -3
- package/dist/commands/definition/diff.js +21 -29
- package/dist/commands/definition/list.d.ts +5 -6
- package/dist/commands/definition/list.js +44 -50
- package/dist/commands/definition/show.d.ts +22 -13
- package/dist/commands/definition/show.js +25 -36
- package/dist/commands/deploy.d.ts +67 -8
- package/dist/commands/deploy.js +136 -32
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +45 -61
- package/dist/commands/fire-action.d.ts +7 -30
- package/dist/commands/fire-action.js +42 -115
- package/dist/commands/list.d.ts +9 -10
- package/dist/commands/list.js +49 -62
- package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
- package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
- package/dist/commands/set-stage.d.ts +27 -3
- package/dist/commands/set-stage.js +63 -12
- package/dist/commands/show.d.ts +3 -2
- package/dist/commands/show.js +15 -21
- package/dist/commands/start.d.ts +56 -0
- package/dist/commands/start.js +133 -0
- package/dist/commands/tail.d.ts +5 -2
- package/dist/commands/tail.js +25 -26
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +5 -0
- package/dist/hooks/prerun/telemetry.js +5 -0
- package/dist/index.js +0 -3
- package/dist/lib/base-command.d.ts +14 -0
- package/dist/lib/base-command.js +10 -0
- package/dist/lib/client.d.ts +11 -14
- package/dist/lib/client.js +29 -34
- package/dist/lib/context.d.ts +98 -0
- package/dist/lib/context.js +83 -0
- package/dist/lib/definitions.d.ts +38 -29
- package/dist/lib/definitions.js +34 -93
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +62 -20
- package/dist/lib/env.d.ts +4 -6
- package/dist/lib/env.js +4 -9
- package/dist/lib/fail.d.ts +12 -0
- package/dist/lib/fail.js +25 -16
- package/dist/lib/flags.d.ts +7 -2
- package/dist/lib/flags.js +7 -3
- package/dist/lib/load-config.d.ts +15 -0
- package/dist/lib/load-config.js +89 -0
- package/dist/lib/operation-args.d.ts +20 -15
- package/dist/lib/operation-args.js +9 -40
- package/dist/lib/ops-report.d.ts +29 -13
- package/dist/lib/ops-report.js +20 -17
- package/dist/lib/params.d.ts +7 -0
- package/dist/lib/params.js +18 -0
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.d.ts +31 -0
- package/dist/lib/select-deployment.js +37 -0
- package/dist/lib/share-definitions.d.ts +86 -0
- package/dist/lib/share-definitions.js +106 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +66 -0
- package/dist/lib/telemetry-setup.js +92 -0
- package/dist/lib/telemetry.d.ts +100 -0
- package/dist/lib/telemetry.js +89 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -21
- package/oclif.manifest.json +133 -113
- package/package.json +16 -8
- package/dist/commands/move-stage.d.ts +0 -52
- package/dist/commands/move-stage.js +0 -86
- package/dist/lib/config.d.ts +0 -18
- 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
|
-
|
|
23
|
-
|
|
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;
|