@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
package/dist/lib/ops-report.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Ora } from 'ora';
|
|
2
1
|
/** A single engine primitive that ran during a write commit — the
|
|
3
2
|
* reportable subset of the engine's `OpAppliedSummary`. */
|
|
4
3
|
export interface RanOp {
|
|
@@ -9,26 +8,43 @@ export interface RanOp {
|
|
|
9
8
|
};
|
|
10
9
|
}
|
|
11
10
|
/**
|
|
12
|
-
* The "ops applied" block shared by write-verb reports (
|
|
11
|
+
* The "ops applied" block shared by write-verb reports (set-stage,
|
|
13
12
|
* fire-action): a blank separator, a header, and one line per primitive
|
|
14
13
|
* that ran. Empty when nothing ran, so callers append it unconditionally.
|
|
15
14
|
*/
|
|
16
15
|
export declare function opsAppliedLines(ranOps: RanOp[] | undefined): string[];
|
|
16
|
+
/** The subset of an engine `OperationResult` the write-verb reports read. */
|
|
17
|
+
export interface WriteOutcome {
|
|
18
|
+
changed: boolean;
|
|
19
|
+
cascaded: number;
|
|
20
|
+
instance: {
|
|
21
|
+
currentStage: string;
|
|
22
|
+
};
|
|
23
|
+
ranOps?: RanOp[];
|
|
24
|
+
}
|
|
17
25
|
/** The spinner message + ops block a write verb prints once its engine
|
|
18
|
-
* call returns. {@link opsAppliedLines} builds `opsLines
|
|
26
|
+
* call returns. {@link opsAppliedLines} builds `opsLines`; verbs whose
|
|
27
|
+
* commits never run ops omit it. */
|
|
19
28
|
export interface WriteReport {
|
|
20
|
-
|
|
29
|
+
changed: boolean;
|
|
21
30
|
message: string;
|
|
22
|
-
opsLines
|
|
31
|
+
opsLines?: string[];
|
|
23
32
|
}
|
|
33
|
+
/** The ", then cascaded N auto-transition(s)" suffix a write report appends
|
|
34
|
+
* when the commit cascaded the instance onward; empty when it didn't. */
|
|
35
|
+
export declare function cascadeTail(cascaded: number): string;
|
|
24
36
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
37
|
+
* Run a write verb behind its spinner: start it, run the engine call, and
|
|
38
|
+
* emit the report — warn and bail on a no-op (`changed: false`), otherwise
|
|
39
|
+
* succeed and print the ops block. A throw fails the spinner and exits
|
|
40
|
+
* through {@link fail}. The one spinner + report + catch shape every write
|
|
41
|
+
* command (abort, set-stage, fire-action, start, definition delete) shares.
|
|
29
42
|
*/
|
|
30
|
-
export declare function
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
export declare function runWriteVerb<T>({ startLabel, failLabel, failHeadline, run, report, log, }: {
|
|
44
|
+
startLabel: string;
|
|
45
|
+
failLabel: string;
|
|
46
|
+
failHeadline: string;
|
|
47
|
+
run: () => Promise<T>;
|
|
48
|
+
report: (result: T) => WriteReport;
|
|
33
49
|
log: (line: string) => void;
|
|
34
|
-
}): void
|
|
50
|
+
}): Promise<void>;
|
package/dist/lib/ops-report.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import logSymbols from 'log-symbols';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* fire-action): a blank separator, a header, and one line per primitive
|
|
6
|
-
* that ran. Empty when nothing ran, so callers append it unconditionally.
|
|
7
|
-
*/
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { fail, failureDetail } from "./fail.js";
|
|
8
5
|
export function opsAppliedLines(ranOps) {
|
|
9
6
|
const details = (ranOps ?? []).map((op) => {
|
|
10
7
|
const target = op.target ? styleText('dim', ` → ${op.target.scope}.${op.target.field}`) : '';
|
|
@@ -12,17 +9,23 @@ export function opsAppliedLines(ranOps) {
|
|
|
12
9
|
});
|
|
13
10
|
return details.length > 0 ? ['', styleText('dim', 'ops applied:'), ...details] : [];
|
|
14
11
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
export function cascadeTail(cascaded) {
|
|
13
|
+
return cascaded > 0 ? `, then cascaded ${cascaded} auto-transition(s)` : '';
|
|
14
|
+
}
|
|
15
|
+
export async function runWriteVerb({ startLabel, failLabel, failHeadline, run, report, log, }) {
|
|
16
|
+
const spinner = ora(startLabel).start();
|
|
17
|
+
try {
|
|
18
|
+
const result = await run();
|
|
19
|
+
const emitted = report(result);
|
|
20
|
+
if (!emitted.changed) {
|
|
21
|
+
spinner.warn(emitted.message);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
spinner.succeed(emitted.message);
|
|
25
|
+
emitted.opsLines?.forEach(log);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
spinner.fail(failLabel);
|
|
29
|
+
fail(failHeadline, failureDetail(error));
|
|
25
30
|
}
|
|
26
|
-
spinner.succeed(report.message);
|
|
27
|
-
report.opsLines.forEach(log);
|
|
28
31
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse repeated `key=value` flag pairs (`--param`, `--field`) into an
|
|
3
|
+
* object. Each value is JSON-parsed so numbers/booleans/objects type
|
|
4
|
+
* correctly, falling back to the raw string. The engine validates the
|
|
5
|
+
* result against the declared types the consuming flag targets.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseParams(pairs: string[]): Record<string, unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function coerceValue(raw) {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(raw);
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return raw;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function parsePair(pair) {
|
|
10
|
+
const eq = pair.indexOf('=');
|
|
11
|
+
if (eq < 1) {
|
|
12
|
+
throw new Error(`expected key=value, got "${pair}"`);
|
|
13
|
+
}
|
|
14
|
+
return [pair.slice(0, eq), coerceValue(pair.slice(eq + 1))];
|
|
15
|
+
}
|
|
16
|
+
export function parseParams(pairs) {
|
|
17
|
+
return Object.fromEntries(pairs.map(parsePair));
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkflowResource } from '@sanity/workflow-engine';
|
|
2
|
+
import type { ReadTarget } from './context.ts';
|
|
3
|
+
/** A read target whose query threw — noted in place and reflected in the
|
|
4
|
+
* command's exit code. */
|
|
5
|
+
export interface ReadFailure {
|
|
6
|
+
resource: WorkflowResource;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run a read over every target, grouping output per resource: a banner when
|
|
11
|
+
* the read spans several, then whatever `run` logs for that target. A throw
|
|
12
|
+
* from one target is noted under its banner and the rest still render — a
|
|
13
|
+
* token that can't read one dataset must not sink the whole listing. A
|
|
14
|
+
* single-target run rethrows instead, keeping the plain unprefixed output
|
|
15
|
+
* and clean error rendering at the command boundary. Callers exit non-zero
|
|
16
|
+
* when failures come back.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runReadAcrossTargets({ targets, log, run, }: {
|
|
19
|
+
targets: ReadTarget[];
|
|
20
|
+
log: (line: string) => void;
|
|
21
|
+
run: (target: ReadTarget) => Promise<void>;
|
|
22
|
+
}): Promise<ReadFailure[]>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logSymbols from 'log-symbols';
|
|
2
|
+
import { failureDetail } from "./fail.js";
|
|
3
|
+
import { groupBanner, resourceLabel } from "./ui.js";
|
|
4
|
+
export async function runReadAcrossTargets({ targets, log, run, }) {
|
|
5
|
+
const failures = [];
|
|
6
|
+
for (const [index, target] of targets.entries()) {
|
|
7
|
+
const banner = groupBanner({
|
|
8
|
+
label: resourceLabel(target.resource),
|
|
9
|
+
index,
|
|
10
|
+
total: targets.length,
|
|
11
|
+
});
|
|
12
|
+
if (banner !== undefined) {
|
|
13
|
+
log(banner);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
await run(target);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (targets.length === 1) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const message = failureDetail(error);
|
|
23
|
+
failures.push({ resource: target.resource, message });
|
|
24
|
+
log(`${logSymbols.error} ${message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return failures;
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Pick the deployment for the requested `--tag`. Tags are unique across a
|
|
4
|
+
* config (enforced by defineWorkflowConfig), so a tag names at most one
|
|
5
|
+
* deployment.
|
|
6
|
+
*
|
|
7
|
+
* With no tag: fall back to the sole deployment when there's exactly one
|
|
8
|
+
* (the common single-environment case), otherwise fail asking for `--tag` —
|
|
9
|
+
* a multi-deployment config is ambiguous without it. `orAlternative` extends
|
|
10
|
+
* that ambiguity message for callers with another way out (deploy's `--all-tags`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
|
|
13
|
+
tag: string | undefined;
|
|
14
|
+
orAlternative?: string;
|
|
15
|
+
}): WorkflowDeployment;
|
|
16
|
+
/**
|
|
17
|
+
* The deployments a `deploy` run acts on: every one with `--all-tags`,
|
|
18
|
+
* otherwise a single target via {@link selectDeployment} and its failure
|
|
19
|
+
* modes. Deploying everything is an explicit opt-in — never a default — so an
|
|
20
|
+
* ambiguous bare `deploy` can't fan a write out across environments.
|
|
21
|
+
*/
|
|
22
|
+
export declare function selectDeployments(config: WorkflowConfig, { tag, allTags }: {
|
|
23
|
+
tag: string | undefined;
|
|
24
|
+
allTags: boolean;
|
|
25
|
+
}): WorkflowDeployment[];
|
|
26
|
+
/**
|
|
27
|
+
* Project a deployment into the engine's {@link DeployTarget} — the shape the
|
|
28
|
+
* deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
|
|
29
|
+
* consume — expanding its handle bindings into the alias map in the same step.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deploymentToTarget(deployment: WorkflowDeployment): DeployTarget;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resourceAliasesToMap, } from '@sanity/workflow-engine';
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
export function selectDeployment(config, { tag, orAlternative = '' }) {
|
|
4
|
+
if (tag === undefined) {
|
|
5
|
+
if (config.deployments.length > 1) {
|
|
6
|
+
fail(`Multiple deployments configured — pass --tag${orAlternative}.`, availableTags(config));
|
|
7
|
+
}
|
|
8
|
+
const [sole] = config.deployments;
|
|
9
|
+
if (sole === undefined) {
|
|
10
|
+
fail('No deployments configured.');
|
|
11
|
+
}
|
|
12
|
+
return sole;
|
|
13
|
+
}
|
|
14
|
+
const deployment = config.deployments.find((candidate) => candidate.tag === tag);
|
|
15
|
+
if (deployment === undefined) {
|
|
16
|
+
fail(`No deployment for tag "${tag}".`, availableTags(config));
|
|
17
|
+
}
|
|
18
|
+
return deployment;
|
|
19
|
+
}
|
|
20
|
+
export function selectDeployments(config, { tag, allTags }) {
|
|
21
|
+
if (allTags) {
|
|
22
|
+
return config.deployments;
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
selectDeployment(config, { tag, orAlternative: ', or --all-tags to deploy every deployment' }),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
function availableTags(config) {
|
|
29
|
+
return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
|
|
30
|
+
}
|
|
31
|
+
export function deploymentToTarget(deployment) {
|
|
32
|
+
return {
|
|
33
|
+
tag: deployment.tag,
|
|
34
|
+
workflowResource: deployment.workflowResource,
|
|
35
|
+
resourceAliases: resourceAliasesToMap(deployment.resourceAliases),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition sharing — an explicit per-invocation opt-in, independent of
|
|
3
|
+
* telemetry consent. Sharing deployed workflow definitions with Sanity is
|
|
4
|
+
* CONTENT sharing, not telemetry: definitions carry customer-authored
|
|
5
|
+
* strings (names, titles, GROQ filters) that the telemetry payload policy
|
|
6
|
+
* bans, so the documents go to Sanity's first-party definition-feedback
|
|
7
|
+
* endpoint — one POST per deploy invocation — and telemetry carries only
|
|
8
|
+
* a content-free marker per shared definition.
|
|
9
|
+
*
|
|
10
|
+
* Consent model: flag-only, never a prompt, nothing persists. Sharing
|
|
11
|
+
* happens exactly when `--share-definitions-with-sanity` is on the
|
|
12
|
+
* invocation; without it, an interactive deploy that created new
|
|
13
|
+
* definition versions logs a one-line stderr hint instead. The telemetry
|
|
14
|
+
* environment gates (`DO_NOT_TRACK`, account consent) do not apply to
|
|
15
|
+
* the feedback POST: it is explicit first-party consent, not tracking.
|
|
16
|
+
*/
|
|
17
|
+
import { type DeployDefinitionResult, type DeployedDefinition, type WorkflowResource } from '@sanity/workflow-engine';
|
|
18
|
+
import { type WorkflowDefinitionSharedData } from './telemetry.ts';
|
|
19
|
+
/** The client slice sharing uses — the read-back of just-deployed
|
|
20
|
+
* documents and the feedback POST, structurally, so a full
|
|
21
|
+
* `SanityClient` satisfies it and tests fake exactly these calls. */
|
|
22
|
+
export interface ShareClient {
|
|
23
|
+
fetch<T>(query: string, params: Record<string, unknown>, options: {
|
|
24
|
+
tag: string;
|
|
25
|
+
}): Promise<T>;
|
|
26
|
+
request<T>(opts: {
|
|
27
|
+
uri: string;
|
|
28
|
+
method: string;
|
|
29
|
+
body: unknown;
|
|
30
|
+
tag: string;
|
|
31
|
+
}): Promise<T>;
|
|
32
|
+
}
|
|
33
|
+
/** One successfully deployed deployment's outcome, paired with the client
|
|
34
|
+
* that can read the just-deployed documents back and the resource the
|
|
35
|
+
* entries' deployment coordinates derive from. */
|
|
36
|
+
export interface ShareCandidate {
|
|
37
|
+
client: ShareClient;
|
|
38
|
+
tag: string;
|
|
39
|
+
resource: WorkflowResource;
|
|
40
|
+
results: DeployDefinitionResult[];
|
|
41
|
+
/** The deploy call's run id — the markers carry the same id the engine's
|
|
42
|
+
* deploy events minted, so donated definitions group with their deploy. */
|
|
43
|
+
deployId: string;
|
|
44
|
+
}
|
|
45
|
+
/** Sanity's first-party definition-feedback endpoint (editorial-ai-backend,
|
|
46
|
+
* routed project-agnostically through the API gateway). */
|
|
47
|
+
export declare const SHARE_ENDPOINT_URI = "/workflow/definition-feedback";
|
|
48
|
+
/**
|
|
49
|
+
* The hint's full copy — a product contract, pinned by test: it must
|
|
50
|
+
* state honestly what sharing sends (the document ships VERBATIM, plus
|
|
51
|
+
* its deployment coordinates) and what never is sent. Logged to stderr
|
|
52
|
+
* after an interactive deploy that created new definition versions
|
|
53
|
+
* without the flag — never a prompt, and never on CI / non-TTY runs.
|
|
54
|
+
*/
|
|
55
|
+
export declare const SHARE_HINT: string;
|
|
56
|
+
/**
|
|
57
|
+
* The content-free telemetry marker for one shared definition — projected
|
|
58
|
+
* from the engine's own deploy-event derivation ({@link definitionDeployedData})
|
|
59
|
+
* so `Editorial Workflows Definition Shared` and `Editorial Workflows Definition Deployed` can
|
|
60
|
+
* never disagree about one definition's structure. Status is dropped:
|
|
61
|
+
* only newly created versions are ever shared.
|
|
62
|
+
*/
|
|
63
|
+
export declare function definitionShareMarker(args: {
|
|
64
|
+
definition: DeployedDefinition;
|
|
65
|
+
contentHash: string;
|
|
66
|
+
deployId: string;
|
|
67
|
+
}): WorkflowDefinitionSharedData;
|
|
68
|
+
/**
|
|
69
|
+
* The whole post-deploy sharing flow. With the flag: created versions →
|
|
70
|
+
* read-back → ONE feedback POST for the invocation → a content-free
|
|
71
|
+
* telemetry marker per shared definition. Without it: the stderr hint
|
|
72
|
+
* (interactive runs only), nothing sent. No newly created versions
|
|
73
|
+
* (unchanged re-deploys, dry runs, nothing succeeded) means no hint and
|
|
74
|
+
* no POST. Any sharing-path failure warns and returns: the deploy
|
|
75
|
+
* already succeeded, and sharing must never turn it into a failure.
|
|
76
|
+
*
|
|
77
|
+
* The optional args exist for the hint's dependencies, mirroring
|
|
78
|
+
* `setupCliTelemetry` — omitted, the real process surfaces apply.
|
|
79
|
+
*/
|
|
80
|
+
export declare function shareDefinitionsAfterDeploy(args: {
|
|
81
|
+
flag: boolean;
|
|
82
|
+
candidates: ShareCandidate[];
|
|
83
|
+
warn: (message: string) => void;
|
|
84
|
+
interactive?: boolean;
|
|
85
|
+
writeStderr?: (message: string) => void;
|
|
86
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { isInteractive } from '@sanity/cli-core';
|
|
3
|
+
import { definitionDeployedData, errorMessage, } from '@sanity/workflow-engine';
|
|
4
|
+
import { buildDefinitionShowQuery } from "./definitions.js";
|
|
5
|
+
import { cliTelemetry, WorkflowDefinitionShared, } from "./telemetry.js";
|
|
6
|
+
export const SHARE_ENDPOINT_URI = '/workflow/definition-feedback';
|
|
7
|
+
const SHARE_TAG = 'definition.share';
|
|
8
|
+
export const SHARE_HINT = `\n${styleText('bold', 'Enjoying workflows?')} Show us how you use them: deploy with\n` +
|
|
9
|
+
`${styleText('cyan', '--share-definitions-with-sanity')} to share newly created definition documents\n` +
|
|
10
|
+
'(verbatim, with their deployment coordinates) with Sanity — never content\n' +
|
|
11
|
+
'documents, workflow instances, or your Sanity auth token.\n';
|
|
12
|
+
export function definitionShareMarker(args) {
|
|
13
|
+
const { status: _status, ...structural } = definitionDeployedData(args.definition, {
|
|
14
|
+
contentHash: args.contentHash,
|
|
15
|
+
status: 'created',
|
|
16
|
+
deployId: args.deployId,
|
|
17
|
+
});
|
|
18
|
+
return structural;
|
|
19
|
+
}
|
|
20
|
+
async function loadShareEntry(args) {
|
|
21
|
+
const { candidate, result, warn } = args;
|
|
22
|
+
const deployed = await loadDeployedForShare(args);
|
|
23
|
+
if (deployed === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (deployed.contentHash === undefined) {
|
|
27
|
+
warn(`${result.name} v${result.version} carries no content hash — not shared.`);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
definition: deployed,
|
|
32
|
+
contentHash: deployed.contentHash,
|
|
33
|
+
name: result.name,
|
|
34
|
+
version: result.version,
|
|
35
|
+
resourceType: candidate.resource.type,
|
|
36
|
+
resourceId: candidate.resource.id,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function loadDeployedForShare(args) {
|
|
40
|
+
const { candidate, result, warn } = args;
|
|
41
|
+
const { groq, params } = buildDefinitionShowQuery({
|
|
42
|
+
name: result.name,
|
|
43
|
+
tag: candidate.tag,
|
|
44
|
+
version: result.version,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const deployed = await candidate.client.fetch(groq, params, {
|
|
48
|
+
tag: SHARE_TAG,
|
|
49
|
+
});
|
|
50
|
+
if (deployed === null) {
|
|
51
|
+
warn(`Could not read back ${result.name} v${result.version} — not shared.`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return deployed;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
warn(`Could not read back ${result.name} v${result.version} — not shared. ${errorMessage(error)}`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function shareDefinitionsAfterDeploy(args) {
|
|
62
|
+
const created = args.candidates.flatMap((candidate) => candidate.results
|
|
63
|
+
.filter((result) => result.status === 'created')
|
|
64
|
+
.map((result) => ({ candidate, result })));
|
|
65
|
+
if (created.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!args.flag) {
|
|
69
|
+
const interactive = args.interactive ?? (isInteractive() && process.stderr.isTTY === true);
|
|
70
|
+
if (interactive) {
|
|
71
|
+
const writeStderr = args.writeStderr ?? ((message) => void process.stderr.write(message));
|
|
72
|
+
writeStderr(SHARE_HINT);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const shared = [];
|
|
77
|
+
try {
|
|
78
|
+
for (const item of created) {
|
|
79
|
+
const entry = await loadShareEntry({ ...item, warn: args.warn });
|
|
80
|
+
if (entry !== undefined) {
|
|
81
|
+
shared.push({ entry, candidate: item.candidate });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (shared.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await shared[0].candidate.client.request({
|
|
88
|
+
uri: SHARE_ENDPOINT_URI,
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: { definitions: shared.map(({ entry }) => entry) },
|
|
91
|
+
tag: SHARE_TAG,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
args.warn(`Definition sharing failed — nothing shared. ${errorMessage(error)}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const { logger } = cliTelemetry();
|
|
100
|
+
for (const { entry, candidate } of shared) {
|
|
101
|
+
logger.log(WorkflowDefinitionShared, definitionShareMarker({ ...entry, deployId: candidate.deployId }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/lib/stub.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import boxen from 'boxen';
|
|
3
3
|
import logSymbols from 'log-symbols';
|
|
4
|
-
/**
|
|
5
|
-
* Base class for commands that aren't wired to the engine yet. Parses
|
|
6
|
-
* the subclass's args + flags, then prints a "not yet implemented"
|
|
7
|
-
* banner plus the parsed inputs so the surface is testable end-to-end,
|
|
8
|
-
* and exits non-zero — a stub must never report success to a caller.
|
|
9
|
-
*/
|
|
10
4
|
export class StubCommand extends Command {
|
|
11
5
|
async run() {
|
|
12
|
-
// `this.parse()` with no args picks up the subclass's static metadata.
|
|
13
6
|
const parsed = await this.parse(this.ctor);
|
|
14
7
|
const body = [
|
|
15
8
|
`${logSymbols.warning} not yet implemented`,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Once-per-invocation assembly of the CLI telemetry shell — the `prerun`
|
|
3
|
+
* hook's whole job. Discovery is deliberately soft: telemetry must never
|
|
4
|
+
* break (or exit) a command, so a missing/broken config, missing token,
|
|
5
|
+
* or dataset-less config just leaves the invocation with telemetry off;
|
|
6
|
+
* the command itself surfaces those problems where they matter.
|
|
7
|
+
*/
|
|
8
|
+
import { processShellUserProperties, type TelemetryIntakeClient, type WorkflowResource } from '@sanity/workflow-engine';
|
|
9
|
+
/** The get/set slice of the shared Sanity user config (cli-core's
|
|
10
|
+
* `getUserConfig`) the shell persists flags into — narrow so tests fake
|
|
11
|
+
* exactly what's read and written. */
|
|
12
|
+
export interface UserConfigStore {
|
|
13
|
+
get: (key: string) => unknown;
|
|
14
|
+
set: (key: string, value: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The one-time transparency notice, mirroring the sanity CLI's
|
|
18
|
+
* `telemetryDisclosure` — both CLIs feed the same pipeline. Written to
|
|
19
|
+
* stderr so it never garbles command output.
|
|
20
|
+
*/
|
|
21
|
+
export declare function maybeShowTelemetryDisclosure(args: {
|
|
22
|
+
userConfig: UserConfigStore;
|
|
23
|
+
writeStderr: (message: string) => void;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Assemble and install the invocation's shell: a config-provided
|
|
27
|
+
* `telemetry` logger replaces the built-in shell wholesale (none of its
|
|
28
|
+
* policy applies); otherwise the built-in Sanity-intake store is
|
|
29
|
+
* constructed when an auth token and a dataset-backed deployment exist —
|
|
30
|
+
* mirroring the standard recipe's "not logged in → nothing sent".
|
|
31
|
+
*
|
|
32
|
+
* The optional args exist for the hook's dependencies (cwd, environment,
|
|
33
|
+
* user config, stderr) — omitted, the real process surfaces apply.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setupCliTelemetry(args?: {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
env?: Record<string, string | undefined>;
|
|
38
|
+
userConfig?: UserConfigStore;
|
|
39
|
+
writeStderr?: (message: string) => void;
|
|
40
|
+
/** The oclif config's version — the prerun hook supplies it. */
|
|
41
|
+
cliVersion?: string;
|
|
42
|
+
/** Intake client override — tests inject a fake here, like the other
|
|
43
|
+
* hook dependencies above; omitted, the real project client applies. */
|
|
44
|
+
client?: TelemetryIntakeClient;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* The invocation's environment user properties — the sanity CLI's precedent
|
|
48
|
+
* set (versions, platform, runtime, project coordinates). `orgId` — which
|
|
49
|
+
* Studio's shell sends and the sanity CLI doesn't yet — attaches separately
|
|
50
|
+
* once {@link resolveOrgId} resolves. Joined to the session's events by
|
|
51
|
+
* sessionId downstream; user identity stays sink-side.
|
|
52
|
+
*/
|
|
53
|
+
export declare function cliUserProperties(args: {
|
|
54
|
+
project: {
|
|
55
|
+
projectId: string;
|
|
56
|
+
resource: WorkflowResource;
|
|
57
|
+
};
|
|
58
|
+
cliVersion: string | undefined;
|
|
59
|
+
}): ReturnType<typeof processShellUserProperties> & {
|
|
60
|
+
cliVersion?: string;
|
|
61
|
+
projectId: string;
|
|
62
|
+
dataset: string;
|
|
63
|
+
};
|
|
64
|
+
/** The intake project's organization id — degrade-soft: a failed, slow, or
|
|
65
|
+
* org-less lookup resolves undefined, never rejects, never breaks a command. */
|
|
66
|
+
export declare function resolveOrgId(client: TelemetryIntakeClient, projectId: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { getUserConfig } from '@sanity/cli-core';
|
|
3
|
+
import { datasetResourceParts, processShellUserProperties, } from '@sanity/workflow-engine';
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
import { clientFor, resolveToken } from "./client.js";
|
|
6
|
+
import { loadWorkflowConfigIfPresent } from "./load-config.js";
|
|
7
|
+
import { createBuiltinTelemetry, raceWithDeadline, setCliTelemetry } from "./telemetry.js";
|
|
8
|
+
const DISCLOSURE_KEY = 'workflowCliTelemetryDisclosed';
|
|
9
|
+
const SANITY_CLI_DISCLOSURE_KEY = 'telemetryDisclosed';
|
|
10
|
+
export function maybeShowTelemetryDisclosure(args) {
|
|
11
|
+
const { userConfig, writeStderr } = args;
|
|
12
|
+
if (userConfig.get(DISCLOSURE_KEY) !== undefined ||
|
|
13
|
+
userConfig.get(SANITY_CLI_DISCLOSURE_KEY) !== undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
writeStderr(`${boxen(`The Sanity workflow CLI collects telemetry data on general usage and errors.
|
|
17
|
+
This helps us improve workflows and prioritize features.
|
|
18
|
+
|
|
19
|
+
To opt in/out, run ${styleText('cyan', 'npx sanity telemetry enable/disable')}.
|
|
20
|
+
Learn more: https://www.sanity.io/telemetry`, { borderColor: 'yellow', borderStyle: 'round', margin: 1, padding: 1 })}\n`);
|
|
21
|
+
userConfig.set(DISCLOSURE_KEY, Date.now());
|
|
22
|
+
}
|
|
23
|
+
function intakeProject(config) {
|
|
24
|
+
const resource = config.deployments
|
|
25
|
+
.map((deployment) => deployment.workflowResource)
|
|
26
|
+
.find((candidate) => candidate.type === 'dataset');
|
|
27
|
+
if (resource === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return { projectId: datasetResourceParts(resource.id).projectId, resource };
|
|
31
|
+
}
|
|
32
|
+
export async function setupCliTelemetry(args) {
|
|
33
|
+
try {
|
|
34
|
+
const config = await loadWorkflowConfigIfPresent(args?.cwd);
|
|
35
|
+
if (config === undefined) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (config.telemetry !== undefined) {
|
|
39
|
+
setCliTelemetry({ kind: 'provided', logger: config.telemetry });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const token = await resolveToken().catch(() => undefined);
|
|
43
|
+
const project = token === undefined ? undefined : intakeProject(config);
|
|
44
|
+
if (token === undefined || project === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const client = args?.client ?? clientFor(project.resource, token);
|
|
48
|
+
const telemetry = createBuiltinTelemetry({
|
|
49
|
+
client,
|
|
50
|
+
projectId: project.projectId,
|
|
51
|
+
env: args?.env ?? process.env,
|
|
52
|
+
});
|
|
53
|
+
setCliTelemetry(telemetry);
|
|
54
|
+
if (!telemetry.envDenied) {
|
|
55
|
+
telemetry.store.logger.updateUserProperties(cliUserProperties({ project, cliVersion: args?.cliVersion }));
|
|
56
|
+
void resolveOrgId(client, project.projectId).then((orgId) => {
|
|
57
|
+
if (orgId !== undefined)
|
|
58
|
+
telemetry.store.logger.updateUserProperties({ orgId });
|
|
59
|
+
});
|
|
60
|
+
maybeShowTelemetryDisclosure({
|
|
61
|
+
userConfig: args?.userConfig ?? getUserConfig(),
|
|
62
|
+
writeStderr: args?.writeStderr ?? ((message) => void process.stderr.write(message)),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const ORG_LOOKUP_DEADLINE_MS = 2_000;
|
|
70
|
+
const CONTEXT_TAG = 'telemetry.context';
|
|
71
|
+
export function cliUserProperties(args) {
|
|
72
|
+
const { project, cliVersion } = args;
|
|
73
|
+
return {
|
|
74
|
+
...processShellUserProperties('cli'),
|
|
75
|
+
...(cliVersion !== undefined ? { cliVersion } : {}),
|
|
76
|
+
projectId: project.projectId,
|
|
77
|
+
dataset: datasetResourceParts(project.resource.id).dataset,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function resolveOrgId(client, projectId) {
|
|
81
|
+
try {
|
|
82
|
+
const project = await raceWithDeadline(client.request({
|
|
83
|
+
uri: `/projects/${projectId}`,
|
|
84
|
+
tag: CONTEXT_TAG,
|
|
85
|
+
timeout: ORG_LOOKUP_DEADLINE_MS,
|
|
86
|
+
}), ORG_LOOKUP_DEADLINE_MS);
|
|
87
|
+
return project?.organizationId ?? undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|