@sanity/workflow-cli 0.11.0 → 0.13.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 +66 -0
- package/README.md +75 -46
- package/dist/commands/{abort.d.ts → editorial-workflows/abort.d.ts} +3 -2
- package/dist/commands/{abort.js → editorial-workflows/abort.js} +7 -6
- package/dist/commands/{definition → editorial-workflows/definition}/delete.d.ts +3 -2
- package/dist/commands/{definition → editorial-workflows/definition}/delete.js +6 -5
- package/dist/commands/{definition → editorial-workflows/definition}/diff.d.ts +2 -1
- package/dist/commands/{definition → editorial-workflows/definition}/diff.js +7 -6
- package/dist/commands/{definition → editorial-workflows/definition}/list.d.ts +5 -1
- package/dist/commands/{definition → editorial-workflows/definition}/list.js +27 -24
- package/dist/commands/{definition → editorial-workflows/definition}/show.d.ts +3 -2
- package/dist/commands/{definition → editorial-workflows/definition}/show.js +9 -8
- package/dist/commands/{deploy.d.ts → editorial-workflows/deploy.d.ts} +2 -1
- package/dist/commands/{deploy.js → editorial-workflows/deploy.js} +14 -13
- package/dist/commands/{diagnose.d.ts → editorial-workflows/diagnose.d.ts} +2 -1
- package/dist/commands/{diagnose.js → editorial-workflows/diagnose.js} +7 -6
- package/dist/commands/{fire-action.d.ts → editorial-workflows/fire-action.d.ts} +3 -2
- package/dist/commands/{fire-action.js → editorial-workflows/fire-action.js} +8 -7
- package/dist/commands/{list.d.ts → editorial-workflows/list.d.ts} +4 -1
- package/dist/commands/{list.js → editorial-workflows/list.js} +33 -28
- package/dist/commands/editorial-workflows/nuke.d.ts +12 -0
- package/dist/commands/editorial-workflows/nuke.js +77 -0
- package/dist/commands/{reset-activity.d.ts → editorial-workflows/reset-activity.d.ts} +2 -1
- package/dist/commands/{reset-activity.js → editorial-workflows/reset-activity.js} +2 -1
- package/dist/commands/{set-stage.d.ts → editorial-workflows/set-stage.d.ts} +4 -3
- package/dist/commands/{set-stage.js → editorial-workflows/set-stage.js} +6 -5
- package/dist/commands/{show.d.ts → editorial-workflows/show.d.ts} +2 -1
- package/dist/commands/{show.js → editorial-workflows/show.js} +40 -26
- package/dist/commands/{start.d.ts → editorial-workflows/start.d.ts} +20 -4
- package/dist/commands/{start.js → editorial-workflows/start.js} +58 -16
- package/dist/commands/{tail.d.ts → editorial-workflows/tail.d.ts} +2 -1
- package/dist/commands/{tail.js → editorial-workflows/tail.js} +10 -8
- package/dist/hooks/finally/telemetry.js +2 -2
- package/dist/hooks/prerun/telemetry.d.ts +4 -3
- package/dist/lib/base-command.d.ts +4 -6
- package/dist/lib/base-command.js +6 -0
- package/dist/lib/client.d.ts +8 -0
- package/dist/lib/client.js +1 -1
- package/dist/lib/context.d.ts +6 -2
- package/dist/lib/context.js +9 -7
- package/dist/lib/definitions.js +2 -2
- package/dist/lib/flags.d.ts +4 -3
- package/dist/lib/nuke.d.ts +89 -0
- package/dist/lib/nuke.js +111 -0
- package/dist/lib/operation-args.d.ts +3 -1
- package/dist/lib/ops-report.d.ts +2 -1
- package/dist/lib/prompt.d.ts +11 -0
- package/dist/lib/prompt.js +4 -0
- package/dist/lib/select-deployment.d.ts +12 -7
- package/dist/lib/select-deployment.js +26 -1
- package/dist/lib/share-definitions.d.ts +23 -28
- package/dist/lib/share-definitions.js +30 -40
- package/dist/lib/telemetry-setup.d.ts +2 -2
- package/dist/lib/telemetry.d.ts +18 -10
- package/dist/lib/telemetry.js +5 -2
- package/dist/lib/ui.d.ts +12 -0
- package/dist/lib/ui.js +9 -0
- package/oclif.manifest.json +138 -46
- package/package.json +11 -9
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args } from '@oclif/core';
|
|
3
|
-
import { displayTitle, errorMessage } from '@sanity/workflow-engine';
|
|
3
|
+
import { assertReadableModel, displayTitle, errorMessage, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import { WorkflowCommand } from "
|
|
6
|
-
import { findInstance, resolveReadTargets } from "
|
|
7
|
-
import { fail, failureDetail } from "
|
|
8
|
-
import { tagFlags } from "
|
|
9
|
-
import { formatTimestamp } from "
|
|
5
|
+
import { WorkflowCommand } from "../../lib/base-command.js";
|
|
6
|
+
import { findInstance, resolveReadTargets } from "../../lib/context.js";
|
|
7
|
+
import { fail, failureDetail } from "../../lib/fail.js";
|
|
8
|
+
import { tagFlags } from "../../lib/flags.js";
|
|
9
|
+
import { formatTimestamp } from "../../lib/ui.js";
|
|
10
10
|
const TAIL_TAG = 'tail';
|
|
11
11
|
export default class Tail extends WorkflowCommand {
|
|
12
|
+
static aliases = ['tail'];
|
|
12
13
|
static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
|
|
13
14
|
static examples = ['<%= config.bin %> tail wf-instance.abc123'];
|
|
14
15
|
static args = {
|
|
@@ -64,9 +65,10 @@ export default class Tail extends WorkflowCommand {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
async printNewEntries({ client, instanceId, seen, }) {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
68
|
+
const raw = await client.getDocument(instanceId, { tag: TAIL_TAG });
|
|
69
|
+
if (!raw)
|
|
69
70
|
return seen;
|
|
71
|
+
const next = assertReadableModel(raw);
|
|
70
72
|
const newEntries = next.history.slice(seen);
|
|
71
73
|
for (const entry of newEntries) {
|
|
72
74
|
this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { finishCliTelemetry, usedFlagNames } from "../../lib/telemetry.js";
|
|
1
|
+
import { finishCliTelemetry, traceCommandId, usedFlagNames } from "../../lib/telemetry.js";
|
|
2
2
|
const hook = async function (options) {
|
|
3
3
|
try {
|
|
4
4
|
await finishCliTelemetry({
|
|
5
|
-
command: options
|
|
5
|
+
command: traceCommandId(options),
|
|
6
6
|
flags: usedFlagNames(options.argv, options.Command?.flags),
|
|
7
7
|
success: options.error === undefined,
|
|
8
8
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Hook } from '@oclif/core';
|
|
2
2
|
/** Builds and installs the invocation's telemetry shell before the command
|
|
3
3
|
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* before oclif parses
|
|
4
|
+
* A deploy that may share definitions—flagless by default or explicitly
|
|
5
|
+
* enabled—forces the store to send despite CI / `DO_NOT_TRACK`. This is
|
|
6
|
+
* resolved from raw argv because the store is built before oclif parses
|
|
7
|
+
* flags. */
|
|
7
8
|
declare const hook: Hook<'prerun'>;
|
|
8
9
|
export default hook;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
/**
|
|
3
|
-
* Base for
|
|
4
|
-
*
|
|
5
|
-
* {@link fail} path
|
|
6
|
-
*
|
|
7
|
-
* detail at their own `fail` sites; every other error propagates to oclif
|
|
8
|
-
* unchanged.
|
|
3
|
+
* Base for workflow commands: prompt interrupts exit with the conventional
|
|
4
|
+
* SIGINT code and no stack, while an auth rejection escaping `run` renders
|
|
5
|
+
* through the clean {@link fail} path with {@link failureDetail}'s recovery
|
|
6
|
+
* hint. Every other error propagates to oclif unchanged.
|
|
9
7
|
*/
|
|
10
8
|
export declare abstract class WorkflowCommand extends Command {
|
|
11
9
|
protected catch(err: Error & {
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
1
2
|
import { Command } from '@oclif/core';
|
|
3
|
+
import { exitCodes } from '@sanity/cli-core';
|
|
2
4
|
import { fail, failureDetail, isAuthRejection } from "./fail.js";
|
|
3
5
|
export class WorkflowCommand extends Command {
|
|
4
6
|
async catch(err) {
|
|
7
|
+
if (err.name === 'ExitPromptError' || err.message === 'SIGINT') {
|
|
8
|
+
this.logToStderr(`${styleText('yellow', '›')} Aborted by user`);
|
|
9
|
+
return this.exit(exitCodes.SIGINT);
|
|
10
|
+
}
|
|
5
11
|
if (isAuthRejection(err)) {
|
|
6
12
|
fail('Authentication failed:', failureDetail(err));
|
|
7
13
|
}
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type SanityClient } from '@sanity/client';
|
|
2
2
|
import { type WorkflowResource } from '@sanity/workflow-engine';
|
|
3
|
+
/**
|
|
4
|
+
* The API host to hit: an explicit `SANITY_API_HOST` wins; otherwise mirror
|
|
5
|
+
* the Sanity CLI — staging routes to the staging API, prod falls through to
|
|
6
|
+
* `@sanity/client`'s default (`undefined` here). Pairs with
|
|
7
|
+
* {@link resolveToken}, which reads the matching session token. Exported so a
|
|
8
|
+
* destructive command can display the environment its clients will hit.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveApiHost(): string | undefined;
|
|
3
11
|
/**
|
|
4
12
|
* Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
|
|
5
13
|
* scripted use); otherwise fall back to the `sanity login` session token.
|
package/dist/lib/client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ENV, envAuthToken } from "./env.js";
|
|
|
5
5
|
import { fail } from "./fail.js";
|
|
6
6
|
const API_VERSION = ENGINE_API_VERSION;
|
|
7
7
|
const STAGING_API_HOST = 'https://api.sanity.work';
|
|
8
|
-
function resolveApiHost() {
|
|
8
|
+
export function resolveApiHost() {
|
|
9
9
|
return process.env[ENV.SANITY_API_HOST] ?? (isStaging() ? STAGING_API_HOST : undefined);
|
|
10
10
|
}
|
|
11
11
|
export async function resolveToken() {
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SanityClient } from '@sanity/client';
|
|
2
|
-
import type
|
|
2
|
+
import { type WorkflowConfig, type WorkflowDeployment, type WorkflowInstance, type WorkflowResource } from '@sanity/workflow-engine';
|
|
3
3
|
import type { EngineScope } from './operation-args.ts';
|
|
4
4
|
export interface DeploymentContext {
|
|
5
5
|
deployment: WorkflowDeployment;
|
|
@@ -40,9 +40,13 @@ export interface ReadTarget {
|
|
|
40
40
|
export declare function resolveReadTargets(flags: {
|
|
41
41
|
tag?: string | undefined;
|
|
42
42
|
}): Promise<ReadTarget[]>;
|
|
43
|
+
/** Distinct resources, keyed by their resource-shaped GDR (`<type>:<id>`).
|
|
44
|
+
* The first occurrence wins its position, so a caller-ordered list (e.g. the
|
|
45
|
+
* engine resource first) keeps its lead entry. */
|
|
46
|
+
export declare function dedupeResources(resources: WorkflowResource[]): WorkflowResource[];
|
|
43
47
|
/** The distinct resources a read targets: the tagged deployment's sole
|
|
44
48
|
* resource, or — untagged — every distinct one the config's deployments
|
|
45
|
-
* mention (
|
|
49
|
+
* mention ({@link dedupeResources}). */
|
|
46
50
|
export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
|
|
47
51
|
/** The single resource a read should target, or fail asking which when the
|
|
48
52
|
* config spans more than one. For the paths that need one definite dataset
|
package/dist/lib/context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertReadableModel, resourceGdr, } from '@sanity/workflow-engine';
|
|
1
2
|
import { clientFor, resolveTokenOrFail } from "./client.js";
|
|
2
3
|
import { fail } from "./fail.js";
|
|
3
4
|
import { loadWorkflowConfig } from "./load-config.js";
|
|
@@ -14,15 +15,14 @@ export async function resolveReadTargets(flags) {
|
|
|
14
15
|
const token = await resolveTokenOrFail();
|
|
15
16
|
return resources.map((resource) => ({ resource, client: clientFor(resource, token) }));
|
|
16
17
|
}
|
|
18
|
+
export function dedupeResources(resources) {
|
|
19
|
+
return [...new Map(resources.map((resource) => [resourceGdr(resource), resource])).values()];
|
|
20
|
+
}
|
|
17
21
|
export function resolveReadResources(config, tag) {
|
|
18
22
|
if (tag !== undefined) {
|
|
19
23
|
return [selectDeployment(config, { tag }).workflowResource];
|
|
20
24
|
}
|
|
21
|
-
const
|
|
22
|
-
`${d.workflowResource.type}:${d.workflowResource.id}`,
|
|
23
|
-
d.workflowResource,
|
|
24
|
-
]));
|
|
25
|
-
const resources = [...byKey.values()];
|
|
25
|
+
const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
|
|
26
26
|
if (resources.length === 0) {
|
|
27
27
|
fail('No deployments configured.');
|
|
28
28
|
}
|
|
@@ -51,7 +51,7 @@ export async function loadInstanceOrFail(client, instanceId) {
|
|
|
51
51
|
if (!instance) {
|
|
52
52
|
fail(`Workflow instance ${instanceId} not found`);
|
|
53
53
|
}
|
|
54
|
-
return instance;
|
|
54
|
+
return assertReadableModel(instance);
|
|
55
55
|
}
|
|
56
56
|
export function soleHitOrFail(hits, subject) {
|
|
57
57
|
const [sole, ...rest] = hits;
|
|
@@ -65,7 +65,9 @@ export async function findInstance({ targets, instanceId, requestTag, }) {
|
|
|
65
65
|
const instance = await target.client.getDocument(instanceId, {
|
|
66
66
|
tag: requestTag,
|
|
67
67
|
});
|
|
68
|
-
return instance === undefined
|
|
68
|
+
return instance === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: { ...target, instance: assertReadableModel(instance) };
|
|
69
71
|
}));
|
|
70
72
|
const hits = probes
|
|
71
73
|
.filter((probe) => probe.status === 'fulfilled')
|
package/dist/lib/definitions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WORKFLOW_DEFINITION_TYPE, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, assertReadableModel, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
2
|
import { fail } from "./fail.js";
|
|
3
3
|
export function buildDefinitionShowQuery(flags) {
|
|
4
4
|
const params = { name: flags.name };
|
|
@@ -28,7 +28,7 @@ export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
|
28
28
|
if (deployed === null && version !== undefined) {
|
|
29
29
|
fail(`No deployed definition "${name}" v${version}.`);
|
|
30
30
|
}
|
|
31
|
-
return deployed
|
|
31
|
+
return deployed === null ? undefined : assertReadableModel(deployed);
|
|
32
32
|
}
|
|
33
33
|
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
34
34
|
if (!only) {
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** The environment tag (e.g. prod, test). For write commands it selects which
|
|
2
|
-
* deployment in the discovered `sanity.workflow` config to act on
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* deployment in the discovered `sanity.workflow` config to act on. A sole
|
|
3
|
+
* deployment needs no tag, and an interactive `deploy` asks when several are
|
|
4
|
+
* configured. For read commands it's an optional query filter — and the
|
|
5
|
+
* resource disambiguator when a config spans more than one. */
|
|
5
6
|
export declare const tagFlags: {
|
|
6
7
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type WorkflowClient, type WorkflowResource } from '@sanity/workflow-engine';
|
|
2
|
+
/** A resource a nuke may delete from, with the client that reads/writes it and
|
|
3
|
+
* whether it is the engine's own resource — the only one that holds instances
|
|
4
|
+
* and definitions (guards can co-locate with subjects in any resource). */
|
|
5
|
+
export interface NukeTarget {
|
|
6
|
+
resource: WorkflowResource;
|
|
7
|
+
client: WorkflowClient;
|
|
8
|
+
holdsEngineDocs: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** One resolved resource of a {@link NukePlan}: the ids to delete, by doc type. */
|
|
11
|
+
export interface NukeResourcePlan {
|
|
12
|
+
resource: WorkflowResource;
|
|
13
|
+
/** The `project.dataset` (or `<type>:<id>`) label the operator confirms by. */
|
|
14
|
+
label: string;
|
|
15
|
+
client: WorkflowClient;
|
|
16
|
+
holdsEngineDocs: boolean;
|
|
17
|
+
instanceIds: string[];
|
|
18
|
+
definitionIds: string[];
|
|
19
|
+
guardIds: string[];
|
|
20
|
+
}
|
|
21
|
+
/** The dry-run plan: what a `nuke --tag <tag>` would delete, per resource. */
|
|
22
|
+
export interface NukePlan {
|
|
23
|
+
tag: string;
|
|
24
|
+
resources: NukeResourcePlan[];
|
|
25
|
+
}
|
|
26
|
+
/** Per-doc-type totals across the whole plan, plus the count of resources that
|
|
27
|
+
* actually hold something to delete ({@link involvedTargets}). */
|
|
28
|
+
export interface PlanCounts {
|
|
29
|
+
instances: number;
|
|
30
|
+
definitions: number;
|
|
31
|
+
guards: number;
|
|
32
|
+
datasets: number;
|
|
33
|
+
total: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve what a tag-scoped nuke would delete, per resource — the dry-run plan
|
|
37
|
+
* printed before any deletion. Instances and definitions are read only from the
|
|
38
|
+
* engine's own resource; guards are read from every target, since a guard
|
|
39
|
+
* co-locates with the subject it locks and may live in a foreign dataset.
|
|
40
|
+
*
|
|
41
|
+
* Every read uses the raw perspective, so a draft-form or otherwise
|
|
42
|
+
* perspective-hidden copy is still surfaced (and swept).
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveNukePlan(args: {
|
|
45
|
+
tag: string;
|
|
46
|
+
targets: NukeTarget[];
|
|
47
|
+
}): Promise<NukePlan>;
|
|
48
|
+
/**
|
|
49
|
+
* Does a guard doc belong to `tag`? A guard's `sourceInstanceId` is the id of
|
|
50
|
+
* the instance that registered it (`<tag>.wf-instance.<random>`), and its own
|
|
51
|
+
* `_id` embeds that same instance id
|
|
52
|
+
* (`temp.system.guard.<tag>.wf-instance.<random>.<name>`). Matching on either —
|
|
53
|
+
* with the trailing `.` so `prod` never matches `prod-eu` — catches guards
|
|
54
|
+
* whose `sourceInstanceId` was never written as well as guards whose instance
|
|
55
|
+
* is already gone.
|
|
56
|
+
*/
|
|
57
|
+
export declare function guardMatchesTag(guard: {
|
|
58
|
+
_id: string;
|
|
59
|
+
sourceInstanceId?: string;
|
|
60
|
+
}, tag: string): boolean;
|
|
61
|
+
/** The `project.dataset` labels of resources the plan actually deletes from —
|
|
62
|
+
* the exact set the operator must type back to confirm. A resource with
|
|
63
|
+
* nothing to delete is not "involved" and is omitted. */
|
|
64
|
+
export declare function involvedTargets(plan: NukePlan): string[];
|
|
65
|
+
/** Roll the plan up into per-doc-type totals for the summary and the
|
|
66
|
+
* nothing-to-do short-circuit. */
|
|
67
|
+
export declare function planCounts(plan: NukePlan): PlanCounts;
|
|
68
|
+
/**
|
|
69
|
+
* Does the operator's typed confirmation name every involved target, and only
|
|
70
|
+
* those? Order-independent, whitespace-tolerant set equality: the operator
|
|
71
|
+
* pastes back all `project.dataset` targets the plan listed.
|
|
72
|
+
*/
|
|
73
|
+
export declare function confirmationMatches(input: string, targets: string[]): boolean;
|
|
74
|
+
/** The dry-run plan as printable lines: a heading, the content-safety
|
|
75
|
+
* invariant, the API host the deletes will hit (plan rows alone can't tell
|
|
76
|
+
* same-named projects on different environments apart), and a per-resource,
|
|
77
|
+
* per-doc-type count table. A guard-only foreign resource shows `—` for the
|
|
78
|
+
* engine-only columns, since those docs can't live there. */
|
|
79
|
+
export declare function renderNukePlan(plan: NukePlan, apiHost: string): string[];
|
|
80
|
+
/** The success line: what was deleted, and across how many datasets. */
|
|
81
|
+
export declare function formatNukeSummary(tag: string, counts: PlanCounts): string;
|
|
82
|
+
/**
|
|
83
|
+
* Delete exactly what the plan printed, in uniform waves over every resource.
|
|
84
|
+
* Wave order is referrers-first — instances, then definitions, then guards —
|
|
85
|
+
* a nicety for an interrupted run (engine docs link by plain strings, so the
|
|
86
|
+
* lake enforces no order); non-engine resources simply carry empty
|
|
87
|
+
* instance/definition lists. Idempotent: an empty plan deletes nothing.
|
|
88
|
+
*/
|
|
89
|
+
export declare function executeNuke(plan: NukePlan): Promise<void>;
|
package/dist/lib/nuke.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { GUARD_DOC_TYPE, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
|
|
3
|
+
import { formatTable, resourceLabel, sectionHeader } from "./ui.js";
|
|
4
|
+
const CHUNK = 200;
|
|
5
|
+
const REQUEST_TAG = 'nuke';
|
|
6
|
+
export async function resolveNukePlan(args) {
|
|
7
|
+
const { tag, targets } = args;
|
|
8
|
+
const resources = await Promise.all(targets.map((target) => resolveResourcePlan({ tag, target })));
|
|
9
|
+
return { tag, resources };
|
|
10
|
+
}
|
|
11
|
+
async function resolveResourcePlan(args) {
|
|
12
|
+
const { tag, target } = args;
|
|
13
|
+
const { resource, client, holdsEngineDocs } = target;
|
|
14
|
+
const noIds = [];
|
|
15
|
+
const [instanceIds, definitionIds, guardIds] = await Promise.all([
|
|
16
|
+
holdsEngineDocs ? engineDocIds({ client, type: WORKFLOW_INSTANCE_TYPE, tag }) : noIds,
|
|
17
|
+
holdsEngineDocs ? engineDocIds({ client, type: WORKFLOW_DEFINITION_TYPE, tag }) : noIds,
|
|
18
|
+
tagGuardIds({ client, tag }),
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
resource,
|
|
22
|
+
label: resourceLabel(resource),
|
|
23
|
+
client,
|
|
24
|
+
holdsEngineDocs,
|
|
25
|
+
instanceIds,
|
|
26
|
+
definitionIds,
|
|
27
|
+
guardIds,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function engineDocIds(args) {
|
|
31
|
+
return args.client.fetch(`*[_type == $type && ${tagScopeFilter()}]._id`, { type: args.type, tag: args.tag }, { perspective: 'raw', tag: REQUEST_TAG });
|
|
32
|
+
}
|
|
33
|
+
async function tagGuardIds(args) {
|
|
34
|
+
const guards = await args.client.fetch(`*[_type == $type]{_id, sourceInstanceId}`, { type: GUARD_DOC_TYPE }, { perspective: 'raw', tag: REQUEST_TAG });
|
|
35
|
+
return guards.filter((guard) => guardMatchesTag(guard, args.tag)).map((guard) => guard._id);
|
|
36
|
+
}
|
|
37
|
+
export function guardMatchesTag(guard, tag) {
|
|
38
|
+
const instancePrefix = `${tag}.wf-instance.`;
|
|
39
|
+
const guardIdPrefix = `${GUARD_DOC_TYPE}.${instancePrefix}`;
|
|
40
|
+
return ((guard.sourceInstanceId?.startsWith(instancePrefix) ?? false) ||
|
|
41
|
+
guard._id.startsWith(guardIdPrefix));
|
|
42
|
+
}
|
|
43
|
+
export function involvedTargets(plan) {
|
|
44
|
+
return plan.resources
|
|
45
|
+
.filter((resource) => resourceTotal(resource) > 0)
|
|
46
|
+
.map((resource) => resource.label);
|
|
47
|
+
}
|
|
48
|
+
function resourceTotal(resource) {
|
|
49
|
+
return resource.instanceIds.length + resource.definitionIds.length + resource.guardIds.length;
|
|
50
|
+
}
|
|
51
|
+
export function planCounts(plan) {
|
|
52
|
+
const totals = plan.resources.reduce((acc, resource) => ({
|
|
53
|
+
instances: acc.instances + resource.instanceIds.length,
|
|
54
|
+
definitions: acc.definitions + resource.definitionIds.length,
|
|
55
|
+
guards: acc.guards + resource.guardIds.length,
|
|
56
|
+
}), { instances: 0, definitions: 0, guards: 0 });
|
|
57
|
+
return {
|
|
58
|
+
...totals,
|
|
59
|
+
datasets: involvedTargets(plan).length,
|
|
60
|
+
total: totals.instances + totals.definitions + totals.guards,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function confirmationMatches(input, targets) {
|
|
64
|
+
const typed = new Set(input.trim().split(/\s+/).filter(Boolean));
|
|
65
|
+
const expected = new Set(targets);
|
|
66
|
+
return typed.size === expected.size && [...expected].every((target) => typed.has(target));
|
|
67
|
+
}
|
|
68
|
+
export function renderNukePlan(plan, apiHost) {
|
|
69
|
+
const rows = plan.resources.map((resource) => [
|
|
70
|
+
resource.label,
|
|
71
|
+
resource.holdsEngineDocs ? String(resource.instanceIds.length) : styleText('dim', '—'),
|
|
72
|
+
resource.holdsEngineDocs ? String(resource.definitionIds.length) : styleText('dim', '—'),
|
|
73
|
+
String(resource.guardIds.length),
|
|
74
|
+
]);
|
|
75
|
+
return [
|
|
76
|
+
sectionHeader(`nuke — tag "${plan.tag}"`),
|
|
77
|
+
styleText('dim', 'engine-owned documents only — content is never touched'),
|
|
78
|
+
styleText('dim', `API host: ${apiHost}`),
|
|
79
|
+
...formatTable(['target', 'instances', 'definitions', 'guards'], rows),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
export function formatNukeSummary(tag, counts) {
|
|
83
|
+
const parts = [
|
|
84
|
+
pluralize(counts.instances, 'instance'),
|
|
85
|
+
pluralize(counts.definitions, 'definition'),
|
|
86
|
+
pluralize(counts.guards, 'guard'),
|
|
87
|
+
].join(', ');
|
|
88
|
+
return `Nuked tag "${tag}": ${parts} across ${pluralize(counts.datasets, 'dataset')}.`;
|
|
89
|
+
}
|
|
90
|
+
function pluralize(count, noun) {
|
|
91
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
92
|
+
}
|
|
93
|
+
export async function executeNuke(plan) {
|
|
94
|
+
for (const resource of plan.resources) {
|
|
95
|
+
await deleteIds(resource.client, resource.instanceIds);
|
|
96
|
+
}
|
|
97
|
+
for (const resource of plan.resources) {
|
|
98
|
+
await deleteIds(resource.client, resource.definitionIds);
|
|
99
|
+
}
|
|
100
|
+
for (const resource of plan.resources) {
|
|
101
|
+
await deleteIds(resource.client, resource.guardIds);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function deleteIds(client, ids) {
|
|
105
|
+
for (let start = 0; start < ids.length; start += CHUNK) {
|
|
106
|
+
const tx = client.transaction();
|
|
107
|
+
for (const id of ids.slice(start, start + CHUNK))
|
|
108
|
+
tx.delete(id);
|
|
109
|
+
await tx.commit({ tag: REQUEST_TAG });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -3,7 +3,9 @@ import { type EngineScopeArgs, type OperationArgs, type WorkflowTelemetryLogger
|
|
|
3
3
|
* The engine-scope a CLI verb needs: which tag partition and which resource
|
|
4
4
|
* the engine's own data lives in — {@link EngineScopeArgs} minus the client.
|
|
5
5
|
* A {@link import('@sanity/workflow-engine').WorkflowDeployment} satisfies it,
|
|
6
|
-
* so commands pass their resolved deployment straight through.
|
|
6
|
+
* so commands pass their resolved deployment straight through. The CLI passes
|
|
7
|
+
* no `resourceClients`, so its writes admit runtime-supplied refs to the
|
|
8
|
+
* workflow resource only.
|
|
7
9
|
*/
|
|
8
10
|
export type EngineScope = Pick<EngineScopeArgs, 'tag' | 'workflowResource'>;
|
|
9
11
|
/**
|
package/dist/lib/ops-report.d.ts
CHANGED
|
@@ -38,7 +38,8 @@ export declare function cascadeTail(cascaded: number): string;
|
|
|
38
38
|
* emit the report — warn and bail on a no-op (`changed: false`), otherwise
|
|
39
39
|
* succeed and print the ops block. A throw fails the spinner and exits
|
|
40
40
|
* through {@link fail}. The one spinner + report + catch shape every write
|
|
41
|
-
* command (abort, set-stage, fire-action, start, definition delete)
|
|
41
|
+
* command (abort, set-stage, fire-action, start, definition delete, nuke)
|
|
42
|
+
* shares.
|
|
42
43
|
*/
|
|
43
44
|
export declare function runWriteVerb<T>({ startLabel, failLabel, failHeadline, run, report, log, }: {
|
|
44
45
|
startLabel: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can this process ask a question on stderr? A strict superset of the
|
|
3
|
+
* {@link isInteractive} gate cli-core's own prompts enforce (stdin TTY,
|
|
4
|
+
* `TERM` not dumb, no `CI` — so its `NonInteractiveError` can never escape a
|
|
5
|
+
* caller that checked here first), adding stderr-TTY because this package's
|
|
6
|
+
* prompts render on stderr: a redirected stderr must refuse rather than block
|
|
7
|
+
* on stdin with the question swallowed. It classifies whether definition
|
|
8
|
+
* sharing can show its full first-run disclosure and guards interactive
|
|
9
|
+
* prompts such as nuke's confirmation.
|
|
10
|
+
*/
|
|
11
|
+
export declare function canPromptOnStderr(): boolean;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
|
|
2
|
+
type ChooseDeploymentTag = (deployments: WorkflowDeployment[]) => Promise<string>;
|
|
3
|
+
interface DeploymentSelectionOptions {
|
|
4
|
+
tag: string | undefined;
|
|
5
|
+
allTags: boolean;
|
|
6
|
+
interactive?: boolean;
|
|
7
|
+
chooseTag?: ChooseDeploymentTag;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Pick the deployment for the requested `--tag`. Tags are unique across a
|
|
4
11
|
* config (enforced by defineWorkflowConfig), so a tag names at most one
|
|
@@ -15,17 +22,15 @@ export declare function selectDeployment(config: WorkflowConfig, { tag, orAltern
|
|
|
15
22
|
}): WorkflowDeployment;
|
|
16
23
|
/**
|
|
17
24
|
* The deployments a `deploy` run acts on: every one with `--all-tags`,
|
|
18
|
-
* otherwise a single target via {@link selectDeployment}
|
|
19
|
-
*
|
|
20
|
-
*
|
|
25
|
+
* otherwise a single target via {@link selectDeployment}. An ambiguous bare
|
|
26
|
+
* interactive run asks for a tag; a run that cannot prompt keeps the explicit
|
|
27
|
+
* flag guidance. Deploying everything is an explicit opt-in — never a default.
|
|
21
28
|
*/
|
|
22
|
-
export declare function selectDeployments(config: WorkflowConfig, { tag, allTags }:
|
|
23
|
-
tag: string | undefined;
|
|
24
|
-
allTags: boolean;
|
|
25
|
-
}): WorkflowDeployment[];
|
|
29
|
+
export declare function selectDeployments(config: WorkflowConfig, { tag, allTags, interactive, chooseTag }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
|
|
26
30
|
/**
|
|
27
31
|
* Project a deployment into the engine's {@link DeployTarget} — the shape the
|
|
28
32
|
* deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
|
|
29
33
|
* consume — expanding its handle bindings into the alias map in the same step.
|
|
30
34
|
*/
|
|
31
35
|
export declare function deploymentToTarget(deployment: WorkflowDeployment): DeployTarget;
|
|
36
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { select } from '@sanity/cli-core/ux';
|
|
1
2
|
import { resourceAliasesToMap, } from '@sanity/workflow-engine';
|
|
2
3
|
import { fail } from "./fail.js";
|
|
4
|
+
import { canPromptOnStderr } from "./prompt.js";
|
|
3
5
|
export function selectDeployment(config, { tag, orAlternative = '' }) {
|
|
4
6
|
if (tag === undefined) {
|
|
5
7
|
if (config.deployments.length > 1) {
|
|
@@ -17,19 +19,42 @@ export function selectDeployment(config, { tag, orAlternative = '' }) {
|
|
|
17
19
|
}
|
|
18
20
|
return deployment;
|
|
19
21
|
}
|
|
20
|
-
export function selectDeployments(config, { tag, allTags }) {
|
|
22
|
+
export async function selectDeployments(config, { tag, allTags, interactive, chooseTag = chooseDeploymentTag }) {
|
|
21
23
|
if (allTags) {
|
|
22
24
|
return config.deployments;
|
|
23
25
|
}
|
|
26
|
+
if (tag === undefined && config.deployments.length > 1) {
|
|
27
|
+
if (!(interactive ?? canPromptOnStderr())) {
|
|
28
|
+
return [
|
|
29
|
+
selectDeployment(config, {
|
|
30
|
+
tag,
|
|
31
|
+
orAlternative: ', or --all-tags to deploy every deployment',
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
const selectedTag = await chooseTag(config.deployments);
|
|
36
|
+
return [selectDeployment(config, { tag: selectedTag })];
|
|
37
|
+
}
|
|
24
38
|
return [
|
|
25
39
|
selectDeployment(config, { tag, orAlternative: ', or --all-tags to deploy every deployment' }),
|
|
26
40
|
];
|
|
27
41
|
}
|
|
42
|
+
async function chooseDeploymentTag(deployments) {
|
|
43
|
+
return select({
|
|
44
|
+
message: 'Select a deployment tag',
|
|
45
|
+
choices: deployments.map(({ name, tag }) => ({
|
|
46
|
+
name: tag,
|
|
47
|
+
value: tag,
|
|
48
|
+
description: name,
|
|
49
|
+
})),
|
|
50
|
+
}, { output: process.stderr });
|
|
51
|
+
}
|
|
28
52
|
function availableTags(config) {
|
|
29
53
|
return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
|
|
30
54
|
}
|
|
31
55
|
export function deploymentToTarget(deployment) {
|
|
32
56
|
return {
|
|
57
|
+
expectedMinReaderModel: deployment.expectedMinReaderModel,
|
|
33
58
|
tag: deployment.tag,
|
|
34
59
|
workflowResource: deployment.workflowResource,
|
|
35
60
|
resourceAliases: resourceAliasesToMap(deployment.resourceAliases),
|
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
* Consent model: opt-OUT. A deploy that creates new definition versions
|
|
11
11
|
* donates by default. The choice is skipped by an explicit flag:
|
|
12
12
|
* `--share-defs` (donate, no prompt) or `--no-share-defs` (don't). Without a
|
|
13
|
-
* flag,
|
|
14
|
-
* in the shared Sanity user config
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* "silent in CI / `DO_NOT_TRACK`" rule; opt in per run with `--share-defs`.
|
|
13
|
+
* flag, the CLI shares. An interactive terminal gets a ONE-TIME notice
|
|
14
|
+
* (persisted in the shared Sanity user config) before the first donation;
|
|
15
|
+
* every later flagless deploy, and every unattended deploy, prints a concise
|
|
16
|
+
* reminder. CI, `DO_NOT_TRACK`, and non-TTY execution do not change the
|
|
17
|
+
* content-sharing default. Only `--no-share-defs` opts out.
|
|
19
18
|
*/
|
|
20
19
|
import { type DeployDefinitionResult, type DeployedDefinition, type WorkflowResource } from '@sanity/workflow-engine';
|
|
21
20
|
import type { UserConfigStore } from './telemetry-setup.ts';
|
|
@@ -50,22 +49,20 @@ export interface ShareCandidate {
|
|
|
50
49
|
* routed project-agnostically through the API gateway). */
|
|
51
50
|
export declare const SHARE_ENDPOINT_URI = "/workflow/definition-feedback";
|
|
52
51
|
/**
|
|
53
|
-
* The first-run
|
|
52
|
+
* The first-run disclosure — a product contract, pinned by test. It must
|
|
54
53
|
* name the recipient (Sanity), state that the document ships VERBATIM with its
|
|
55
54
|
* deployment coordinates and is kept, name what is never shared, and disclose
|
|
56
|
-
* that the
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* that the notice is remembered machine-wide and name the per-run opt-out.
|
|
56
|
+
* This is a notice, not a choice: sharing is the default unless the invocation
|
|
57
|
+
* supplies `--no-share-defs`.
|
|
59
58
|
*/
|
|
60
|
-
export declare const
|
|
59
|
+
export declare const SHARE_FIRST_RUN_NOTICE: string;
|
|
61
60
|
/**
|
|
62
|
-
* The
|
|
63
|
-
* first
|
|
64
|
-
*
|
|
65
|
-
* override. `_ON` shows when the remembered answer shares, `_OFF` when it does not.
|
|
61
|
+
* The status block printed on every flagless deploy that does not show the
|
|
62
|
+
* first-run disclosure. It keeps the default donation obvious in terminals and
|
|
63
|
+
* unattended logs and names both explicit controls.
|
|
66
64
|
*/
|
|
67
|
-
export declare const
|
|
68
|
-
export declare const SHARE_REMINDER_OFF: string;
|
|
65
|
+
export declare const SHARE_REMINDER: string;
|
|
69
66
|
/**
|
|
70
67
|
* The content-free telemetry marker for one shared definition — projected
|
|
71
68
|
* from the engine's own deploy-event derivation ({@link definitionDeployedData})
|
|
@@ -80,9 +77,9 @@ export declare function definitionShareMarker(args: {
|
|
|
80
77
|
}): WorkflowDefinitionSharedData;
|
|
81
78
|
/**
|
|
82
79
|
* Whether the invocation's telemetry should send despite the environment
|
|
83
|
-
* denial (CI / `DO_NOT_TRACK`).
|
|
84
|
-
*
|
|
85
|
-
*
|
|
80
|
+
* denial (CI / `DO_NOT_TRACK`). Every flagless or explicitly enabled deploy
|
|
81
|
+
* that can create versions forces it, because definition donation defaults on
|
|
82
|
+
* in those environments too. Account-level consent is never
|
|
86
83
|
* overridden (the intake still reads `/intake/telemetry-status`).
|
|
87
84
|
*
|
|
88
85
|
* Computed from raw argv because the prerun hook builds the store before oclif
|
|
@@ -98,12 +95,12 @@ export declare function shouldForceShareTelemetry(args: {
|
|
|
98
95
|
argv: string[];
|
|
99
96
|
}): boolean;
|
|
100
97
|
/**
|
|
101
|
-
* The whole post-deploy sharing flow.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* adoption dashboards can measure opt-out rate. No newly created
|
|
105
|
-
* (unchanged re-deploys, dry runs, nothing succeeded) means no
|
|
106
|
-
*
|
|
98
|
+
* The whole post-deploy sharing flow. An explicit flag decides directly;
|
|
99
|
+
* otherwise sharing defaults on after emitting either the one-time disclosure
|
|
100
|
+
* or recurring reminder. It always records the content-free per-invocation
|
|
101
|
+
* decision so adoption dashboards can measure opt-out rate. No newly created
|
|
102
|
+
* versions (unchanged re-deploys, dry runs, nothing succeeded) means no
|
|
103
|
+
* decision, notice, or POST.
|
|
107
104
|
*
|
|
108
105
|
* The optional args exist for the flow's dependencies, mirroring
|
|
109
106
|
* `setupCliTelemetry` — omitted, the real process surfaces apply.
|
|
@@ -113,8 +110,6 @@ export declare function shareDefinitionsAfterDeploy(args: {
|
|
|
113
110
|
candidates: ShareCandidate[];
|
|
114
111
|
warn: (message: string) => void;
|
|
115
112
|
interactive?: boolean;
|
|
116
|
-
envDenied?: boolean;
|
|
117
113
|
writeStderr?: (message: string) => void;
|
|
118
114
|
userConfig?: UserConfigStore;
|
|
119
|
-
confirmShare?: () => Promise<boolean>;
|
|
120
115
|
}): Promise<void>;
|