bestax-migrate 2.0.1 → 2.1.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/README.md +13 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +104 -1
- package/dist/index.js +1 -1
- package/dist/telemetry-core.d.ts +45 -0
- package/dist/telemetry-core.d.ts.map +1 -0
- package/dist/telemetry-core.js +141 -0
- package/dist/telemetry.d.ts +53 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +45 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -84,6 +84,19 @@ A codemod rewrites your source in place, so how it is built and published matter
|
|
|
84
84
|
|
|
85
85
|
Full detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)
|
|
86
86
|
|
|
87
|
+
## Telemetry
|
|
88
|
+
|
|
89
|
+
`bestax-migrate` can send one **anonymous** usage event after a successful
|
|
90
|
+
run — only if you opt in when asked (once, on an interactive terminal). The
|
|
91
|
+
event is just the run's shape — source library, CSS mode, the dry/deps flags, a
|
|
92
|
+
capped changed-file count, and per-rule TODO counts — plus the CLI version,
|
|
93
|
+
Node major version, and OS platform name. Never file paths, file contents,
|
|
94
|
+
code, names, IPs, or machine IDs, and no identifier exists that could link two
|
|
95
|
+
events together. Opt out any time with `--no-telemetry`,
|
|
96
|
+
`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.
|
|
97
|
+
|
|
98
|
+
Full disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)
|
|
99
|
+
|
|
87
100
|
## License
|
|
88
101
|
|
|
89
102
|
MIT © Alex Smith
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* bestax-migrate <source> <paths...> [--dry] [--print] [--extensions ...]
|
|
5
5
|
* [--css bestax|bulma|keep] [--no-deps]
|
|
6
|
+
* [--telemetry|--no-telemetry]
|
|
6
7
|
*
|
|
7
8
|
* Walks the given files/directories, routes each file by type (JS/TSX →
|
|
8
9
|
* jscodeshift transform, .scss/.sass → the source's stylesheet transform),
|
|
@@ -10,7 +11,10 @@
|
|
|
10
11
|
* (unless --dry), and prints the run report. TODO annotations are expected
|
|
11
12
|
* output — the exit code is 0 whenever the run itself succeeds.
|
|
12
13
|
*/
|
|
14
|
+
import { createInterface } from 'node:readline/promises';
|
|
15
|
+
import type { Readable, Writable } from 'node:stream';
|
|
13
16
|
import { Command } from 'commander';
|
|
17
|
+
import { type MigrateRunStats } from './telemetry.js';
|
|
14
18
|
export declare function collectFiles(targets: string[], extensions: string[]): string[];
|
|
15
19
|
/** Nearest package.json for each target, walking up to the filesystem root. */
|
|
16
20
|
export declare function findPackageJsons(targets: string[]): string[];
|
|
@@ -18,5 +22,20 @@ export interface CliIo {
|
|
|
18
22
|
log: (message: string) => void;
|
|
19
23
|
error: (message: string) => void;
|
|
20
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* One-question consent prompt (readline — this package carries no interactive
|
|
27
|
+
* prompt dependency). Returns null when the question is cancelled (Ctrl-C /
|
|
28
|
+
* Ctrl-D): a cancel is not an answer and nothing is persisted. The streams are
|
|
29
|
+
* injectable so tests can drive the question without a TTY.
|
|
30
|
+
*/
|
|
31
|
+
export declare function promptTelemetryConsent(io: CliIo, input?: Readable, output?: Writable, makeInterface?: typeof createInterface): Promise<boolean | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Consent + beacon after a successful run. Only success events are reported,
|
|
34
|
+
* the beacon is awaited (so no error path can kill an in-flight request), and
|
|
35
|
+
* nothing in here may throw or alter the exit code. The prompt is skipped
|
|
36
|
+
* without a TTY on both ends, under an explicit flag, or with DO_NOT_TRACK
|
|
37
|
+
* set (the non-interactive path must never hang).
|
|
38
|
+
*/
|
|
39
|
+
export declare function handleTelemetry(stats: MigrateRunStats, flag: boolean | undefined, io: CliIo, promptConsent?: (io: CliIo) => Promise<boolean | null>): Promise<void>;
|
|
21
40
|
export declare function createCLI(io?: CliIo): Command;
|
|
22
41
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAqBxE,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,GACnB,MAAM,EAAE,CAkBV;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAkB5D;AAED,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AA2ID;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,KAAK,EACT,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,EAGjC,aAAa,GAAE,OAAO,eAAiC,GACtD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAyBzB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,OAAO,GAAG,SAAS,EACzB,EAAE,EAAE,KAAK,EACT,aAAa,GAAE,CAAC,EAAE,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAA0B,GAC7E,OAAO,CAAC,IAAI,CAAC,CAqBf;AAYD,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAkD,GACrD,OAAO,CAkHT"}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* bestax-migrate <source> <paths...> [--dry] [--print] [--extensions ...]
|
|
5
5
|
* [--css bestax|bulma|keep] [--no-deps]
|
|
6
|
+
* [--telemetry|--no-telemetry]
|
|
6
7
|
*
|
|
7
8
|
* Walks the given files/directories, routes each file by type (JS/TSX →
|
|
8
9
|
* jscodeshift transform, .scss/.sass → the source's stylesheet transform),
|
|
@@ -12,11 +13,13 @@
|
|
|
12
13
|
*/
|
|
13
14
|
import fs from 'node:fs';
|
|
14
15
|
import path from 'node:path';
|
|
16
|
+
import { createInterface } from 'node:readline/promises';
|
|
15
17
|
import chalk from 'chalk';
|
|
16
18
|
import { Command } from 'commander';
|
|
17
19
|
import { Reporter } from './report.js';
|
|
18
20
|
import { runTransform } from './runner.js';
|
|
19
21
|
import { getSource, sourceNames } from './sources/registry.js';
|
|
22
|
+
import { reportMigrateRun } from './telemetry.js';
|
|
20
23
|
const SKIP_DIRS = new Set([
|
|
21
24
|
'node_modules',
|
|
22
25
|
'.git',
|
|
@@ -172,6 +175,91 @@ function migrateDependencies(source, targets, reporter, io, options, bulmaRefere
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
}
|
|
178
|
+
const TELEMETRY_NOTICE = 'Help improve bestax-migrate — share anonymous usage stats?\n' +
|
|
179
|
+
"Sends only the run's shape (source library, CSS mode, dry/deps flags,\n" +
|
|
180
|
+
'changed-file count, rule names and counts) plus CLI version, Node major,\n' +
|
|
181
|
+
'and OS name. Never file paths, file contents, or code.\n' +
|
|
182
|
+
' Details & opt-out: https://bestax.io/docs/guides/telemetry\n' +
|
|
183
|
+
' Feedback welcome: https://github.com/allxsmith/bestax/issues';
|
|
184
|
+
const TELEMETRY_ACK_ON = 'Thanks! Opt out anytime with --no-telemetry.';
|
|
185
|
+
const TELEMETRY_ACK_OFF = "No problem — we won't ask again.";
|
|
186
|
+
const TELEMETRY_ACK_UNSAVED = "Couldn't save your choice (config dir not writable) — you may be asked again.";
|
|
187
|
+
/**
|
|
188
|
+
* One-question consent prompt (readline — this package carries no interactive
|
|
189
|
+
* prompt dependency). Returns null when the question is cancelled (Ctrl-C /
|
|
190
|
+
* Ctrl-D): a cancel is not an answer and nothing is persisted. The streams are
|
|
191
|
+
* injectable so tests can drive the question without a TTY.
|
|
192
|
+
*/
|
|
193
|
+
export async function promptTelemetryConsent(io, input = process.stdin, output = process.stdout,
|
|
194
|
+
// Injectable for tests: SIGINT is only emitted by real TTY input, so a test
|
|
195
|
+
// captures the interface through this factory and emits it directly.
|
|
196
|
+
makeInterface = createInterface) {
|
|
197
|
+
io.log('');
|
|
198
|
+
io.log(chalk.gray(TELEMETRY_NOTICE));
|
|
199
|
+
const rl = makeInterface({ input, output });
|
|
200
|
+
const cancelled = new AbortController();
|
|
201
|
+
// Ctrl-C emits SIGINT on the interface and Ctrl-D closes it; both must
|
|
202
|
+
// reject the pending question instead of leaving it hanging forever.
|
|
203
|
+
rl.once('SIGINT', () => {
|
|
204
|
+
// Honor the interrupt instead of swallowing it into a zero exit: the run
|
|
205
|
+
// itself already succeeded, so no abrupt kill — but the ^C must be
|
|
206
|
+
// visible to callers. 130 = 128 + SIGINT, the shell convention.
|
|
207
|
+
process.exitCode = 130;
|
|
208
|
+
rl.close();
|
|
209
|
+
});
|
|
210
|
+
rl.once('close', () => cancelled.abort());
|
|
211
|
+
try {
|
|
212
|
+
const answer = await rl.question('Share anonymous usage stats? (y/N) ', {
|
|
213
|
+
signal: cancelled.signal,
|
|
214
|
+
});
|
|
215
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
rl.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Consent + beacon after a successful run. Only success events are reported,
|
|
226
|
+
* the beacon is awaited (so no error path can kill an in-flight request), and
|
|
227
|
+
* nothing in here may throw or alter the exit code. The prompt is skipped
|
|
228
|
+
* without a TTY on both ends, under an explicit flag, or with DO_NOT_TRACK
|
|
229
|
+
* set (the non-interactive path must never hang).
|
|
230
|
+
*/
|
|
231
|
+
export async function handleTelemetry(stats, flag, io, promptConsent = promptTelemetryConsent) {
|
|
232
|
+
try {
|
|
233
|
+
await reportMigrateRun(stats, flag, {
|
|
234
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
235
|
+
promptConsent: () => promptConsent(io),
|
|
236
|
+
onDecided: (enabled, persisted) => {
|
|
237
|
+
// Worded on whether the write actually stuck: promising "we won't
|
|
238
|
+
// ask again" after a swallowed write failure would be false.
|
|
239
|
+
if (!persisted) {
|
|
240
|
+
io.log(chalk.dim(TELEMETRY_ACK_UNSAVED));
|
|
241
|
+
}
|
|
242
|
+
else if (enabled) {
|
|
243
|
+
io.log(chalk.dim(TELEMETRY_ACK_ON));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
io.log(chalk.dim(TELEMETRY_ACK_OFF));
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Telemetry must never affect the migration's outcome.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Post-run "star us" nudge — TTY only, so piped output stays parseable. */
|
|
256
|
+
function displayStarNudge(io) {
|
|
257
|
+
if (process.stdout.isTTY !== true)
|
|
258
|
+
return;
|
|
259
|
+
io.log('');
|
|
260
|
+
io.log(chalk.yellow('★ If you enjoy using bestax-bulma, please star us on GitHub!'));
|
|
261
|
+
io.log(chalk.dim(' https://github.com/allxsmith/bestax'));
|
|
262
|
+
}
|
|
175
263
|
export function createCLI(io = { log: console.log, error: console.error }) {
|
|
176
264
|
const program = new Command();
|
|
177
265
|
program
|
|
@@ -184,7 +272,9 @@ export function createCLI(io = { log: console.log, error: console.error }) {
|
|
|
184
272
|
.option('-e, --extensions <list>', 'comma-separated file extensions to include', 'js,jsx,ts,tsx,scss,sass')
|
|
185
273
|
.option('--css <mode>', `stylesheet target: ${CSS_MODES.join(', ')}`, 'bestax')
|
|
186
274
|
.option('--no-deps', 'skip updating package.json dependencies')
|
|
187
|
-
.
|
|
275
|
+
.option('--telemetry', 'enable anonymous usage telemetry (https://bestax.io/docs/guides/telemetry)')
|
|
276
|
+
.option('--no-telemetry', 'disable anonymous usage telemetry')
|
|
277
|
+
.action(async (sourceName, targets, options) => {
|
|
188
278
|
const source = getSource(sourceName);
|
|
189
279
|
if (!source) {
|
|
190
280
|
io.error(`${chalk.red('Unknown source')} "${sourceName}". Available sources: ${sourceNames().join(', ')}`);
|
|
@@ -220,6 +310,19 @@ export function createCLI(io = { log: console.log, error: console.error }) {
|
|
|
220
310
|
reportUnsupportedFiles(source, targets, reporter, extensions);
|
|
221
311
|
migrateDependencies(source, targets, reporter, io, runOptions, bulmaReferenced);
|
|
222
312
|
io.log(reporter.render(source.label + (runOptions.dry ? ' (dry run)' : '')));
|
|
313
|
+
displayStarNudge(io);
|
|
314
|
+
// Success path only: rule names and counts, never file/line/message.
|
|
315
|
+
const stats = {
|
|
316
|
+
source: source.name,
|
|
317
|
+
cssMode: runOptions.cssMode,
|
|
318
|
+
dry: runOptions.dry,
|
|
319
|
+
deps: runOptions.deps,
|
|
320
|
+
changedCount: reporter.changedCount,
|
|
321
|
+
todosByRule: reporter
|
|
322
|
+
.todosByRule()
|
|
323
|
+
.map(({ rule, entries }) => ({ rule, count: entries.length })),
|
|
324
|
+
};
|
|
325
|
+
await handleTelemetry(stats, options.telemetry, io);
|
|
223
326
|
});
|
|
224
327
|
return program;
|
|
225
328
|
}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI telemetry kernel (disclosure: https://bestax.io/docs/guides/telemetry).
|
|
3
|
+
*
|
|
4
|
+
* Copied byte-for-byte into create-bestax and bestax-migrate — both publish
|
|
5
|
+
* standalone, so this cannot be a workspace package. `check:conformance
|
|
6
|
+
* --only=telemetry-core` fails if the copies diverge. Edit this file in
|
|
7
|
+
* create-bestax and copy it over the migrate one.
|
|
8
|
+
*
|
|
9
|
+
* No identifier of any kind is generated. Undecided means off. Every failure
|
|
10
|
+
* here is silent: telemetry must never affect the CLI's outcome.
|
|
11
|
+
*/
|
|
12
|
+
export type TelemetryDecision = 'on' | 'off' | 'undecided';
|
|
13
|
+
export type TelemetrySource = 'flag' | 'dnt' | 'env' | 'config' | 'default';
|
|
14
|
+
export interface ResolvedTelemetry {
|
|
15
|
+
decision: TelemetryDecision;
|
|
16
|
+
source: TelemetrySource;
|
|
17
|
+
}
|
|
18
|
+
/** Shared across the bestax CLI family so consent is asked at most once. */
|
|
19
|
+
export declare function telemetryConfigPath(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Precedence: flag (persisted by the caller) → DO_NOT_TRACK → BESTAX_TELEMETRY
|
|
22
|
+
* (per-run, never persisted) → config file → undecided (off).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveTelemetry(flag?: boolean): Promise<ResolvedTelemetry>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns whether the decision was actually written: callers word their
|
|
27
|
+
* acknowledgement on it, because promising "we won't ask again" after a
|
|
28
|
+
* swallowed write failure would be false on every future run.
|
|
29
|
+
*/
|
|
30
|
+
export declare function persistTelemetryDecision(enabled: boolean, decidedBy: string): Promise<boolean>;
|
|
31
|
+
export declare function sendTelemetry(payload: unknown): Promise<void>;
|
|
32
|
+
export declare function getToolVersion(): string;
|
|
33
|
+
export interface ReportRunOptions {
|
|
34
|
+
/** True only when a consent question could actually be answered. */
|
|
35
|
+
interactive: boolean;
|
|
36
|
+
/** Owns the consent UI up to the answer (notice, question); null = cancelled. */
|
|
37
|
+
promptConsent: () => Promise<boolean | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Called after a prompted answer has been (attempted to be) saved, so the
|
|
40
|
+
* acknowledgement can tell the truth about whether it will stick.
|
|
41
|
+
*/
|
|
42
|
+
onDecided?: (enabled: boolean, persisted: boolean) => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function reportRun(flag: boolean | undefined, toolName: string, options: ReportRunOptions, payload: unknown): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=telemetry-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-core.d.ts","sourceRoot":"","sources":["../src/telemetry-core.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,KAAK,GAAG,WAAW,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,EAAE,eAAe,CAAC;CACzB;AAYD,4EAA4E;AAC5E,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C;AA6BD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,CAAC,EAAE,OAAO,GACb,OAAO,CAAC,iBAAiB,CAAC,CAyB5B;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAWnE;AAED,wBAAgB,cAAc,IAAI,MAAM,CAUvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,WAAW,EAAE,OAAO,CAAC;IACrB,iFAAiF;IACjF,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC7C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5D;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,OAAO,GAAG,SAAS,EACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAqCf"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
const DEFAULT_ENDPOINT = 'https://bestax.io/api/t';
|
|
7
|
+
const TIMEOUT_MS = 1500;
|
|
8
|
+
/** Shared across the bestax CLI family so consent is asked at most once. */
|
|
9
|
+
export function telemetryConfigPath() {
|
|
10
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
11
|
+
return join(base, 'bestax', 'telemetry.json');
|
|
12
|
+
}
|
|
13
|
+
async function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(await readFile(telemetryConfigPath(), 'utf-8'));
|
|
16
|
+
// Consent fails closed: only a well-formed v1 record is trusted. A bare
|
|
17
|
+
// {"enabled":true}, an array, or an unknown future schema version reads
|
|
18
|
+
// as "never asked" (prompt again) — never as an opt-in.
|
|
19
|
+
if (parsed !== null &&
|
|
20
|
+
typeof parsed === 'object' &&
|
|
21
|
+
!Array.isArray(parsed) &&
|
|
22
|
+
parsed.version === 1) {
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Missing or corrupt config reads as "never asked".
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function doNotTrack() {
|
|
32
|
+
const dnt = process.env.DO_NOT_TRACK;
|
|
33
|
+
return dnt !== undefined && dnt !== '' && dnt !== '0';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Precedence: flag (persisted by the caller) → DO_NOT_TRACK → BESTAX_TELEMETRY
|
|
37
|
+
* (per-run, never persisted) → config file → undecided (off).
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveTelemetry(flag) {
|
|
40
|
+
if (flag !== undefined) {
|
|
41
|
+
return {
|
|
42
|
+
decision: flag ? 'on' : 'off',
|
|
43
|
+
source: 'flag',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (doNotTrack()) {
|
|
47
|
+
return { decision: 'off', source: 'dnt' };
|
|
48
|
+
}
|
|
49
|
+
const env = process.env.BESTAX_TELEMETRY;
|
|
50
|
+
if (env === '0' || env === '1') {
|
|
51
|
+
return {
|
|
52
|
+
decision: env === '1' ? 'on' : 'off',
|
|
53
|
+
source: 'env',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const config = await readConfig();
|
|
57
|
+
if (config && typeof config.enabled === 'boolean') {
|
|
58
|
+
return {
|
|
59
|
+
decision: config.enabled ? 'on' : 'off',
|
|
60
|
+
source: 'config',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { decision: 'undecided', source: 'default' };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns whether the decision was actually written: callers word their
|
|
67
|
+
* acknowledgement on it, because promising "we won't ask again" after a
|
|
68
|
+
* swallowed write failure would be false on every future run.
|
|
69
|
+
*/
|
|
70
|
+
export async function persistTelemetryDecision(enabled, decidedBy) {
|
|
71
|
+
try {
|
|
72
|
+
const config = {
|
|
73
|
+
version: 1,
|
|
74
|
+
enabled,
|
|
75
|
+
decidedAt: new Date().toISOString(),
|
|
76
|
+
decidedBy,
|
|
77
|
+
};
|
|
78
|
+
const filePath = telemetryConfigPath();
|
|
79
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
80
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// An unwritable config dir must not break the CLI.
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function sendTelemetry(payload) {
|
|
89
|
+
try {
|
|
90
|
+
await fetch(process.env.BESTAX_TELEMETRY_ENDPOINT || DEFAULT_ENDPOINT, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/json' },
|
|
93
|
+
body: JSON.stringify(payload),
|
|
94
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Fire-and-forget: the response (or its absence) is never surfaced.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function getToolVersion() {
|
|
102
|
+
try {
|
|
103
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
104
|
+
const packageJson = JSON.parse(readFileSync(join(dir, '..', 'package.json'), 'utf-8'));
|
|
105
|
+
return packageJson.version || '0.0.0';
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return '0.0.0';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function reportRun(flag, toolName, options, payload) {
|
|
112
|
+
const resolved = await resolveTelemetry(flag);
|
|
113
|
+
let decision = resolved.decision;
|
|
114
|
+
if (resolved.source === 'flag') {
|
|
115
|
+
// Flags are the scripted equivalent of the prompt: decided once,
|
|
116
|
+
// remembered — EXCEPT under DO_NOT_TRACK, where the flag still applies to
|
|
117
|
+
// this one run (an explicit ask on the command line wins) but is never
|
|
118
|
+
// written down, so a copied command containing --telemetry cannot enable
|
|
119
|
+
// telemetry beyond the run it was typed for.
|
|
120
|
+
if (!doNotTrack()) {
|
|
121
|
+
const persisted = await persistTelemetryDecision(decision === 'on', `${toolName}@${getToolVersion()}`);
|
|
122
|
+
// A silently unsaved flag is worse than an unsaved answer: with an
|
|
123
|
+
// opposite value already in the shared config, --no-telemetry would
|
|
124
|
+
// apply to this run only and the next family run would re-enable.
|
|
125
|
+
if (!persisted) {
|
|
126
|
+
options.onDecided?.(decision === 'on', false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (decision === 'undecided' && options.interactive) {
|
|
131
|
+
const answer = await options.promptConsent();
|
|
132
|
+
if (answer !== null) {
|
|
133
|
+
const persisted = await persistTelemetryDecision(answer, `${toolName}@${getToolVersion()}`);
|
|
134
|
+
decision = answer ? 'on' : 'off';
|
|
135
|
+
options.onDecided?.(answer, persisted);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (decision === 'on') {
|
|
139
|
+
await sendTelemetry(payload);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReportRunOptions } from './telemetry-core.js';
|
|
2
|
+
/**
|
|
3
|
+
* bestax-migrate payload + reporter. The kernel lives in telemetry-core.ts
|
|
4
|
+
* (copied from create-bestax; `check:conformance --only=telemetry-core`).
|
|
5
|
+
*
|
|
6
|
+
* Envelope and props are closed enums, versions, or bounded integers.
|
|
7
|
+
* `todosByRule` slugs are migrate's own rule names (including `prop:<jsxProp>`),
|
|
8
|
+
* never file paths, messages, or code.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Client half of the wire-contract caps. The worker's schema.ts / validate.ts
|
|
12
|
+
* (telemetry-worker) hold the other half; scripts/telemetry-contract.test.mjs
|
|
13
|
+
* pins each pair equal (TODO_RULES_CAP === MAX_TODO_RULES, TODO_COUNT_CAP ===
|
|
14
|
+
* MAX_CHANGED_COUNT, CHANGED_COUNT_CAP === CHANGED_COUNT_DOUBLE_CAP).
|
|
15
|
+
*/
|
|
16
|
+
export declare const TODO_RULES_CAP = 20;
|
|
17
|
+
export declare const TODO_COUNT_CAP = 100000;
|
|
18
|
+
export declare const CHANGED_COUNT_CAP = 10000;
|
|
19
|
+
export interface MigrateRunStats {
|
|
20
|
+
/** Registry source name, e.g. 'react-bulma-components'. */
|
|
21
|
+
source: string;
|
|
22
|
+
/** 'bestax' | 'bulma' | 'keep'. */
|
|
23
|
+
cssMode: string;
|
|
24
|
+
dry: boolean;
|
|
25
|
+
deps: boolean;
|
|
26
|
+
changedCount: number;
|
|
27
|
+
todosByRule: Array<{
|
|
28
|
+
rule: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface MigratePayload {
|
|
33
|
+
v: 1;
|
|
34
|
+
tool: 'bestax-migrate';
|
|
35
|
+
event: 'migrate';
|
|
36
|
+
toolVersion: string;
|
|
37
|
+
nodeMajor: number;
|
|
38
|
+
platform: string;
|
|
39
|
+
props: {
|
|
40
|
+
source: string;
|
|
41
|
+
cssMode: string;
|
|
42
|
+
dry: boolean;
|
|
43
|
+
deps: boolean;
|
|
44
|
+
changedCount: number;
|
|
45
|
+
};
|
|
46
|
+
todosByRule?: Array<{
|
|
47
|
+
rule: string;
|
|
48
|
+
count: number;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
export declare function buildMigratePayload(stats: MigrateRunStats): MigratePayload;
|
|
52
|
+
export declare function reportMigrateRun(stats: MigrateRunStats, flag: boolean | undefined, options: ReportRunOptions): Promise<void>;
|
|
53
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,cAAc,SAAS,CAAC;AACrC,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,CAAC,CAAC;IACL,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,OAAO,CAAC;QACb,IAAI,EAAE,OAAO,CAAC;QAGd,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,cAAc,CAuB1E;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,OAAO,GAAG,SAAS,EACzB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getToolVersion, reportRun } from './telemetry-core.js';
|
|
2
|
+
/**
|
|
3
|
+
* bestax-migrate payload + reporter. The kernel lives in telemetry-core.ts
|
|
4
|
+
* (copied from create-bestax; `check:conformance --only=telemetry-core`).
|
|
5
|
+
*
|
|
6
|
+
* Envelope and props are closed enums, versions, or bounded integers.
|
|
7
|
+
* `todosByRule` slugs are migrate's own rule names (including `prop:<jsxProp>`),
|
|
8
|
+
* never file paths, messages, or code.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Client half of the wire-contract caps. The worker's schema.ts / validate.ts
|
|
12
|
+
* (telemetry-worker) hold the other half; scripts/telemetry-contract.test.mjs
|
|
13
|
+
* pins each pair equal (TODO_RULES_CAP === MAX_TODO_RULES, TODO_COUNT_CAP ===
|
|
14
|
+
* MAX_CHANGED_COUNT, CHANGED_COUNT_CAP === CHANGED_COUNT_DOUBLE_CAP).
|
|
15
|
+
*/
|
|
16
|
+
export const TODO_RULES_CAP = 20;
|
|
17
|
+
export const TODO_COUNT_CAP = 100000;
|
|
18
|
+
export const CHANGED_COUNT_CAP = 10000;
|
|
19
|
+
export function buildMigratePayload(stats) {
|
|
20
|
+
const todosByRule = stats.todosByRule
|
|
21
|
+
.slice(0, TODO_RULES_CAP)
|
|
22
|
+
.map(({ rule, count }) => ({
|
|
23
|
+
rule,
|
|
24
|
+
count: Math.min(count, TODO_COUNT_CAP),
|
|
25
|
+
}));
|
|
26
|
+
return {
|
|
27
|
+
v: 1,
|
|
28
|
+
tool: 'bestax-migrate',
|
|
29
|
+
event: 'migrate',
|
|
30
|
+
toolVersion: getToolVersion(),
|
|
31
|
+
nodeMajor: Number(process.versions.node.split('.')[0]),
|
|
32
|
+
platform: process.platform,
|
|
33
|
+
props: {
|
|
34
|
+
source: stats.source,
|
|
35
|
+
cssMode: stats.cssMode,
|
|
36
|
+
dry: stats.dry,
|
|
37
|
+
deps: stats.deps,
|
|
38
|
+
changedCount: Math.min(stats.changedCount, CHANGED_COUNT_CAP),
|
|
39
|
+
},
|
|
40
|
+
...(todosByRule.length > 0 ? { todosByRule } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function reportMigrateRun(stats, flag, options) {
|
|
44
|
+
await reportRun(flag, 'bestax-migrate', options, buildMigratePayload(stats));
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestax-migrate",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Codemods for migrating existing React apps from other Bulma libraries to @allxsmith/bestax-bulma",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-dom": "^19.2.8",
|
|
52
52
|
"ts-jest": "^29.4.12",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
54
|
-
"@allxsmith/bestax-bulma": "^5.11.
|
|
54
|
+
"@allxsmith/bestax-bulma": "^5.11.2"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"clean": "rimraf dist",
|
|
71
71
|
"release": "npx semantic-release"
|
|
72
72
|
},
|
|
73
|
-
"readme": "# bestax-migrate\n\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://opensource.org/licenses/MIT)\n[](https://socket.dev/npm/package/bestax-migrate/overview)\n[](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[](https://www.npmjs.com/package/bestax-migrate#provenance)\n[](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported source libraries:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite `react-bulma-components` imports to `@allxsmith/bestax-bulma` (including `const { Input } = Form` destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`)\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove `react-bulma-components`, add `@allxsmith/bestax-bulma`, bump `bulma` to `^1`, and swap the dead `node-sass` for dart `sass` (no install is ever run)\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the [migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n## Options\n\n| Flag | Description |\n| -------------------- | -------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout |\n| `--extensions`, `-e` | File extensions to include (default `js,jsx,ts,tsx,scss,sass`) |\n| `--css <mode>` | Stylesheet target: `bestax` (default), `bulma`, or `keep` |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## License\n\nMIT © Alex Smith\n"
|
|
73
|
+
"readme": "# bestax-migrate\n\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://opensource.org/licenses/MIT)\n[](https://socket.dev/npm/package/bestax-migrate/overview)\n[](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[](https://www.npmjs.com/package/bestax-migrate#provenance)\n[](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported source libraries:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite `react-bulma-components` imports to `@allxsmith/bestax-bulma` (including `const { Input } = Form` destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`)\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove `react-bulma-components`, add `@allxsmith/bestax-bulma`, bump `bulma` to `^1`, and swap the dead `node-sass` for dart `sass` (no install is ever run)\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the [migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n## Options\n\n| Flag | Description |\n| -------------------- | -------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout |\n| `--extensions`, `-e` | File extensions to include (default `js,jsx,ts,tsx,scss,sass`) |\n| `--css <mode>` | Stylesheet target: `bestax` (default), `bulma`, or `keep` |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## Telemetry\n\n`bestax-migrate` can send one **anonymous** usage event after a successful\nrun — only if you opt in when asked (once, on an interactive terminal). The\nevent is just the run's shape — source library, CSS mode, the dry/deps flags, a\ncapped changed-file count, and per-rule TODO counts — plus the CLI version,\nNode major version, and OS platform name. Never file paths, file contents,\ncode, names, IPs, or machine IDs, and no identifier exists that could link two\nevents together. Opt out any time with `--no-telemetry`,\n`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.\n\nFull disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)\n\n## License\n\nMIT © Alex Smith\n"
|
|
74
74
|
}
|