@prisma-next/cli 0.12.0-dev.12 → 0.12.0-dev.14
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-CDr4o07S.mjs → client-Cdxcme1x.mjs} +4 -4
- package/dist/{client-CDr4o07S.mjs.map → client-Cdxcme1x.mjs.map} +1 -1
- 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 +4 -4
- package/dist/commands/migration-graph.mjs +1 -1
- package/dist/commands/migration-list.d.mts +3 -3
- package/dist/commands/migration-list.mjs +2 -2
- 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 +3 -3
- 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-C8J1WMvO.mjs → contract-infer-DaFPNrZH.mjs} +3 -3
- package/dist/{contract-infer-C8J1WMvO.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-BeRHwN8M.mjs → db-verify-BSA1a_W_.mjs} +4 -4
- package/dist/{db-verify-BeRHwN8M.mjs.map → db-verify-BSA1a_W_.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +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-BlKR2Zln.mjs → inspect-live-schema-Dn56wDhG.mjs} +3 -3
- package/dist/{inspect-live-schema-BlKR2Zln.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-BAGUiGOK.mjs → migration-command-scaffold-V52dV2Tv.mjs} +3 -3
- package/dist/{migration-command-scaffold-BAGUiGOK.mjs.map → migration-command-scaffold-V52dV2Tv.mjs.map} +1 -1
- package/dist/{migration-graph-C9WC-7eO.mjs → migration-graph-Cm3oee8C.mjs} +3 -3
- package/dist/{migration-graph-C9WC-7eO.mjs.map → migration-graph-Cm3oee8C.mjs.map} +1 -1
- 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-CeC5ec2Y.d.mts → types-DiC683UW.d.mts} +1 -1
- package/dist/{types-CeC5ec2Y.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/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import * as clack from '@clack/prompts';
|
|
3
|
-
import { readUserConfig, resolveGating, writeUserConfig } from '@prisma-next/cli-telemetry';
|
|
4
3
|
import { extname, join, normalize } from 'pathe';
|
|
5
4
|
import type { GlobalFlags } from '../../utils/global-flags';
|
|
6
|
-
import { isCI } from '../../utils/is-ci';
|
|
7
5
|
import {
|
|
8
6
|
errorInitAuthoringSchemaPathMismatch,
|
|
9
7
|
errorInitInvalidFlagValue,
|
|
@@ -71,18 +69,6 @@ export interface ResolvedInitInputs {
|
|
|
71
69
|
* is added separately via the install step.
|
|
72
70
|
*/
|
|
73
71
|
readonly removePreviousFacade: string | null;
|
|
74
|
-
/**
|
|
75
|
-
* Telemetry consent answer recorded during this `init` run, or `null`
|
|
76
|
-
* when no prompt was shown. The prompt fires only on the
|
|
77
|
-
* canPrompt + !autoAcceptPrompts + no env/CI opt-out +
|
|
78
|
-
* `enableTelemetry === undefined` intersection; outside that window
|
|
79
|
-
* the field is `null` and the stored preference (if any) stays
|
|
80
|
-
* unchanged. The answer has already been persisted to
|
|
81
|
-
* `$XDG_CONFIG_HOME/prisma-next/config.json` by
|
|
82
|
-
* the time `runInit` sees this value — it's surfaced here purely so
|
|
83
|
-
* the post-init summary can mention what the user chose.
|
|
84
|
-
*/
|
|
85
|
-
readonly enableTelemetry: boolean | null;
|
|
86
72
|
/**
|
|
87
73
|
* Whether to run `npx skills add prisma/prisma-next#v<version>` at the
|
|
88
74
|
* project level after install + emit. True by default; `--no-skill`
|
|
@@ -106,12 +92,6 @@ const AUTHORING_VALUES: ReadonlyMap<string, AuthoringId> = new Map([
|
|
|
106
92
|
['ts', 'typescript'],
|
|
107
93
|
]);
|
|
108
94
|
|
|
109
|
-
export const TELEMETRY_CONSENT_MESSAGE = [
|
|
110
|
-
'Help us prioritize features by sharing anonymous CLI usage data?',
|
|
111
|
-
'The telemetry implementation is open source and fully transparent.',
|
|
112
|
-
'(packages/1-framework/3-tooling/cli-telemetry and apps/telemetry-backend).',
|
|
113
|
-
].join(' ');
|
|
114
|
-
|
|
115
95
|
/**
|
|
116
96
|
* Resolves every required input for `runInit`. In interactive mode, missing
|
|
117
97
|
* inputs are prompted via clack; in non-interactive mode, missing required
|
|
@@ -197,11 +177,6 @@ export async function resolveInitInputs(ctx: {
|
|
|
197
177
|
autoAcceptPrompts,
|
|
198
178
|
});
|
|
199
179
|
|
|
200
|
-
const enableTelemetry = await resolveTelemetryConsent({
|
|
201
|
-
canPrompt,
|
|
202
|
-
autoAcceptPrompts,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
180
|
// Skill-install gating. `--no-skill` (commander parses
|
|
206
181
|
// `options.skill === false`) is the only escape hatch; otherwise
|
|
207
182
|
// project-level install is unconditional. The skill is always
|
|
@@ -219,60 +194,10 @@ export async function resolveInitInputs(ctx: {
|
|
|
219
194
|
strictProbe: Boolean(options.strictProbe),
|
|
220
195
|
reinit,
|
|
221
196
|
removePreviousFacade,
|
|
222
|
-
enableTelemetry,
|
|
223
197
|
installProjectSkill,
|
|
224
198
|
};
|
|
225
199
|
}
|
|
226
200
|
|
|
227
|
-
/**
|
|
228
|
-
* The interactive telemetry consent prompt. Shown as the last
|
|
229
|
-
* question of the `init` sequence iff:
|
|
230
|
-
* 1. `canPrompt === true` (interactive stdin / stdout combo),
|
|
231
|
-
* 2. `autoAcceptPrompts === false` (the user did not pass `--yes`),
|
|
232
|
-
* 3. neither telemetry env opt-out is active,
|
|
233
|
-
* 4. the process is not running in CI,
|
|
234
|
-
* 5. the stored `enableTelemetry` value is `undefined` (the user
|
|
235
|
-
* has never been asked, or skipped the prompt previously).
|
|
236
|
-
*
|
|
237
|
-
* Outside that intersection the function returns `null` and leaves the
|
|
238
|
-
* stored preference untouched. Inside it, the user's answer is
|
|
239
|
-
* persisted via `writeUserConfig({ enableTelemetry })` before this
|
|
240
|
-
* function returns; on an affirmative answer `writeUserConfig`
|
|
241
|
-
* generates and stores the v4 `installationId` in the same write.
|
|
242
|
-
*
|
|
243
|
-
* The wording names CLI usage data and points to the open-source
|
|
244
|
-
* client/backend paths. Default value is `true` to match precedent
|
|
245
|
-
* and to make the consent answer a single keystroke once it's been
|
|
246
|
-
* disclosed.
|
|
247
|
-
*/
|
|
248
|
-
async function resolveTelemetryConsent(opts: {
|
|
249
|
-
readonly canPrompt: boolean;
|
|
250
|
-
readonly autoAcceptPrompts: boolean;
|
|
251
|
-
}): Promise<boolean | null> {
|
|
252
|
-
if (!opts.canPrompt || opts.autoAcceptPrompts || isCI()) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
const config = readUserConfig();
|
|
256
|
-
const gating = resolveGating({ env: process.env, config });
|
|
257
|
-
if (!gating.enabled && gating.reason === 'env-override') {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
const stored = config.enableTelemetry;
|
|
261
|
-
if (stored !== undefined) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
const result = await clack.confirm({
|
|
265
|
-
message: TELEMETRY_CONSENT_MESSAGE,
|
|
266
|
-
initialValue: true,
|
|
267
|
-
output: process.stderr,
|
|
268
|
-
});
|
|
269
|
-
if (clack.isCancel(result)) {
|
|
270
|
-
throw errorInitUserAborted();
|
|
271
|
-
}
|
|
272
|
-
writeUserConfig({ enableTelemetry: Boolean(result) });
|
|
273
|
-
return Boolean(result);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
201
|
async function resolveWriteEnv(opts: {
|
|
277
202
|
readonly flag: boolean | undefined;
|
|
278
203
|
readonly canPrompt: boolean;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { userConfigPath, writeUserConfig } from '@prisma-next/cli-telemetry';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import {
|
|
4
|
+
addGlobalOptions,
|
|
5
|
+
setCommandDescriptions,
|
|
6
|
+
setCommandExamples,
|
|
7
|
+
} from '../../utils/command-helpers';
|
|
8
|
+
import { formatCommandHelp } from '../../utils/formatters/help';
|
|
9
|
+
import {
|
|
10
|
+
type CommonCommandOptions,
|
|
11
|
+
parseGlobalFlags,
|
|
12
|
+
parseGlobalFlagsOrExit,
|
|
13
|
+
} from '../../utils/global-flags';
|
|
14
|
+
import { isCI } from '../../utils/is-ci';
|
|
15
|
+
import { createTerminalUI } from '../../utils/terminal-ui';
|
|
16
|
+
import { formatTelemetryStatusLines, resolveTelemetryStatus } from './status';
|
|
17
|
+
|
|
18
|
+
function createTelemetryStatusCommand(): Command {
|
|
19
|
+
const command = new Command('status');
|
|
20
|
+
setCommandDescriptions(
|
|
21
|
+
command,
|
|
22
|
+
'Show whether anonymous CLI telemetry is enabled and why',
|
|
23
|
+
'Reports whether telemetry is currently enabled or disabled and the reason\n' +
|
|
24
|
+
'(default-on, stored opt-out, environment opt-out, or CI), the path to your\n' +
|
|
25
|
+
'user-level config file, and whether an installation ID has been stored.\n' +
|
|
26
|
+
'Read-only: never sends an event, never mints an ID, never writes anything.',
|
|
27
|
+
);
|
|
28
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
29
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
30
|
+
const ui = createTerminalUI(flags);
|
|
31
|
+
const status = resolveTelemetryStatus({ env: process.env, inCI: isCI() });
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
ui.output(JSON.stringify(status));
|
|
34
|
+
} else {
|
|
35
|
+
for (const line of formatTelemetryStatusLines(status)) {
|
|
36
|
+
ui.output(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createTelemetryEnableCommand(): Command {
|
|
44
|
+
const command = new Command('enable');
|
|
45
|
+
setCommandDescriptions(
|
|
46
|
+
command,
|
|
47
|
+
'Enable anonymous CLI telemetry',
|
|
48
|
+
'Stores "enableTelemetry": true in your user-level config and mints an\n' +
|
|
49
|
+
'installation ID if one is not already stored.',
|
|
50
|
+
);
|
|
51
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
52
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
53
|
+
writeUserConfig({ enableTelemetry: true });
|
|
54
|
+
const ui = createTerminalUI(flags);
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
ui.output(JSON.stringify({ enableTelemetry: true, configPath: userConfigPath() }));
|
|
57
|
+
} else {
|
|
58
|
+
ui.output(`Telemetry enabled. Preference stored in ${userConfigPath()}.`);
|
|
59
|
+
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createTelemetryDisableCommand(): Command {
|
|
65
|
+
const command = new Command('disable');
|
|
66
|
+
setCommandDescriptions(
|
|
67
|
+
command,
|
|
68
|
+
'Disable anonymous CLI telemetry',
|
|
69
|
+
'Stores "enableTelemetry": false in your user-level config. No installation\n' +
|
|
70
|
+
'ID is minted and no event is sent.',
|
|
71
|
+
);
|
|
72
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
73
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
74
|
+
writeUserConfig({ enableTelemetry: false });
|
|
75
|
+
const ui = createTerminalUI(flags);
|
|
76
|
+
if (flags.json) {
|
|
77
|
+
ui.output(JSON.stringify({ enableTelemetry: false, configPath: userConfigPath() }));
|
|
78
|
+
} else {
|
|
79
|
+
ui.output(`Telemetry disabled. Preference stored in ${userConfigPath()}.`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createTelemetryCommand(): Command {
|
|
86
|
+
const command = new Command('telemetry');
|
|
87
|
+
setCommandDescriptions(
|
|
88
|
+
command,
|
|
89
|
+
'Inspect and change anonymous CLI telemetry',
|
|
90
|
+
'Show telemetry status, or enable / disable anonymous CLI usage data.\n' +
|
|
91
|
+
'Telemetry is on by default (opt-out); see https://prisma-next.dev/docs/cli/telemetry\n' +
|
|
92
|
+
'for what is collected and why.',
|
|
93
|
+
);
|
|
94
|
+
setCommandExamples(command, [
|
|
95
|
+
'prisma-next telemetry status',
|
|
96
|
+
'prisma-next telemetry disable',
|
|
97
|
+
'prisma-next telemetry enable',
|
|
98
|
+
]);
|
|
99
|
+
command.configureHelp({
|
|
100
|
+
formatHelp: (cmd) => formatCommandHelp({ command: cmd, flags: parseGlobalFlags({}) }),
|
|
101
|
+
subcommandDescription: () => '',
|
|
102
|
+
});
|
|
103
|
+
command.addCommand(createTelemetryStatusCommand());
|
|
104
|
+
command.addCommand(createTelemetryEnableCommand());
|
|
105
|
+
command.addCommand(createTelemetryDisableCommand());
|
|
106
|
+
return command;
|
|
107
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readUserConfig, resolveGating, userConfigPath } from '@prisma-next/cli-telemetry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Why telemetry resolves the way it does, in the order the CLI's
|
|
5
|
+
* `resolveTelemetryGate` evaluates: CI hard-disables first, then the env
|
|
6
|
+
* opt-outs, then the stored `enableTelemetry`, then the opt-out default.
|
|
7
|
+
*/
|
|
8
|
+
export type TelemetryStatusReason =
|
|
9
|
+
| 'ci'
|
|
10
|
+
| 'env-opt-out'
|
|
11
|
+
| 'stored-opt-out'
|
|
12
|
+
| 'stored-opt-in'
|
|
13
|
+
| 'default-on';
|
|
14
|
+
|
|
15
|
+
export interface TelemetryStatus {
|
|
16
|
+
readonly enabled: boolean;
|
|
17
|
+
readonly reason: TelemetryStatusReason;
|
|
18
|
+
readonly configPath: string;
|
|
19
|
+
readonly installationIdStored: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the same gate the runtime uses (CI check + `resolveGating`) and
|
|
24
|
+
* projects it into a user-facing status. Pure read: never mints, never
|
|
25
|
+
* writes. The `installationId` value itself is never surfaced — only its
|
|
26
|
+
* presence — so `status` discloses nothing identifying.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTelemetryStatus(inputs: {
|
|
29
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
30
|
+
readonly inCI: boolean;
|
|
31
|
+
}): TelemetryStatus {
|
|
32
|
+
const config = readUserConfig();
|
|
33
|
+
const configPath = userConfigPath();
|
|
34
|
+
const installationIdStored =
|
|
35
|
+
typeof config.installationId === 'string' && config.installationId.length > 0;
|
|
36
|
+
|
|
37
|
+
if (inputs.inCI) {
|
|
38
|
+
return { enabled: false, reason: 'ci', configPath, installationIdStored };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gating = resolveGating({ env: inputs.env, config });
|
|
42
|
+
if (!gating.enabled) {
|
|
43
|
+
const reason: TelemetryStatusReason =
|
|
44
|
+
gating.reason === 'env-override' ? 'env-opt-out' : 'stored-opt-out';
|
|
45
|
+
return { enabled: false, reason, configPath, installationIdStored };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const reason: TelemetryStatusReason =
|
|
49
|
+
config.enableTelemetry === true ? 'stored-opt-in' : 'default-on';
|
|
50
|
+
return { enabled: true, reason, configPath, installationIdStored };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REASON_EXPLANATION: Record<TelemetryStatusReason, string> = {
|
|
54
|
+
ci: 'CI environment detected — telemetry is hard-disabled.',
|
|
55
|
+
'env-opt-out': 'an environment opt-out is set (DO_NOT_TRACK / PRISMA_NEXT_DISABLE_TELEMETRY).',
|
|
56
|
+
'stored-opt-out': '"enableTelemetry": false is stored in your config.',
|
|
57
|
+
'stored-opt-in': '"enableTelemetry": true is stored in your config.',
|
|
58
|
+
'default-on': 'no explicit choice is stored, so the opt-out default applies.',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function formatTelemetryStatusLines(status: TelemetryStatus): string[] {
|
|
62
|
+
return [
|
|
63
|
+
`Telemetry is ${status.enabled ? 'enabled' : 'disabled'}: ${REASON_EXPLANATION[status.reason]}`,
|
|
64
|
+
`Config file: ${status.configPath}`,
|
|
65
|
+
`Installation ID: ${status.installationIdStored ? 'stored' : 'not stored'}`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
@@ -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
|
-
}
|