@taqwright/taqwright 0.0.24

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.
Files changed (132) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/auto-appium.d.ts +12 -0
  4. package/dist/auto-appium.js +77 -0
  5. package/dist/bin/branding.d.ts +6 -0
  6. package/dist/bin/branding.js +22 -0
  7. package/dist/bin/index.d.ts +2 -0
  8. package/dist/bin/index.js +321 -0
  9. package/dist/bin/init.d.ts +26 -0
  10. package/dist/bin/init.js +902 -0
  11. package/dist/bin/inspect.d.ts +9 -0
  12. package/dist/bin/inspect.js +91 -0
  13. package/dist/bin/report-branding.d.ts +2 -0
  14. package/dist/bin/report-branding.js +42 -0
  15. package/dist/branding-assets.d.ts +1 -0
  16. package/dist/branding-assets.js +1 -0
  17. package/dist/capabilities-helpers.d.ts +7 -0
  18. package/dist/capabilities-helpers.js +14 -0
  19. package/dist/capabilities.d.ts +6 -0
  20. package/dist/capabilities.js +86 -0
  21. package/dist/config.d.ts +17 -0
  22. package/dist/config.js +235 -0
  23. package/dist/discovery-setup.d.ts +1 -0
  24. package/dist/discovery-setup.js +61 -0
  25. package/dist/discovery.d.ts +17 -0
  26. package/dist/discovery.js +55 -0
  27. package/dist/docs/configuration.html +376 -0
  28. package/dist/docs/custom-reporters.html +265 -0
  29. package/dist/docs/docker.html +339 -0
  30. package/dist/docs/docs.js +173 -0
  31. package/dist/docs/generating-tests.html +161 -0
  32. package/dist/docs/images/taqwright-html-report.png +0 -0
  33. package/dist/docs/index.html +13 -0
  34. package/dist/docs/installation.html +686 -0
  35. package/dist/docs/parallel.html +271 -0
  36. package/dist/docs/running-tests.html +385 -0
  37. package/dist/docs/styles.css +460 -0
  38. package/dist/docs/writing-tests.html +565 -0
  39. package/dist/doctor.d.ts +33 -0
  40. package/dist/doctor.js +508 -0
  41. package/dist/expect.d.ts +38 -0
  42. package/dist/expect.js +96 -0
  43. package/dist/fixture/artifact-mode.d.ts +2 -0
  44. package/dist/fixture/artifact-mode.js +7 -0
  45. package/dist/fixture/index.d.ts +15 -0
  46. package/dist/fixture/index.js +324 -0
  47. package/dist/images/taqwright-html-report.png +0 -0
  48. package/dist/images/taqwright_favicon.png +0 -0
  49. package/dist/images/taqwright_logo.png +0 -0
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.js +7 -0
  52. package/dist/inspector/codegen-appium.d.ts +3 -0
  53. package/dist/inspector/codegen-appium.js +228 -0
  54. package/dist/inspector/devices.d.ts +41 -0
  55. package/dist/inspector/devices.js +422 -0
  56. package/dist/inspector/locator-suggester.d.ts +23 -0
  57. package/dist/inspector/locator-suggester.js +539 -0
  58. package/dist/inspector/recorder.d.ts +128 -0
  59. package/dist/inspector/recorder.js +162 -0
  60. package/dist/inspector/server.d.ts +39 -0
  61. package/dist/inspector/server.js +1210 -0
  62. package/dist/inspector/session.d.ts +84 -0
  63. package/dist/inspector/session.js +262 -0
  64. package/dist/inspector/ui.d.ts +1 -0
  65. package/dist/inspector/ui.js +5508 -0
  66. package/dist/keys.d.ts +3 -0
  67. package/dist/keys.js +28 -0
  68. package/dist/locator/index.d.ts +206 -0
  69. package/dist/locator/index.js +1506 -0
  70. package/dist/logger.d.ts +5 -0
  71. package/dist/logger.js +5 -0
  72. package/dist/mobile/index.d.ts +130 -0
  73. package/dist/mobile/index.js +762 -0
  74. package/dist/network/android.d.ts +5 -0
  75. package/dist/network/android.js +87 -0
  76. package/dist/network/ca.d.ts +10 -0
  77. package/dist/network/ca.js +136 -0
  78. package/dist/network/har.d.ts +90 -0
  79. package/dist/network/har.js +101 -0
  80. package/dist/network/host-proxy.d.ts +16 -0
  81. package/dist/network/host-proxy.js +134 -0
  82. package/dist/network/index.d.ts +26 -0
  83. package/dist/network/index.js +105 -0
  84. package/dist/network/ios-sim.d.ts +3 -0
  85. package/dist/network/ios-sim.js +29 -0
  86. package/dist/network/proxy.d.ts +13 -0
  87. package/dist/network/proxy.js +310 -0
  88. package/dist/providers/appium.d.ts +23 -0
  89. package/dist/providers/appium.js +288 -0
  90. package/dist/providers/browserstack/index.d.ts +5 -0
  91. package/dist/providers/browserstack/index.js +77 -0
  92. package/dist/providers/browserstack/utils.d.ts +1 -0
  93. package/dist/providers/browserstack/utils.js +6 -0
  94. package/dist/providers/cloud.d.ts +53 -0
  95. package/dist/providers/cloud.js +117 -0
  96. package/dist/providers/emulator/index.d.ts +8 -0
  97. package/dist/providers/emulator/index.js +47 -0
  98. package/dist/providers/index.d.ts +10 -0
  99. package/dist/providers/index.js +33 -0
  100. package/dist/providers/lambdatest/index.d.ts +28 -0
  101. package/dist/providers/lambdatest/index.js +99 -0
  102. package/dist/providers/lambdatest/utils.d.ts +1 -0
  103. package/dist/providers/lambdatest/utils.js +6 -0
  104. package/dist/providers/local/index.d.ts +9 -0
  105. package/dist/providers/local/index.js +53 -0
  106. package/dist/providers/local-session.d.ts +16 -0
  107. package/dist/providers/local-session.js +55 -0
  108. package/dist/setup/archive.d.ts +2 -0
  109. package/dist/setup/archive.js +43 -0
  110. package/dist/setup/avd.d.ts +12 -0
  111. package/dist/setup/avd.js +103 -0
  112. package/dist/setup/index.d.ts +6 -0
  113. package/dist/setup/index.js +55 -0
  114. package/dist/setup/install-android.d.ts +2 -0
  115. package/dist/setup/install-android.js +70 -0
  116. package/dist/setup/install-appium.d.ts +1 -0
  117. package/dist/setup/install-appium.js +64 -0
  118. package/dist/setup/install-jdk.d.ts +1 -0
  119. package/dist/setup/install-jdk.js +58 -0
  120. package/dist/setup/paths.d.ts +16 -0
  121. package/dist/setup/paths.js +88 -0
  122. package/dist/setup/spawn-tool.d.ts +3 -0
  123. package/dist/setup/spawn-tool.js +11 -0
  124. package/dist/tracer/index.d.ts +34 -0
  125. package/dist/tracer/index.js +687 -0
  126. package/dist/tracer/proxy.d.ts +3 -0
  127. package/dist/tracer/proxy.js +60 -0
  128. package/dist/types/index.d.ts +189 -0
  129. package/dist/types/index.js +6 -0
  130. package/dist/utils.d.ts +2 -0
  131. package/dist/utils.js +37 -0
  132. package/package.json +79 -0
@@ -0,0 +1,902 @@
1
+ import { mkdir, writeFile, readdir } from 'node:fs/promises';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { resolve, join, basename, relative, dirname } from 'node:path';
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { stdin, stdout } from 'node:process';
8
+ import { runSetup } from '../setup/index.js';
9
+ import { download } from '../setup/archive.js';
10
+ import { spawnTool } from '../setup/spawn-tool.js';
11
+ import { detectAndroidToolchain, listAvds } from '../doctor.js';
12
+ const execP = promisify(exec);
13
+ const DEMO_APK_FILENAME = 'DemoApp-v1.0.0.apk';
14
+ const DEMO_APP_BUNDLE_ID = 'com.taqelah.demo_app';
15
+ const DEMO_APK_URL = 'https://github.com/taqelah/demo-app/releases/download/v1.0.0/DemoApp-v1.0.0.apk';
16
+ const DEMO_AVD_NAME = 'taqwright_api34';
17
+ export async function runInit(argDir, opts = {}) {
18
+ const scripted = argDir !== undefined &&
19
+ opts.testDir !== undefined &&
20
+ opts.platform !== undefined &&
21
+ opts.install !== undefined;
22
+ const interactive = Boolean(stdin.isTTY) && !scripted;
23
+ const isMac = process.platform === 'darwin';
24
+ console.log('\ntaqwright init β€” scaffold a new project\n');
25
+ let targetDir;
26
+ let testDir;
27
+ let platforms;
28
+ let install;
29
+ let installToolchain;
30
+ let withAvd;
31
+ let demoApp;
32
+ let deviceAvdName;
33
+ let detected;
34
+ if (!interactive) {
35
+ const dirInput = argDir ?? './taqwright-tests';
36
+ targetDir = resolve(process.cwd(), dirInput);
37
+ const locErr = projectLocationError(targetDir);
38
+ if (locErr) {
39
+ console.error(`error: ${locErr}.`);
40
+ process.exit(1);
41
+ }
42
+ if (existsSync(targetDir) && (await isNonEmpty(targetDir)) && !opts.yes) {
43
+ console.error(`error: "${targetDir}" is not empty. Re-run with --yes to write into it anyway.`);
44
+ process.exit(1);
45
+ }
46
+ testDir = opts.testDir ?? 'tests';
47
+ if (!isValidTestDir(testDir)) {
48
+ console.error(`error: invalid --test-dir "${testDir}" β€” ${TEST_DIR_HINT}.`);
49
+ process.exit(1);
50
+ }
51
+ platforms =
52
+ opts.platform === 'both' ? ['android', 'ios'] : [(opts.platform ?? 'android')];
53
+ install = opts.install ?? true;
54
+ installToolchain = opts.installToolchain ?? false;
55
+ withAvd = (opts.withAvd ?? false) && installToolchain;
56
+ demoApp = (opts.demoApp ?? false) && platforms.includes('android');
57
+ if (!stdin.isTTY && !scripted) {
58
+ console.log(`(no TTY β€” running non-interactively: dir=${dirInput}, testDir=${testDir}, ` +
59
+ `platform=${opts.platform ?? 'android'}, install=${install})\n`);
60
+ }
61
+ }
62
+ else {
63
+ const rl = createInterface({ input: stdin, output: stdout });
64
+ console.log('Press Enter to accept the default shown in (parentheses).\n');
65
+ try {
66
+ targetDir = await askProjectDir(rl, argDir);
67
+ if (existsSync(targetDir) && (await isNonEmpty(targetDir)) && !opts.yes) {
68
+ const proceed = await yesNo(rl, `Directory "${targetDir}" is not empty β€” continue and write into it?`, false);
69
+ if (!proceed) {
70
+ console.log('aborted.');
71
+ return;
72
+ }
73
+ }
74
+ testDir = opts.testDir ?? (await askTestDir(rl));
75
+ if (!isValidTestDir(testDir)) {
76
+ console.error(`error: invalid --test-dir "${testDir}" β€” ${TEST_DIR_HINT}.`);
77
+ process.exit(1);
78
+ }
79
+ const platformInput = opts.platform ??
80
+ (await askChoice(rl, 'Platform', platformChoices(isMac), 'android'));
81
+ platforms = platformInput === 'both' ? ['android', 'ios'] : [platformInput];
82
+ install = opts.install ?? true;
83
+ if (opts.installToolchain !== undefined) {
84
+ installToolchain = opts.installToolchain;
85
+ }
86
+ else if (!platforms.includes('android')) {
87
+ installToolchain = false;
88
+ }
89
+ else {
90
+ detected = await detectAndroidToolchain();
91
+ printAndroidToolchainStatus(detected);
92
+ if (detected.ready) {
93
+ console.log(' β†’ detected a working Android toolchain β€” skipping install.');
94
+ if (!detected.avd) {
95
+ console.log(' (no AVD found β€” add one with `taqwright install --with-avd`, or use a device/cloud)');
96
+ }
97
+ installToolchain = false;
98
+ }
99
+ else {
100
+ installToolchain = await yesNo(rl, 'Auto-install the Android toolchain now? (~700 MB: JDK + Android SDK + Appium β€” ' +
101
+ "a complete self-contained set; won't touch your system tools)", false);
102
+ }
103
+ }
104
+ if (opts.withAvd !== undefined) {
105
+ withAvd = opts.withAvd;
106
+ }
107
+ else if (!installToolchain) {
108
+ withAvd = false;
109
+ }
110
+ else if (detected?.avd) {
111
+ withAvd = await yesNo(rl, `You already have an AVD (${detected.avdNames.join(', ')}) β€” ` +
112
+ 'create the managed taqwright_api34 too? (~1 GB: system image + AVD)', false);
113
+ }
114
+ else {
115
+ withAvd = await yesNo(rl, 'Also create an Android emulator now? (~1 GB: system image + AVD). ' +
116
+ 'Skip and no emulator is created β€” boot the example test on a physical ' +
117
+ 'device, or add one later with `taqwright install --with-avd`', true);
118
+ }
119
+ if (platforms.includes('android') && !withAvd && detected?.avd) {
120
+ deviceAvdName =
121
+ detected.avdNames.length === 1
122
+ ? detected.avdNames[0]
123
+ : await askAvdChoice(rl, detected.avdNames);
124
+ }
125
+ demoApp = (opts.demoApp ?? true) && platforms.includes('android');
126
+ }
127
+ finally {
128
+ rl.close();
129
+ }
130
+ }
131
+ const platErr = platformSupportError(isMac, platforms);
132
+ if (platErr) {
133
+ console.error(`error: ${platErr}.`);
134
+ process.exit(1);
135
+ }
136
+ const projectName = basename(targetDir);
137
+ const pkgName = toPackageName(projectName);
138
+ if (projectName !== pkgName) {
139
+ console.log(`note: folder "${projectName}" kept as-is, but the package name was normalized to "${pkgName}".`);
140
+ }
141
+ try {
142
+ await mkdir(join(targetDir, testDir), { recursive: true });
143
+ }
144
+ catch (err) {
145
+ console.error(`error: cannot create "${targetDir}": ${err.message}\n` +
146
+ ' Check the path and your write permissions, then try again.');
147
+ process.exit(1);
148
+ }
149
+ let demoAppReady = false;
150
+ if (demoApp) {
151
+ const apkPath = join(targetDir, 'app', DEMO_APK_FILENAME);
152
+ process.stdout.write(`\nDownloading the demo app (${DEMO_APK_FILENAME})… `);
153
+ try {
154
+ await download(DEMO_APK_URL, apkPath, AbortSignal.timeout(120_000));
155
+ demoAppReady = true;
156
+ console.log('done.');
157
+ }
158
+ catch (err) {
159
+ console.log('failed.');
160
+ console.error(` Could not fetch the demo app (${err.message}).\n` +
161
+ ' Scaffolding continues β€” drop an APK in app/ and set buildPath/appBundleId,\n' +
162
+ ` or download it manually from ${DEMO_APK_URL}`);
163
+ }
164
+ }
165
+ const scopeForBoth = platforms.length === 2 && demoAppReady;
166
+ const demoAvdReady = demoAppReady && withAvd;
167
+ const files = [
168
+ ['package.json', packageJsonTemplate(pkgName)],
169
+ ['.npmrc', npmrcTemplate()],
170
+ ['tsconfig.json', tsconfigTemplate(testDir)],
171
+ [
172
+ 'taqwright.config.ts',
173
+ configTemplate(platforms, testDir, {
174
+ demoApp: demoAppReady,
175
+ demoAvd: demoAvdReady,
176
+ scoped: scopeForBoth,
177
+ deviceName: deviceAvdName,
178
+ }),
179
+ ],
180
+ ];
181
+ if (scopeForBoth) {
182
+ files.push([join(testDir, 'android', 'example.spec.ts'), exampleTestTemplate(true)]);
183
+ files.push([join(testDir, 'ios', 'example.spec.ts'), exampleTestTemplate(false)]);
184
+ }
185
+ else {
186
+ files.push([join(testDir, 'example.spec.ts'), exampleTestTemplate(demoAppReady)]);
187
+ }
188
+ files.push(['.gitignore', gitignoreTemplate()]);
189
+ let overwrite = !!opts.yes;
190
+ if (!overwrite && interactive) {
191
+ const conflicts = files.filter(([rel]) => existsSync(join(targetDir, rel)));
192
+ if (conflicts.length > 0) {
193
+ const rl2 = createInterface({ input: stdin, output: stdout });
194
+ try {
195
+ overwrite = await yesNo(rl2, `${conflicts.length} file(s) already exist β€” overwrite them?`, false);
196
+ }
197
+ finally {
198
+ rl2.close();
199
+ }
200
+ }
201
+ }
202
+ const written = [];
203
+ const skipped = [];
204
+ for (const [rel, content] of files) {
205
+ const dest = join(targetDir, rel);
206
+ if (existsSync(dest) && !overwrite) {
207
+ skipped.push(rel);
208
+ continue;
209
+ }
210
+ try {
211
+ await mkdir(dirname(dest), { recursive: true });
212
+ await writeFile(dest, content);
213
+ }
214
+ catch (err) {
215
+ console.error(`error: failed writing ${rel}: ${err.message}`);
216
+ process.exit(1);
217
+ }
218
+ written.push(rel);
219
+ }
220
+ const showRoot = relative(process.cwd(), targetDir) || '.';
221
+ if (written.length) {
222
+ console.log('\nCreated:');
223
+ for (const rel of written)
224
+ console.log(' ' + join(showRoot, rel));
225
+ if (demoAppReady)
226
+ console.log(' ' + join(showRoot, 'app', DEMO_APK_FILENAME));
227
+ }
228
+ if (skipped.length) {
229
+ console.log('\nSkipped (already exist β€” re-run with --yes to overwrite):');
230
+ for (const rel of skipped)
231
+ console.log(' ' + join(showRoot, rel));
232
+ }
233
+ const cdHint = relative(process.cwd(), targetDir) || '.';
234
+ if (install) {
235
+ const linkedDev = await isTaqwrightGloballyLinked();
236
+ if (linkedDev) {
237
+ console.log('\nDetected globally-linked taqwright β€” will `npm link @taqwright/taqwright` after install (instead of fetching from the registry).');
238
+ }
239
+ console.log('\nRunning npm install …');
240
+ const code = await runNpm(['install'], targetDir);
241
+ if (code !== 0) {
242
+ console.error(`\nnpm install exited with code ${code}.`);
243
+ if (!linkedDev) {
244
+ console.error('\n@taqwright/taqwright installs from git+ssh://git@github.com/taqelah/taqwright.git,');
245
+ console.error('so a failure here usually means no SSH access to the private taqelah/taqwright repo.');
246
+ console.error('Verify your GitHub SSH key with: ssh -T git@github.com');
247
+ console.error('To use a local taqwright build instead:');
248
+ console.error(' cd /path/to/taqwright && npm link');
249
+ console.error(` cd ${cdHint} && npm install && npm link @taqwright/taqwright`);
250
+ }
251
+ process.exit(code);
252
+ }
253
+ if (linkedDev) {
254
+ const linkCode = await runNpm(['link', '@taqwright/taqwright'], targetDir);
255
+ if (linkCode !== 0) {
256
+ console.error('npm link @taqwright/taqwright failed.');
257
+ process.exit(linkCode);
258
+ }
259
+ }
260
+ }
261
+ let toolchainInstalled = false;
262
+ if (installToolchain && platforms.includes('android')) {
263
+ console.log((withAvd
264
+ ? '\nInstalling the Android toolchain + emulator β€” this can take several minutes…'
265
+ : '\nInstalling the Android toolchain β€” this can take a few minutes…') +
266
+ "\n(installs under taqwright's own dir β€” your shell's JAVA_HOME/PATH stays as-is)\n");
267
+ try {
268
+ await runSetup({ withAvd });
269
+ toolchainInstalled = true;
270
+ }
271
+ catch (err) {
272
+ console.error(`\ntaqwright install failed: ${err.message}`);
273
+ console.error('Scaffolding succeeded; retry the toolchain later with: npx taqwright install');
274
+ }
275
+ }
276
+ else if (installToolchain && !platforms.includes('android')) {
277
+ console.log('\n(Skipping --install-toolchain: `taqwright install` provisions the Android stack, ' +
278
+ 'but this project is iOS-only.)');
279
+ }
280
+ console.log('\nNext steps:');
281
+ console.log(` cd ${cdHint}`);
282
+ if (!install)
283
+ console.log(' npm install');
284
+ if (platforms.includes('android')) {
285
+ const hasAvd = detected?.avd ?? (await listAvds()).length > 0;
286
+ if (toolchainInstalled && !withAvd && !hasAvd) {
287
+ console.log(' npx taqwright install --with-avd # add an Android emulator (~1 GB), or use a physical device');
288
+ }
289
+ else if (!toolchainInstalled && !detected?.ready) {
290
+ console.log(hasAvd
291
+ ? ' npx taqwright install # Android toolchain (JDK + SDK + Appium) β€” you already have an emulator'
292
+ : ' npx taqwright install --with-avd # Android toolchain + emulator (JDK + SDK + Appium + AVD); drop --with-avd to skip the ~1 GB emulator');
293
+ }
294
+ }
295
+ console.log(' npx taqwright test');
296
+ console.log('\nCommands:');
297
+ console.log(' npx taqwright doctor');
298
+ console.log(' npx taqwright codegen');
299
+ console.log(' npx taqwright test');
300
+ console.log(' npx taqwright show-report');
301
+ if (!demoAppReady && platforms.includes('android')) {
302
+ console.log('\nNo demo app was added β€” the example test is a no-op stub. Drop an APK in\n' +
303
+ 'app/ and set buildPath/appBundleId in taqwright.config.ts, or re-run\n' +
304
+ '`npx taqwright init --demo-app` to fetch the demo app.');
305
+ }
306
+ if (demoAppReady && !demoAvdReady) {
307
+ if (deviceAvdName) {
308
+ console.log(`\nThe config targets the "${deviceAvdName}" AVD β€” taqwright boots it automatically\n` +
309
+ 'when you run the test above (or uses it if already running).');
310
+ }
311
+ else if (detected?.avd) {
312
+ console.log('\nBoot one of your emulators (or connect a device) and set device.name in\n' +
313
+ 'taqwright.config.ts before running the test above.');
314
+ }
315
+ else {
316
+ console.log('\nNo emulator was found β€” run `npx taqwright install --with-avd` to create one (or\n' +
317
+ 'connect a device), then set device.name + autoStartDevice in taqwright.config.ts.');
318
+ }
319
+ }
320
+ console.log('');
321
+ }
322
+ async function ask(rl, label, def) {
323
+ const answer = (await rl.question(`? ${label} (${def}): `)).trim();
324
+ return answer || def;
325
+ }
326
+ async function askChoice(rl, label, choices, def) {
327
+ const list = choices.join('/');
328
+ while (true) {
329
+ const raw = (await rl.question(`? ${label} [${list}]: `)).trim().toLowerCase();
330
+ if (!raw)
331
+ return def;
332
+ if (choices.includes(raw))
333
+ return raw;
334
+ console.log(` please answer with one of: ${choices.join(', ')}`);
335
+ }
336
+ }
337
+ async function yesNo(rl, label, def) {
338
+ const hint = def ? 'Y/n' : 'y/N';
339
+ while (true) {
340
+ const raw = (await rl.question(`? ${label} (${hint}): `)).trim().toLowerCase();
341
+ if (!raw)
342
+ return def;
343
+ if (['y', 'yes'].includes(raw))
344
+ return true;
345
+ if (['n', 'no'].includes(raw))
346
+ return false;
347
+ console.log(' please answer y or n');
348
+ }
349
+ }
350
+ async function askAvdChoice(rl, avds) {
351
+ const placeholder = avds.length + 1;
352
+ console.log(' Detected AVDs:');
353
+ avds.forEach((a, i) => console.log(` ${i + 1}) ${a}`));
354
+ console.log(` ${placeholder}) leave placeholder (edit the config later)`);
355
+ while (true) {
356
+ const raw = (await ask(rl, `Which AVD should the config target? (1-${placeholder})`, '1')).trim();
357
+ const n = Number(raw);
358
+ if (Number.isInteger(n) && n >= 1 && n <= avds.length)
359
+ return avds[n - 1];
360
+ if (n === placeholder)
361
+ return undefined;
362
+ console.log(` please enter a number 1-${placeholder}`);
363
+ }
364
+ }
365
+ async function isNonEmpty(dir) {
366
+ try {
367
+ const entries = await readdir(dir);
368
+ return entries.length > 0;
369
+ }
370
+ catch {
371
+ return false;
372
+ }
373
+ }
374
+ const RESERVED_DIR_NAMES = new Set([
375
+ 'con',
376
+ 'prn',
377
+ 'aux',
378
+ 'nul',
379
+ 'com1',
380
+ 'com2',
381
+ 'com3',
382
+ 'com4',
383
+ 'com5',
384
+ 'com6',
385
+ 'com7',
386
+ 'com8',
387
+ 'com9',
388
+ 'lpt1',
389
+ 'lpt2',
390
+ 'lpt3',
391
+ 'lpt4',
392
+ 'lpt5',
393
+ 'lpt6',
394
+ 'lpt7',
395
+ 'lpt8',
396
+ 'lpt9',
397
+ 'app',
398
+ 'node_modules',
399
+ 'playwright-report',
400
+ 'dist',
401
+ ]);
402
+ const TEST_DIR_HINT = 'use letters, digits, dot, dash or underscore (no spaces/special chars), ' +
403
+ 'and avoid reserved names like "app" or "node_modules"';
404
+ export function isReservedDirName(name) {
405
+ return RESERVED_DIR_NAMES.has(name.toLowerCase());
406
+ }
407
+ export function platformChoices(isMac) {
408
+ return isMac ? ['android', 'ios', 'both'] : ['android'];
409
+ }
410
+ export function platformSupportError(isMac, platforms) {
411
+ if (!isMac && platforms.includes('ios')) {
412
+ return 'iOS testing requires macOS (Xcode + simulators). On Windows/Linux, use --platform android';
413
+ }
414
+ return null;
415
+ }
416
+ function printAndroidToolchainStatus(tc) {
417
+ const mark = (ok) => (ok ? 'βœ“' : 'βœ—');
418
+ const jv = tc.jdkVersion ? ` v${tc.jdkVersion}` : '';
419
+ const jdkLine = tc.jdk === 'ok'
420
+ ? `βœ“ JDK (java${jv})`
421
+ : tc.jdk === 'too-old'
422
+ ? `⚠ JDK${jv} β€” too old, need 17+`
423
+ : tc.jdk === 'unknown'
424
+ ? '⚠ JDK (java) β€” version unreadable'
425
+ : 'βœ— JDK (java) β€” not found';
426
+ const av = tc.appiumVersion ? ` (v${tc.appiumVersion})` : '';
427
+ const appiumLine = tc.appium === 'recommended'
428
+ ? `βœ“ Appium 3.x${av}`
429
+ : tc.appium === 'best-effort'
430
+ ? `⚠ Appium 2.x${av} β€” best-effort, not the supported version`
431
+ : tc.appium === 'unsupported'
432
+ ? `βœ— Appium${av} β€” unsupported version`
433
+ : 'βœ— Appium β€” not found';
434
+ console.log('\nAndroid toolchain:');
435
+ console.log(` ${jdkLine}`);
436
+ console.log(` ${mark(tc.sdk)} Android SDK (adb)`);
437
+ console.log(` ${appiumLine}`);
438
+ console.log(` ${mark(tc.uiautomator2)} uiautomator2 driver`);
439
+ console.log(tc.avd
440
+ ? ` βœ“ Android emulator (AVD): ${tc.avdNames.join(', ')}`
441
+ : ' βœ— Android emulator (AVD) β€” none');
442
+ }
443
+ function projectLocationError(targetDir) {
444
+ const name = basename(targetDir);
445
+ if (isReservedDirName(name)) {
446
+ return `"${name}" is a reserved name β€” choose a different project folder`;
447
+ }
448
+ const exists = existsSync(targetDir);
449
+ const t = projectTargetError(exists, exists && statSync(targetDir).isDirectory());
450
+ if (t)
451
+ return `"${targetDir}" ${t}`;
452
+ return null;
453
+ }
454
+ async function askProjectDir(rl, argDir) {
455
+ while (true) {
456
+ const dirInput = argDir ?? (await ask(rl, 'Project location', './taqwright-tests'));
457
+ const targetDir = resolve(process.cwd(), dirInput);
458
+ const err = projectLocationError(targetDir);
459
+ if (!err)
460
+ return targetDir;
461
+ if (argDir !== undefined) {
462
+ console.error(`error: ${err}.`);
463
+ process.exit(1);
464
+ }
465
+ console.log(` ${err} β€” try another.`);
466
+ }
467
+ }
468
+ export function isValidTestDir(name) {
469
+ if (!name)
470
+ return false;
471
+ if (!/^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(name))
472
+ return false;
473
+ if (isReservedDirName(name))
474
+ return false;
475
+ return true;
476
+ }
477
+ async function askTestDir(rl) {
478
+ while (true) {
479
+ const name = await ask(rl, 'Test folder name', 'tests');
480
+ if (isValidTestDir(name))
481
+ return name;
482
+ console.log(` please ${TEST_DIR_HINT}`);
483
+ }
484
+ }
485
+ export function projectTargetError(exists, isDirectory) {
486
+ if (exists && !isDirectory)
487
+ return 'exists and is not a directory';
488
+ return null;
489
+ }
490
+ export function toPackageName(raw) {
491
+ const cleaned = raw
492
+ .toLowerCase()
493
+ .replace(/[^a-z0-9._-]+/g, '-')
494
+ .replace(/^[._-]+/, '')
495
+ .replace(/-+$/, '');
496
+ return cleaned || 'taqwright-tests';
497
+ }
498
+ function runNpm(args, cwd) {
499
+ return new Promise((resolve_) => {
500
+ const child = spawnTool('npm', args, { cwd, stdio: 'inherit' });
501
+ child.on('exit', (code, signal) => {
502
+ if (signal)
503
+ resolve_(128);
504
+ else
505
+ resolve_(code ?? 0);
506
+ });
507
+ child.on('error', (err) => {
508
+ console.error(`failed to spawn npm ${args.join(' ')}:`, err.message);
509
+ resolve_(1);
510
+ });
511
+ });
512
+ }
513
+ async function isTaqwrightGloballyLinked() {
514
+ try {
515
+ const { stdout } = await execP('npm root -g');
516
+ return existsSync(join(stdout.trim(), '@taqwright', 'taqwright'));
517
+ }
518
+ catch {
519
+ return false;
520
+ }
521
+ }
522
+ function packageJsonTemplate(name) {
523
+ const obj = {
524
+ name,
525
+ private: true,
526
+ version: '0.0.1',
527
+ type: 'module',
528
+ scripts: {
529
+ test: 'taqwright test',
530
+ codegen: 'taqwright codegen',
531
+ doctor: 'taqwright doctor',
532
+ devices: 'taqwright devices',
533
+ report: 'taqwright show-report',
534
+ },
535
+ devDependencies: {
536
+ '@taqwright/taqwright': 'git+ssh://git@github.com/taqelah/taqwright.git',
537
+ '@types/node': '^24.0.0',
538
+ typescript: '^5.4.0',
539
+ },
540
+ engines: {
541
+ node: '>=24.0.0 <26.0.0',
542
+ },
543
+ overrides: {
544
+ '@wdio/config': {
545
+ glob: '^13',
546
+ },
547
+ },
548
+ };
549
+ return JSON.stringify(obj, null, 2) + '\n';
550
+ }
551
+ function tsconfigTemplate(testDir) {
552
+ const obj = {
553
+ compilerOptions: {
554
+ target: 'ES2022',
555
+ module: 'NodeNext',
556
+ moduleResolution: 'NodeNext',
557
+ esModuleInterop: true,
558
+ strict: true,
559
+ skipLibCheck: true,
560
+ resolveJsonModule: true,
561
+ types: ['node'],
562
+ },
563
+ include: [`${testDir}/**/*`, 'taqwright.config.ts'],
564
+ };
565
+ return JSON.stringify(obj, null, 2) + '\n';
566
+ }
567
+ export function configTemplate(platforms, testDir, opts) {
568
+ const projects = platforms
569
+ .map((p) => projectBlock(p, {
570
+ demoApp: opts.demoApp,
571
+ demoAvd: opts.demoAvd,
572
+ deviceName: opts.deviceName,
573
+ scopedTestMatch: opts.scoped ? `'**/${p}/**'` : undefined,
574
+ }))
575
+ .join(',\n');
576
+ return `import { defineConfig, Platform } from '@taqwright/taqwright';
577
+
578
+ // Every config knob is listed here. Essentials are uncommented; everything
579
+ // else is a commented placeholder you can enable by removing the leading
580
+ // "// ". Hover any field in your editor for the full type docs.
581
+ export default defineConfig({
582
+ testDir: './${testDir}',
583
+ timeout: 60_000,
584
+ expectTimeout: 30_000,
585
+ // 'html' writes playwright-report/ β€” view it with: npx taqwright show-report
586
+ reporter: [['list'], ['html', { open: 'never', title: 'Taqwright Test Report' }]],
587
+
588
+ // ─── Optional top-level overrides ─────────────────────────────────
589
+ // retries: 1,
590
+ // outputDir: './test-results',
591
+ // fullyParallel: false,
592
+ // forbidOnly: !!process.env.CI,
593
+ // testMatch: ['**/*.spec.ts'],
594
+ // testIgnore: ['**/wip/**'],
595
+ // globalSetup: './setup.ts',
596
+ // globalTeardown: './teardown.ts',
597
+
598
+ projects: [
599
+ ${projects},
600
+
601
+ // ─── Cloud examples (BrowserStack / LambdaTest) ─────────────────
602
+ // Uncomment a block below to add a cloud project. Set the matching
603
+ // env vars before launching:
604
+ // BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY
605
+ // LAMBDATEST_USERNAME / LAMBDATEST_ACCESS_KEY
606
+ // For now, cloud devices are wired through the inspector
607
+ // ('taqwright inspect'); cloud test-runner support lands separately.
608
+ //
609
+ // {
610
+ // name: 'browserstack',
611
+ // use: {
612
+ // platform: Platform.ANDROID,
613
+ // device: {
614
+ // provider: 'browserstack',
615
+ // name: 'Google Pixel 8',
616
+ // osVersion: '14.0',
617
+ // orientation: 'portrait',
618
+ // },
619
+ // resetBetweenTests: true,
620
+ // buildPath: 'bs://<app-id-from-app-upload>',
621
+ // appBundleId: 'com.example.app',
622
+ // },
623
+ // },
624
+ // {
625
+ // name: 'lambdatest',
626
+ // use: {
627
+ // platform: Platform.IOS,
628
+ // device: {
629
+ // provider: 'lambdatest',
630
+ // name: 'iPhone 15',
631
+ // osVersion: '17',
632
+ // },
633
+ // resetBetweenTests: true,
634
+ // buildPath: 'lt://<app-id-from-app-upload>',
635
+ // appBundleId: 'com.example.MyApp',
636
+ // },
637
+ // },
638
+ ],
639
+ });
640
+ `;
641
+ }
642
+ function projectBlock(p, opts) {
643
+ const { demoApp, demoAvd, deviceName, scopedTestMatch } = opts;
644
+ const isAndroid = p === 'android';
645
+ const demoWired = isAndroid && demoApp;
646
+ const demoAvdWired = isAndroid && demoAvd;
647
+ const platformConst = isAndroid ? 'Platform.ANDROID' : 'Platform.IOS';
648
+ const projectName = isAndroid ? 'android' : 'ios';
649
+ const deviceNameLine = demoAvdWired
650
+ ? ` name: '${DEMO_AVD_NAME}', // AVD from \`taqwright install --with-avd\``
651
+ : isAndroid && deviceName
652
+ ? ` name: '${deviceName}', // your detected AVD`
653
+ : demoWired
654
+ ? ' // name: /Pixel/, // no managed AVD β€” bring a running device, or `taqwright install --with-avd`'
655
+ : isAndroid
656
+ ? ' // name: /Pixel/,'
657
+ : ' name: /iPhone/,';
658
+ const autoStartDeviceLine = demoAvdWired
659
+ ? ` autoStartDevice: true, // cold-boots the ${DEMO_AVD_NAME} AVD`
660
+ : isAndroid && deviceName
661
+ ? ` autoStartDevice: true, // cold-boots the ${deviceName} AVD`
662
+ : ' // autoStartDevice: true,';
663
+ const exampleUdid = isAndroid ? "'emulator-5554'" : "'00000000-0000-0000-0000-000000000000'";
664
+ const exampleOsVersion = isAndroid ? "'14'" : "'17'";
665
+ const examplePath = isAndroid ? "'/absolute/path/to/app.apk'" : "'/absolute/path/to/MyApp.app'";
666
+ const exampleBundleId = isAndroid ? "'com.example.app'" : "'com.example.MyApp'";
667
+ const testMatchLine = scopedTestMatch
668
+ ? ` testMatch: [${scopedTestMatch}],`
669
+ : ` // testMatch: ['**/${projectName}/*.spec.ts'],`;
670
+ const resetBlock = isAndroid && demoApp
671
+ ? ` // ─── Reset between tests ────────────────────────────────────
672
+ // Bound to the bundled demo app (app/${DEMO_APK_FILENAME}).
673
+ // resetBetweenTests reinstalls + relaunches it fresh before every
674
+ // test, so each starts from a known state. All three are
675
+ // type-required together.
676
+ resetBetweenTests: true,
677
+ buildPath: './app/${DEMO_APK_FILENAME}',
678
+ appBundleId: '${DEMO_APP_BUNDLE_ID}',`
679
+ : ` // ─── Reset between tests ────────────────────────────────────
680
+ // Uncomment all three lines below to terminate β†’ uninstall β†’
681
+ // reinstall β†’ relaunch the app before every test. Required if
682
+ // you want each test to start from a known state. The TS type
683
+ // for use enforces all three together.
684
+ //
685
+ // resetBetweenTests: true,
686
+ // buildPath: ${examplePath},
687
+ // appBundleId: ${exampleBundleId},`;
688
+ return ` {
689
+ name: '${projectName}',
690
+ use: {
691
+ platform: ${platformConst},
692
+ device: {
693
+ provider: 'emulator',
694
+ ${deviceNameLine}
695
+ // osVersion: ${exampleOsVersion},
696
+ // udid: ${exampleUdid},
697
+ // orientation: 'portrait',
698
+ //
699
+ // ─── Parallel runs (optional) ────────────────────────────
700
+ // Declare a pool of devices to fan tests out across, then
701
+ // bump \`workers\` at the top of this config to match. Worker
702
+ // N picks pool[N]; \`workers > pool.length\` fails fast. Each
703
+ // worker gets its own Appium + driver ports auto-staggered.
704
+ // pool: [
705
+ // { udid: 'emulator-5554', name: 'Pixel_7_API_34' },
706
+ // { udid: 'emulator-5556', name: 'Pixel_7_API_34_2' },
707
+ // { udid: 'emulator-5558', name: 'Pixel_7_API_34_3' },
708
+ // ],
709
+ //
710
+ // Or skip the pool entirely and let taqwright discover local
711
+ // devices and partition them across \`workers\` for you β€” it
712
+ // cold-boots shutdown AVDs/simulators to reach the count and
713
+ // fails fast if too few are available. Mutually exclusive with
714
+ // \`pool\` / \`udid\`.
715
+ // autoDiscover: true,
716
+ },
717
+ // Spawn \`npx appium\` automatically when nothing is listening on
718
+ // the configured host:port. Set \`autoStart: false\` to manage
719
+ // Appium yourself (e.g. an Appium server you start by hand).
720
+ appium: {
721
+ autoStart: true,
722
+ // Boot an offline Android emulator automatically. Needs a
723
+ // string device.name equal to the AVD id (e.g. 'Pixel_7_API_34',
724
+ // see 'emulator -list-avds'); a RegExp name is rejected at
725
+ // config load. iOS simulators boot via XCUITest regardless.
726
+ ${autoStartDeviceLine}
727
+ host: 'localhost',
728
+ port: 4723, // Appium 3 default
729
+ path: '/', // Appium 3 default (Appium 1.x used '/wd/hub')
730
+ // newCommandTimeout: 240,
731
+ // logLevel: 'warn',
732
+ },
733
+
734
+ ${resetBlock}
735
+
736
+ // ─── Extra capabilities (escape hatch) ──────────────────────
737
+ // Anything Appium accepts; merged on top of the auto-built caps.
738
+ // capabilities: {
739
+ // 'appium:autoGrantPermissions': true,
740
+ // 'appium:autoAcceptAlerts': true,
741
+ // },
742
+
743
+ // ─── Per-project locator-action timeout (ms) ────────────────
744
+ // Overrides the top-level \`expectTimeout\` for this project only.
745
+ // expectTimeout: 30_000,
746
+
747
+ // ─── Trace artifact ─────────────────────────────────────────
748
+ // Captures a per-action screenshot + page-source timeline as a
749
+ // self-contained \`trace.html\` under the test's output dir, also
750
+ // attached to the Playwright HTML report. Adds one screenshot +
751
+ // page-source round-trip per action (~100–300ms local, more
752
+ // over USB) β€” recommended for CI: 'on-failure'.
753
+ // 'off' β€” no overhead (default)
754
+ // 'on' β€” every test
755
+ // 'on-failure' β€” only failed tests
756
+ // 'retain-on-failure' β€” alias of 'on-failure' on mobile
757
+ // trace: 'on-failure',
758
+
759
+ // ─── Screen recording (video) ───────────────────────────────
760
+ // Records the device screen via Appium for the whole run and
761
+ // attaches a screen.mp4 to the Playwright HTML report (as
762
+ // 'taqwright-video'). No per-action cost like trace, but every
763
+ // run pays the device recorder + an mp4 transfer at teardown β€”
764
+ // recommended for CI: 'on-failure'. iOS-simulator support varies.
765
+ // 'off' β€” no recording (default)
766
+ // 'on' β€” every test
767
+ // 'on-failure' β€” only failed tests
768
+ // 'retain-on-failure' β€” alias of 'on-failure' on mobile
769
+ // video: 'on-failure',
770
+
771
+ // ─── Network capture (HAR) ──────────────────────────────────
772
+ // Routes app traffic through a local MITM proxy and attaches a
773
+ // HAR 1.2 file to the Playwright HTML report (as 'taqwright-har').
774
+ // Zero-touch on userdebug Android emulators and iOS Simulators β€”
775
+ // taqwright generates its own CA, installs it on the device, sets
776
+ // the device/host proxy, and tears everything down on teardown
777
+ // (including crash paths). Cloud projects skip this (the hub
778
+ // captures HAR server-side); real devices and Google Play AVDs
779
+ // are skipped with a note in the artifact.
780
+ // 'off' β€” no capture (default)
781
+ // 'on' β€” every test
782
+ // 'on-failure' β€” only failed tests
783
+ // 'retain-on-failure' β€” alias of 'on-failure' on mobile
784
+ // network: 'on-failure',
785
+ },
786
+
787
+ // ─── Per-project test-runner overrides ────────────────────────
788
+ // timeout: 90_000,
789
+ // retries: 2,
790
+ // grep: /smoke/,
791
+ // grepInvert: /flaky/,
792
+ // dependencies: ['setup'],
793
+ ${testMatchLine}
794
+ }`;
795
+ }
796
+ export function exampleTestTemplate(demoApp) {
797
+ if (demoApp) {
798
+ return `import { test, expect } from '@taqwright/taqwright';
799
+
800
+ // ─── Example tests (demo app) ────────────────────────────────────
801
+ // Run against the bundled demo app (app/${DEMO_APK_FILENAME}). The
802
+ // config sets resetBetweenTests:true, so taqwright reinstalls +
803
+ // relaunches it fresh before each test β€” every test starts at the
804
+ // login screen. \`npx taqwright test\` should pass once a device /
805
+ // emulator is up. (Android selectors β€” the demo app is an APK.)
806
+ test('user can log in to the demo app', async ({ mobile }) => {
807
+ await mobile.getByXpath("//*[@hint='Username']").fill('emma@demoapp.com');
808
+ await mobile.getByXpath("//*[@hint='Password']").fill('10203040');
809
+ await mobile.getByUiSelector('new UiSelector().description("Login")').click();
810
+ await expect(mobile.getByUiSelector('new UiSelector().description("View All")')).toBeVisible();
811
+ });
812
+
813
+ test('login fails with invalid username & password', async ({ mobile }) => {
814
+ await mobile.getByXpath("//*[@hint='Username']").fill('invalidusername');
815
+ await mobile.getByXpath("//*[@hint='Password']").fill('invalidpassword');
816
+ await mobile.getByUiSelector('new UiSelector().description("Login")').click();
817
+ await expect(mobile.getByXpath("//*[contains(@content-desc, 'Invalid username or password.')]")).toBeVisible();
818
+ });
819
+
820
+ test('login is blocked without username & password', async ({ mobile }) => {
821
+ await mobile.getByUiSelector('new UiSelector().description("Login")').click();
822
+ await expect(mobile.getByUiSelector('new UiSelector().description("Please enter your username")')).toBeVisible();
823
+ await expect(mobile.getByUiSelector('new UiSelector().description("Please enter your password")')).toBeVisible();
824
+ });
825
+ `;
826
+ }
827
+ return `import { test, expect } from '@taqwright/taqwright';
828
+
829
+ // ─── First test ──────────────────────────────────────────────────
830
+ // Runs without an app installed β€” just confirms the device + Appium
831
+ // stack is wired up. Fill in your own \`buildPath\` + \`appBundleId\`
832
+ // in taqwright.config.ts then write a real test below.
833
+ test('screen has positive dimensions', async ({ mobile }) => {
834
+ const size = await mobile.getScreenSize();
835
+ expect(size.width).toBeGreaterThan(0);
836
+ expect(size.height).toBeGreaterThan(0);
837
+ });
838
+
839
+ // ─── Realistic-shape example (commented) ─────────────────────────
840
+ // Uncomment after pointing the config at your app. Showcases the
841
+ // idiomatic taqwright surface:
842
+ //
843
+ // * Locator entry points: getById / getByText / getByLabel / getByRole / ...
844
+ // * Chain methods: .first() / .nth(i) / .filter({ hasText }) / .locator(child) / .all()
845
+ // * Auto-retrying matchers (Playwright-style) on a Locator:
846
+ // await expect(loc).toBeVisible() / .toHaveText() / .toBeChecked() / .toHaveCount(n) / ...
847
+ // * Plain \`expect(value)\` (no await) for numbers, strings, arrays.
848
+ //
849
+ // test('login flow', async ({ mobile }) => {
850
+ // await mobile.getById('Username').fill('demo@example.com');
851
+ // await mobile.getById('Password').fill('hunter2');
852
+ // await mobile.getByRole('button', { name: 'Sign in' }).click();
853
+ //
854
+ // // Auto-waits up to expectTimeout for the heading to appear.
855
+ // await expect(mobile.getByText('Welcome')).toBeVisible();
856
+ //
857
+ // // Chain β€” disambiguate the 3rd row in a repeating list.
858
+ // await mobile.getByType('XCUIElementTypeCell').nth(2).click();
859
+ //
860
+ // // Filter β€” pick the Wi-Fi row by its label, then tap its switch.
861
+ // await mobile.getByType('android.widget.LinearLayout')
862
+ // .filter({ hasText: 'Wi-Fi' })
863
+ // .locator(mobile.getByType('android.widget.Switch'))
864
+ // .check();
865
+ //
866
+ // // Plain-value expect β€” for non-Locator data.
867
+ // const items = await mobile.getByType('CartItem').all();
868
+ // expect(items).toHaveLength(3);
869
+ // });
870
+
871
+ // ─── Pause for interactive debugging (commented) ─────────────────
872
+ // Drop this anywhere in a test to hand off to the inspector. The
873
+ // in-flight WebDriver session is attached (no new Appium boot), the
874
+ // inspector opens in your browser, and the test resumes when you
875
+ // click "Resume" in the UI. Set \`PWDEBUG=0\` in CI to make it a
876
+ // no-op without removing the call.
877
+ //
878
+ // test('paused for inspection', async ({ mobile }) => {
879
+ // await mobile.getById('Login').click();
880
+ // await mobile.pause(); // ← browser opens; click around; click Resume
881
+ // await expect(mobile.getByText('Dashboard')).toBeVisible();
882
+ // });
883
+ `;
884
+ }
885
+ function gitignoreTemplate() {
886
+ return `node_modules
887
+ dist
888
+ test-results
889
+ playwright-report
890
+ .DS_Store
891
+ *.log
892
+ `;
893
+ }
894
+ function npmrcTemplate() {
895
+ return `# Active path today: @taqwright/taqwright installs over git+ssh β€” no token needed.
896
+ # Future (once published to GitHub Packages), uncomment and supply a token:
897
+ # @taqwright:registry=https://npm.pkg.github.com
898
+ # Auth needs a Personal Access Token with read:packages. Don't commit it β€”
899
+ # put it in your user ~/.npmrc or a CI env var, e.g.:
900
+ # //npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}
901
+ `;
902
+ }