@prisma-next/cli 0.12.0-dev.2 → 0.12.0-dev.21
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/dist/cli.mjs +177 -160
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-Cdxcme1x.mjs} +21 -8
- package/dist/client-Cdxcme1x.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-Cmdqyhz9.mjs} +32 -2
- package/dist/{command-helpers-Bbw1GbwL.mjs.map → command-helpers-Cmdqyhz9.mjs.map} +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +4 -4
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.mjs +5 -5
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.mjs +5 -5
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +23 -5
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +2 -2
- package/dist/commands/migration-list.d.mts +3 -3
- package/dist/commands/migration-list.mjs +3 -3
- package/dist/commands/migration-log.d.mts +3 -3
- package/dist/commands/migration-log.mjs +3 -3
- package/dist/commands/migration-new.mjs +3 -3
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +3 -3
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +4 -4
- package/dist/commands/migration-status.mjs.map +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +2 -2
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-Cz0z5PJi.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-Cz0z5PJi.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-CC9jDOmu.mjs} +3 -3
- package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-CC9jDOmu.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-DPMij44i.mjs} +3 -3
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-DPMij44i.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-DaFPNrZH.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-DaFPNrZH.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-CirAEsM8.mjs} +2 -2
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-CirAEsM8.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-BSA1a_W_.mjs} +4 -4
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-BSA1a_W_.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-DynSvww4.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-DynSvww4.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-B6kKrmf7.mjs} +5 -58
- package/dist/init-B6kKrmf7.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-Dn56wDhG.mjs} +3 -3
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-Dn56wDhG.mjs.map} +1 -1
- package/dist/{migration-check-BiBJoYYW.mjs → migration-check-DzH1u-O1.mjs} +2 -2
- package/dist/{migration-check-BiBJoYYW.mjs.map → migration-check-DzH1u-O1.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-V52dV2Tv.mjs} +3 -3
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-V52dV2Tv.mjs.map} +1 -1
- package/dist/{migration-graph-D7DVUElV.mjs → migration-graph-DKl_IYsF.mjs} +377 -85
- package/dist/migration-graph-DKl_IYsF.mjs.map +1 -0
- package/dist/{migration-list-styler-BRwF4-gy.mjs → migration-list-styler-COQbZmXk.mjs} +61 -46
- package/dist/migration-list-styler-COQbZmXk.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CaeKCKp4.mjs} +5 -5
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CaeKCKp4.mjs.map} +1 -1
- package/dist/{migration-types-D2FW63pr.d.mts → migration-types-CAQ-0TEE.d.mts} +1 -1
- package/dist/{migration-types-D2FW63pr.d.mts.map → migration-types-CAQ-0TEE.d.mts.map} +1 -1
- package/dist/{migrations-Cv2jxNNK.mjs → migrations-DQ1t3XFL.mjs} +2 -2
- package/dist/{migrations-Cv2jxNNK.mjs.map → migrations-DQ1t3XFL.mjs.map} +1 -1
- package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
- package/dist/telemetry-Q88WHwlv.mjs +122 -0
- package/dist/telemetry-Q88WHwlv.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-C3xGyxW-.d.mts} +1 -1
- package/dist/{terminal-ui-5Y6mrg93.d.mts.map → terminal-ui-C3xGyxW-.d.mts.map} +1 -1
- package/dist/{types-Dt_SfqFm.d.mts → types-DiC683UW.d.mts} +8 -2
- package/dist/{types-Dt_SfqFm.d.mts.map → types-DiC683UW.d.mts.map} +1 -1
- package/dist/{verify-DCA9Sldu.mjs → verify-CreSJ1Mz.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-CreSJ1Mz.mjs.map} +1 -1
- package/package.json +22 -18
- package/src/cli.ts +5 -0
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/migration-graph.ts +43 -2
- package/src/commands/migration-status.ts +1 -1
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +11 -1
- package/src/control-api/operations/apply.ts +1 -0
- package/src/control-api/operations/migration-apply.ts +10 -3
- package/src/control-api/types.ts +12 -1
- package/src/utils/formatters/migration-graph-lane-colors.ts +31 -0
- package/src/utils/formatters/migration-graph-layout.ts +51 -7
- package/src/utils/formatters/migration-graph-tree-render.ts +414 -51
- package/src/utils/formatters/migration-list-graph-topology.ts +67 -83
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
|
@@ -30,10 +30,30 @@ function bumpDegree(map: Map<string, number>, key: string): void {
|
|
|
30
30
|
map.set(key, (map.get(key) ?? 0) + 1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function compareNodesRootFirst(a: string, b: string): number {
|
|
34
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
35
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
36
|
+
return a.localeCompare(b);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shortest-path distance of each node from the forward roots, over the given
|
|
41
|
+
* candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);
|
|
42
|
+
* a rooted component therefore distances every node by how many forward steps
|
|
43
|
+
* it sits from a root. A component with no root (a pure cycle) is seeded from
|
|
44
|
+
* its single lexically-smallest node so the cycle still gets a stable layering.
|
|
45
|
+
*
|
|
46
|
+
* Crucially this is *shortest* path, not longest: a backward (rollback) edge
|
|
47
|
+
* `deep → shallow` never offers a shorter route to the already-shallower
|
|
48
|
+
* target, so it is inert here. Distances are thus stable whether or not the
|
|
49
|
+
* rollbacks are still in the candidate set — which is what lets the peel below
|
|
50
|
+
* tell a genuine back-edge (target strictly shallower than source) apart from a
|
|
51
|
+
* forward edge that merely happens to share the back-edge's cycle.
|
|
52
|
+
*/
|
|
53
|
+
function forwardDistances(
|
|
34
54
|
nodes: ReadonlySet<string>,
|
|
35
55
|
candidates: readonly NormalizedEdge[],
|
|
36
|
-
):
|
|
56
|
+
): Map<string, number> {
|
|
37
57
|
const inDegree = new Map<string, number>();
|
|
38
58
|
for (const node of nodes) {
|
|
39
59
|
inDegree.set(node, 0);
|
|
@@ -42,44 +62,24 @@ function forwardRootsForDepth(
|
|
|
42
62
|
bumpDegree(inDegree, edge.to);
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
const roots
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
roots.push(node);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
roots.sort((a, b) => {
|
|
52
|
-
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
53
|
-
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
54
|
-
return a.localeCompare(b);
|
|
55
|
-
});
|
|
56
|
-
if (roots.length > 0) return roots;
|
|
65
|
+
const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);
|
|
66
|
+
roots.sort(compareNodesRootFirst);
|
|
67
|
+
const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return a.localeCompare(b);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function longestPathDepths(
|
|
66
|
-
nodes: ReadonlySet<string>,
|
|
67
|
-
candidates: readonly NormalizedEdge[],
|
|
68
|
-
): Map<string, number> {
|
|
69
|
-
const depth = new Map<string, number>();
|
|
70
|
-
for (const root of forwardRootsForDepth(nodes, candidates)) {
|
|
71
|
-
depth.set(root, 0);
|
|
69
|
+
const dist = new Map<string, number>();
|
|
70
|
+
for (const seed of seeds) {
|
|
71
|
+
dist.set(seed, 0);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const maxPasses = nodes.size;
|
|
75
75
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
76
76
|
let changed = false;
|
|
77
77
|
for (const edge of candidates) {
|
|
78
|
-
const base =
|
|
78
|
+
const base = dist.get(edge.from);
|
|
79
79
|
if (base === undefined) continue;
|
|
80
80
|
const next = base + 1;
|
|
81
|
-
if (next
|
|
82
|
-
|
|
81
|
+
if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {
|
|
82
|
+
dist.set(edge.to, next);
|
|
83
83
|
changed = true;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -87,12 +87,12 @@ function longestPathDepths(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
for (const node of nodes) {
|
|
90
|
-
if (!
|
|
91
|
-
|
|
90
|
+
if (!dist.has(node)) {
|
|
91
|
+
dist.set(node, 0);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
return
|
|
95
|
+
return dist;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function canReachForward(
|
|
@@ -126,45 +126,24 @@ function canReachForward(
|
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// recomputing — so cycles are broken deterministically regardless of edge
|
|
148
|
-
// order. `isMarginalForwardEdge` is only a fallback for the residual case and
|
|
149
|
-
// is reached only while the candidate set is still cyclic.
|
|
150
|
-
function shouldPeelForwardEdge(
|
|
151
|
-
nodes: ReadonlySet<string>,
|
|
152
|
-
candidates: readonly NormalizedEdge[],
|
|
153
|
-
edge: NormalizedEdge,
|
|
154
|
-
): boolean {
|
|
155
|
-
const without = candidates.filter((candidate) => candidate !== edge);
|
|
156
|
-
const depthWithout = longestPathDepths(nodes, without);
|
|
157
|
-
const fromDepth = depthWithout.get(edge.from) ?? 0;
|
|
158
|
-
const toWithout = depthWithout.get(edge.to) ?? 0;
|
|
159
|
-
|
|
160
|
-
if (canReachForward(edge.to, edge.from, without) && fromDepth > toWithout + 1) {
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return !isMarginalForwardEdge(nodes, candidates, edge);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function peelNonMarginalForwardEdges(
|
|
129
|
+
/**
|
|
130
|
+
* Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`
|
|
131
|
+
* is a rollback exactly when both hold:
|
|
132
|
+
* 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over
|
|
133
|
+
* the other forward edges, so the edge closes a cycle; and
|
|
134
|
+
* 2. `to` is strictly shallower than `from` (smaller forward distance) — the
|
|
135
|
+
* edge points back toward the root rather than advancing history.
|
|
136
|
+
*
|
|
137
|
+
* Condition 2 is the discriminator: in a cycle created by a rollback every edge
|
|
138
|
+
* satisfies condition 1, but only the rollback itself runs deep → shallow. The
|
|
139
|
+
* forward chain edges run shallow → deep and are never peeled, however many
|
|
140
|
+
* rollbacks converge on the same target. Tight back-edges whose source and
|
|
141
|
+
* target sit at the same distance (mutual two-node cycles) are already resolved
|
|
142
|
+
* by the DFS immediate-parent rule, so they never reach this pass. One edge is
|
|
143
|
+
* peeled per iteration (dirName-descending tie-break) and distances/reachability
|
|
144
|
+
* are recomputed, making the outcome independent of edge input order.
|
|
145
|
+
*/
|
|
146
|
+
function peelNodeSkippingRollbacks(
|
|
168
147
|
nodes: ReadonlySet<string>,
|
|
169
148
|
kindByMigrationHash: Map<string, MigrationEdgeKind>,
|
|
170
149
|
nonSelf: readonly NormalizedEdge[],
|
|
@@ -172,13 +151,18 @@ function peelNonMarginalForwardEdges(
|
|
|
172
151
|
let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === 'forward');
|
|
173
152
|
|
|
174
153
|
while (candidates.length > 0) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
154
|
+
const dist = forwardDistances(nodes, candidates);
|
|
155
|
+
const backEdges = candidates.filter((edge) => {
|
|
156
|
+
const toDist = dist.get(edge.to) ?? 0;
|
|
157
|
+
const fromDist = dist.get(edge.from) ?? 0;
|
|
158
|
+
if (toDist >= fromDist) return false;
|
|
159
|
+
const without = candidates.filter((candidate) => candidate !== edge);
|
|
160
|
+
return canReachForward(edge.to, edge.from, without);
|
|
161
|
+
});
|
|
162
|
+
if (backEdges.length === 0) break;
|
|
163
|
+
|
|
164
|
+
backEdges.sort(compareDirNameDesc);
|
|
165
|
+
const rollback = backEdges[0];
|
|
182
166
|
if (rollback === undefined) break;
|
|
183
167
|
|
|
184
168
|
kindByMigrationHash.set(rollback.hash, 'rollback');
|
|
@@ -189,8 +173,8 @@ function peelNonMarginalForwardEdges(
|
|
|
189
173
|
/**
|
|
190
174
|
* DFS with dirName-descending traversal. A GRAY target is a rollback only when it
|
|
191
175
|
* is the immediate DFS parent of the source — cross-links to other GRAY nodes
|
|
192
|
-
* stay forward. A follow-up peel pass
|
|
193
|
-
*
|
|
176
|
+
* stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is
|
|
177
|
+
* a forward-ancestor of the source and sits strictly shallower than it).
|
|
194
178
|
*/
|
|
195
179
|
function classifyNormalizedEdges(edges: readonly NormalizedEdge[]): MigrationListGraphTopology {
|
|
196
180
|
const nodes = new Set<string>();
|
|
@@ -307,7 +291,7 @@ function classifyNormalizedEdges(edges: readonly NormalizedEdge[]): MigrationLis
|
|
|
307
291
|
runDfsFrom(root);
|
|
308
292
|
}
|
|
309
293
|
|
|
310
|
-
|
|
294
|
+
peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);
|
|
311
295
|
|
|
312
296
|
const forwardInDegree = new Map<string, number>();
|
|
313
297
|
const forwardOutDegree = new Map<string, number>();
|
|
@@ -180,3 +180,38 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
|
|
|
180
180
|
|
|
181
181
|
return flags as GlobalFlags;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Bridges the two TTY checks (stdout via `flags`, stdin via
|
|
186
|
+
* `process.stdin.isTTY`) into the `canPrompt` boolean the interactive
|
|
187
|
+
* `init` flow consumes.
|
|
188
|
+
*
|
|
189
|
+
* Per the [Style Guide § Interactivity](../../../../../../../docs/CLI%20Style%20Guide.md#interactivity):
|
|
190
|
+
*
|
|
191
|
+
* - `flags.interactive` governs *decoration* (TerminalUI, intro/outro,
|
|
192
|
+
* spinners) and is derived from stdout-TTY by `parseGlobalFlags`,
|
|
193
|
+
* honouring `--interactive` / `--no-interactive`.
|
|
194
|
+
* - Prompting additionally requires a stdin TTY — closing stdin is a
|
|
195
|
+
* common signal in CI / agent environments even when stdout stays
|
|
196
|
+
* attached.
|
|
197
|
+
* - `--interactive` is the explicit override: when the user passes it,
|
|
198
|
+
* we honour it (e.g. testing flows where stdin is stubbed).
|
|
199
|
+
*
|
|
200
|
+
* Single source of truth for the interactive-prompt decision: both the
|
|
201
|
+
* `init` action handler and the preAction telemetry bridge derive
|
|
202
|
+
* prompt-eligibility from this helper so they cannot drift. Lives in
|
|
203
|
+
* `global-flags` (alongside `parseGlobalFlags`) to keep
|
|
204
|
+
* `utils/telemetry` and `commands/init/index` free of an import cycle.
|
|
205
|
+
*
|
|
206
|
+
* Exported so callers and tests can derive the same value without
|
|
207
|
+
* touching `process` globals.
|
|
208
|
+
*/
|
|
209
|
+
export function deriveCanPrompt(opts: {
|
|
210
|
+
readonly flagsInteractive: boolean | undefined;
|
|
211
|
+
readonly optionInteractive: boolean | undefined;
|
|
212
|
+
readonly stdinIsTTY: boolean;
|
|
213
|
+
}): boolean {
|
|
214
|
+
if (opts.optionInteractive === true) return true;
|
|
215
|
+
if (opts.flagsInteractive === false) return false;
|
|
216
|
+
return opts.stdinIsTTY;
|
|
217
|
+
}
|
package/src/utils/telemetry.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
2
2
|
import {
|
|
3
3
|
type CommanderOptionShape,
|
|
4
4
|
type CommanderResultShape,
|
|
5
|
+
ensureInstallationId,
|
|
5
6
|
readUserConfig,
|
|
6
7
|
resolveGating,
|
|
7
8
|
runTelemetry,
|
|
8
9
|
type TelemetryRunOutcome,
|
|
9
10
|
type UserConfig,
|
|
11
|
+
userConfigPath,
|
|
10
12
|
} from '@prisma-next/cli-telemetry';
|
|
11
|
-
import { ifDefined } from '@prisma-next/utils/defined';
|
|
12
13
|
import type { Command } from 'commander';
|
|
13
14
|
import { version as CLI_VERSION } from '../../package.json' with { type: 'json' };
|
|
14
15
|
import { isCI } from './is-ci';
|
|
@@ -78,11 +79,7 @@ function senderPath(): string {
|
|
|
78
79
|
return fileURLToPath(new URL(import.meta.resolve('@prisma-next/cli-telemetry/sender')));
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function fireTelemetry(
|
|
82
|
-
actionCommand: Command,
|
|
83
|
-
userConfig: UserConfig,
|
|
84
|
-
overrides: { readonly databaseTarget?: string } = {},
|
|
85
|
-
): TelemetryRunOutcome {
|
|
82
|
+
function fireTelemetry(actionCommand: Command, userConfig: UserConfig): TelemetryRunOutcome {
|
|
86
83
|
return runTelemetry({
|
|
87
84
|
command: commanderSnapshotForTelemetry(actionCommand),
|
|
88
85
|
version: CLI_VERSION,
|
|
@@ -91,7 +88,6 @@ function fireTelemetry(
|
|
|
91
88
|
isCI: isCI(),
|
|
92
89
|
env: process.env,
|
|
93
90
|
userConfig,
|
|
94
|
-
...ifDefined('databaseTarget', overrides.databaseTarget),
|
|
95
91
|
});
|
|
96
92
|
}
|
|
97
93
|
|
|
@@ -107,35 +103,75 @@ function fireTelemetry(
|
|
|
107
103
|
* config touches disk. The child loading user TS code is acceptable
|
|
108
104
|
* only because it's gated behind the same resolved-enabled signal.
|
|
109
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Builds the one-time first-run disclosure. The resolved absolute path to
|
|
108
|
+
* the user-level config file is substituted in so the user can see exactly
|
|
109
|
+
* which file to edit (it must not be confused with `prisma-next.config.ts`).
|
|
110
|
+
* `prisma-next telemetry disable` is named as the primary, friendliest
|
|
111
|
+
* opt-out, alongside the env vars and the config edit.
|
|
112
|
+
*/
|
|
113
|
+
function firstRunNotice(configPath: string): string {
|
|
114
|
+
return [
|
|
115
|
+
'Prisma Next collects anonymous CLI usage data, enabled by default.',
|
|
116
|
+
"What's collected and why: https://prisma-next.dev/docs/cli/telemetry.",
|
|
117
|
+
'Opt out: run "prisma-next telemetry disable", set DO_NOT_TRACK=1 or',
|
|
118
|
+
`PRISMA_NEXT_DISABLE_TELEMETRY=1, or set "enableTelemetry": false in ${configPath}.`,
|
|
119
|
+
].join(' ');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Best-effort first-run disclosure + installationId mint. Runs only on the
|
|
124
|
+
* gating-enabled path. Prints the notice to stderr (never stdout) and mints
|
|
125
|
+
* a persistent id without touching `enableTelemetry`, so the opt-out default
|
|
126
|
+
* stays intact and no unasked-for consent is recorded.
|
|
127
|
+
*
|
|
128
|
+
* Every step is wrapped so an un-writable config dir (or any other failure)
|
|
129
|
+
* never throws and never blocks the command. Returns the minted (or
|
|
130
|
+
* pre-existing) id so the caller can forward it to `runTelemetry` without a
|
|
131
|
+
* redundant disk read. On mint failure it returns `undefined`: the notice may
|
|
132
|
+
* reprint next run, and `runTelemetry` no-ops on the missing id.
|
|
133
|
+
*/
|
|
134
|
+
function discloseAndMintOnFirstRun(): string | undefined {
|
|
135
|
+
try {
|
|
136
|
+
process.stderr.write(`${firstRunNotice(userConfigPath())}\n`);
|
|
137
|
+
} catch {}
|
|
138
|
+
try {
|
|
139
|
+
return ensureInstallationId();
|
|
140
|
+
} catch {}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* True when the run is the `telemetry` command (or one of its
|
|
146
|
+
* subcommands). The usage-telemetry preAction fire is exempted for it:
|
|
147
|
+
* it would be absurd for `telemetry disable` to send a usage event before
|
|
148
|
+
* disabling, or for `telemetry status` to mint an id + send while merely
|
|
149
|
+
* reporting state. This is the only command-specific exemption.
|
|
150
|
+
*
|
|
151
|
+
* The check is rooted at the program: the path must be
|
|
152
|
+
* `['prisma-next', 'telemetry', …]`, so it matches the top-level
|
|
153
|
+
* `telemetry` command and its subcommands without matching a hypothetical
|
|
154
|
+
* nested `… telemetry` elsewhere.
|
|
155
|
+
*/
|
|
156
|
+
function isTelemetryCommand(actionCommand: Command): boolean {
|
|
157
|
+
return commandPathFor(actionCommand)[1] === 'telemetry';
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
export function fireTelemetryFromPreAction(actionCommand: Command): TelemetryRunOutcome {
|
|
161
|
+
if (isTelemetryCommand(actionCommand)) {
|
|
162
|
+
return { spawned: false, reason: 'gated-off' };
|
|
163
|
+
}
|
|
111
164
|
const gate = resolveTelemetryGate();
|
|
112
165
|
if (!gate.enabled) {
|
|
113
166
|
return gate.outcome;
|
|
114
167
|
}
|
|
168
|
+
const storedId = gate.userConfig.installationId;
|
|
169
|
+
if (typeof storedId !== 'string' || storedId.length === 0) {
|
|
170
|
+
const installationId = discloseAndMintOnFirstRun();
|
|
171
|
+
return fireTelemetry(
|
|
172
|
+
actionCommand,
|
|
173
|
+
installationId === undefined ? gate.userConfig : { ...gate.userConfig, installationId },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
115
176
|
return fireTelemetry(actionCommand, gate.userConfig);
|
|
116
177
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Manual one-shot telemetry path for the first `init` run where the user
|
|
120
|
-
* explicitly answers Yes to the consent prompt. The preAction hook for
|
|
121
|
-
* that same run has already resolved before consent existed, so it is
|
|
122
|
-
* default-off. After consent is persisted, `runInit` calls this helper
|
|
123
|
-
* exactly for that first affirmative answer; subsequent init runs skip
|
|
124
|
-
* it because the prompt is not shown again.
|
|
125
|
-
*
|
|
126
|
-
* The child's c12 load would return `databaseTarget: null` for this
|
|
127
|
-
* specific invocation because `prisma-next.config.*` is not yet on
|
|
128
|
-
* disk (init writes it later in the same run). To preserve the
|
|
129
|
-
* prompt-chosen target in the first-init telemetry event, this
|
|
130
|
-
* helper forwards the value as a parent-side IPC override on
|
|
131
|
-
* `ParentToSenderPayload.databaseTarget` — the child consults the
|
|
132
|
-
* override first and falls back to its c12 result when absent.
|
|
133
|
-
*/
|
|
134
|
-
export function fireTelemetryAfterInitConsent(
|
|
135
|
-
actionCommand: Command,
|
|
136
|
-
inputs: { readonly databaseTarget: string },
|
|
137
|
-
): TelemetryRunOutcome {
|
|
138
|
-
return fireTelemetry(actionCommand, readUserConfig(), {
|
|
139
|
-
databaseTarget: inputs.databaseTarget,
|
|
140
|
-
});
|
|
141
|
-
}
|