@capacitor/cli 9.0.0-alpha.8 → 9.0.0-beta.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.
@@ -12,10 +12,14 @@ const errors_1 = require("../errors");
12
12
  const common_2 = require("../ios/common");
13
13
  const log_1 = require("../log");
14
14
  const fs_1 = require("../util/fs");
15
+ const npm_1 = require("../util/npm");
16
+ const spm_1 = require("../util/spm");
15
17
  const subprocess_1 = require("../util/subprocess");
16
18
  const template_1 = require("../util/template");
19
+ const migrate_debug_xcconfig_1 = require("./migrate-debug-xcconfig");
17
20
  const migrate_edge_to_edge_1 = require("./migrate-edge-to-edge");
18
21
  const migrate_swiftui_1 = require("./migrate-swiftui");
22
+ const migrate_to_modern_project_1 = require("./migrate-to-modern-project");
19
23
  let allDependencies = {};
20
24
  const libs = ['@capacitor/core', '@capacitor/cli', '@capacitor/ios', '@capacitor/android'];
21
25
  const plugins = [
@@ -47,9 +51,10 @@ const plugins = [
47
51
  ];
48
52
  const coreVersion = 'next';
49
53
  const pluginVersion = 'next';
50
- const gradleVersion = '9.5.1';
54
+ const gradleVersion = '9.8.2';
51
55
  const iOSVersion = '16';
52
56
  let installFailed = false;
57
+ let scriptsBlocked = false;
53
58
  // AGP 9 changed the defaults of these properties and Android Studio's AGP Upgrade Assistant
54
59
  // writes them into gradle.properties as compatibility shims. Capacitor apps need none of them,
55
60
  // most are deprecated and will be removed in AGP 10, and some (android.builtInKotlin,
@@ -135,6 +140,13 @@ async function migrateCommand(config, noprompt, packagemanager) {
135
140
  log_1.logger.error(`${installerType} install failed. Try deleting node_modules folder and running ${colors_1.default.input(`${installerType} install --force`)} manually.`);
136
141
  installFailed = true;
137
142
  }
143
+ if (runNpmInstall && !installFailed) {
144
+ const blocked = await (0, npm_1.getBlockedInstallScripts)(config.app.rootDir, installerType);
145
+ if (blocked.length > 0) {
146
+ (0, npm_1.logBlockedInstallScripts)(blocked);
147
+ scriptsBlocked = (0, npm_1.hasBlockedCapacitorScripts)(blocked);
148
+ }
149
+ }
138
150
  // Update iOS Projects
139
151
  if (allDependencies['@capacitor/ios'] && (0, fs_extra_1.existsSync)(config.ios.platformDirAbs)) {
140
152
  const currentiOSVersion = (0, common_2.getMajoriOSVersion)(config);
@@ -156,7 +168,18 @@ async function migrateCommand(config, noprompt, packagemanager) {
156
168
  await (0, common_1.runTask)(`Migrating AppDelegate.swift`, () => {
157
169
  return updateAppDelegate((0, path_1.join)(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'));
158
170
  });
171
+ await (0, common_1.runTask)(`Removing debug.xcconfig.`, () => {
172
+ return (0, migrate_debug_xcconfig_1.removeDebugXcconfig)(config);
173
+ });
174
+ await (0, common_1.runTask)(`Migrating to recommended project settings`, () => {
175
+ return (0, migrate_to_modern_project_1.migrateToModernProject)(config);
176
+ });
159
177
  await (0, migrate_swiftui_1.migrateToSwiftUI)(config);
178
+ if ((await config.ios.packageManager) === 'SPM') {
179
+ await (0, common_1.runTask)('Ensuring Package.resolved is in CapApp-SPM/.gitignore.', () => {
180
+ return (0, spm_1.ensureSPMGitignoreEntry)(config);
181
+ });
182
+ }
160
183
  }
161
184
  if (!installFailed) {
162
185
  await (0, common_1.runTask)(`Running cap sync.`, () => {
@@ -242,6 +265,9 @@ async function migrateCommand(config, noprompt, packagemanager) {
242
265
  return;
243
266
  }
244
267
  txt = txt.replace(/= {2}'/g, `= '`);
268
+ if (txt.includes('androidxCoreKTXVersion')) {
269
+ log_1.logger.warn(`androidxCoreKTXVersion might not be needed anymore, check your plugins`);
270
+ }
245
271
  (0, fs_extra_1.writeFileSync)(variablesPath, txt, { encoding: 'utf-8' });
246
272
  for (const variable of Object.keys(variablesAndClasspaths.variables)) {
247
273
  let replaceStart = `${variable} = '`;
@@ -272,6 +298,7 @@ async function migrateCommand(config, noprompt, packagemanager) {
272
298
  firebaseMessagingVersion: '25.0.1',
273
299
  playServicesLocationVersion: '21.4.0',
274
300
  androidxBrowserVersion: '1.10.0',
301
+ androidxCoreKTXVersion: '1.19.1',
275
302
  androidxMaterialVersion: '1.14.0',
276
303
  androidxExifInterfaceVersion: '1.4.2',
277
304
  googleMapsPlayServicesVersion: '20.0.0',
@@ -292,9 +319,12 @@ async function migrateCommand(config, noprompt, packagemanager) {
292
319
  await (0, common_1.runTask)(`Writing breaking changes.`, () => {
293
320
  return writeBreakingChanges();
294
321
  });
295
- if (!installFailed) {
322
+ if (!installFailed && !scriptsBlocked) {
296
323
  (0, log_1.logSuccess)(`Migration to Capacitor ${coreVersion} is complete. Run and test your app!`);
297
324
  }
325
+ else if (scriptsBlocked) {
326
+ log_1.logger.warn(`Migration to Capacitor ${coreVersion} is incomplete because npm blocked install scripts. Approve them as described above, then run ${colors_1.default.input('npx cap sync')}.`);
327
+ }
298
328
  else {
299
329
  log_1.logger.warn(`Migration to Capacitor ${coreVersion} is incomplete. Check the log messages for more information.`);
300
330
  }
@@ -475,6 +505,13 @@ async function updateAppBuildGradle(filename) {
475
505
  }
476
506
  let replaced = txt;
477
507
  replaced = replaced.replace(`proguard-android.txt`, `proguard-android-optimize.txt`);
508
+ const releaseMinify = /(\brelease\s*\{[^}]*?minifyEnabled\s+)(true|false)/;
509
+ if (releaseMinify.test(replaced)) {
510
+ replaced = replaced.replace(releaseMinify, '$1true');
511
+ }
512
+ else {
513
+ log_1.logger.warn(`Unable to set minifyEnabled true in the release block of ${filename}. Set it manually to turn on R8.`);
514
+ }
478
515
  replaced = replaced.replace(' targetSdkVersion rootProject.ext.targetSdkVersion\n', '');
479
516
  replaced = replaced.replace(` implementation project(':capacitor-cordova-android-plugins')\n`, '');
480
517
  replaced = replaced.replace(`repositories {
@@ -649,5 +686,6 @@ exports.__testables = {
649
686
  removeAGPMigrationProperties,
650
687
  removeJCenter,
651
688
  removeJCenterFromGradleFile,
689
+ updateAppBuildGradle,
652
690
  updateGradleProperties,
653
691
  };
package/dist/tasks/run.js CHANGED
@@ -67,15 +67,17 @@ async function runCommand(config, selectedPlatformName, options) {
67
67
  }
68
68
  return;
69
69
  }
70
+ let cordovaPlugins;
70
71
  try {
71
72
  if (options.sync) {
72
73
  await (0, sync_1.sync)(config, platformName, false, true);
73
74
  }
74
- const cordovaPlugins = await (0, cordova_1.getCordovaPlugins)(config, platformName);
75
+ const plugins = await (0, cordova_1.getCordovaPlugins)(config, platformName);
76
+ cordovaPlugins = plugins;
75
77
  if (options.url) {
76
78
  await livereload_1.CapLiveReloadHelper.editCapConfigForLiveReload(config, platformName, options);
77
79
  if (platformName === config.android.name) {
78
- await await (0, cordova_1.writeCordovaAndroidManifest)(cordovaPlugins, config, platformName, true);
80
+ await (0, cordova_1.writeCordovaAndroidManifest)(plugins, config, platformName, true);
79
81
  }
80
82
  }
81
83
  await run(config, platformName, options);
@@ -84,7 +86,7 @@ async function runCommand(config, selectedPlatformName, options) {
84
86
  .then(async () => {
85
87
  await livereload_1.CapLiveReloadHelper.revertCapConfigForLiveReload();
86
88
  if (platformName === config.android.name) {
87
- await (0, cordova_1.writeCordovaAndroidManifest)(cordovaPlugins, config, platformName, false);
89
+ await (0, cordova_1.writeCordovaAndroidManifest)(plugins, config, platformName, false);
88
90
  }
89
91
  })
90
92
  .then(() => process.exit());
@@ -95,6 +97,9 @@ async function runCommand(config, selectedPlatformName, options) {
95
97
  catch (e) {
96
98
  if (options.url) {
97
99
  await livereload_1.CapLiveReloadHelper.revertCapConfigForLiveReload();
100
+ if (cordovaPlugins && platformName === config.android.name) {
101
+ await (0, cordova_1.writeCordovaAndroidManifest)(cordovaPlugins, config, platformName, false).catch((revertError) => log_1.logger.warn(`Could not revert the live reload cleartext configuration: ${revertError}`));
102
+ }
98
103
  }
99
104
  if (!(0, errors_1.isFatal)(e)) {
100
105
  (0, errors_1.fatal)((_a = e.stack) !== null && _a !== void 0 ? _a : e);
@@ -6,6 +6,7 @@ const colors_1 = tslib_1.__importDefault(require("../colors"));
6
6
  const errors_1 = require("../errors");
7
7
  const log_1 = require("../log");
8
8
  const sysconfig_1 = require("../sysconfig");
9
+ const telemetry_1 = require("../telemetry");
9
10
  const THANK_YOU = `\nThank you for helping to make Capacitor better! 💖` +
10
11
  `\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/docs/next/cli/telemetry')}\n`;
11
12
  async function telemetryCommand(onOrOff) {
@@ -24,7 +25,13 @@ async function telemetryCommand(onOrOff) {
24
25
  }
25
26
  }
26
27
  else {
27
- log_1.logger.info(`Telemetry is ${colors_1.default.strong(sysconfig.telemetry ? 'on' : 'off')}`);
28
+ log_1.logger.info(`Telemetry is ${colors_1.default.strong(sysconfig.telemetry === false ? 'off' : 'on')}`);
29
+ }
30
+ // The stored setting is reported above because that is what this command
31
+ // writes, but the environment wins at send time. Saying so avoids the
32
+ // command contradicting what the CLI actually does.
33
+ if ((0, telemetry_1.isTelemetryDisabledByEnv)(process.env)) {
34
+ log_1.logger.info(`Telemetry is disabled for this environment by ${colors_1.default.input('CAP_TELEMETRY_DISABLED')}`);
28
35
  }
29
36
  }
30
37
  exports.telemetryCommand = telemetryCommand;
package/dist/telemetry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getIOSPackageManager = exports.sendMetric = exports.telemetryAction = void 0;
3
+ exports.getIOSPackageManager = exports.getProjectIdentity = exports.runContextFields = exports.isTelemetryEnabled = exports.isTelemetryDisabledByEnv = exports.needsAwaitedDelivery = exports.sendMetric = exports.getTelemetryVersion = exports.getTelemetryArguments = exports.getTelemetryError = exports.getTelemetryOptions = exports.telemetryAction = exports.TELEMETRY_SAFE_CHOICES = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const commander_1 = require("commander");
6
6
  const debug_1 = tslib_1.__importDefault(require("debug"));
@@ -8,6 +8,7 @@ const fs_extra_1 = require("fs-extra");
8
8
  const colors_1 = tslib_1.__importDefault(require("./colors"));
9
9
  const ipc_1 = require("./ipc");
10
10
  const log_1 = require("./log");
11
+ const run_context_1 = require("./run-context");
11
12
  const sysconfig_1 = require("./sysconfig");
12
13
  const subprocess_1 = require("./util/subprocess");
13
14
  const term_1 = require("./util/term");
@@ -15,6 +16,58 @@ const debug = (0, debug_1.default)('capacitor:telemetry');
15
16
  const THANK_YOU = `\nThank you for helping improve Capacitor by sharing anonymous usage data! 💖` +
16
17
  `\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/docs/next/cli/telemetry')}` +
17
18
  `\nYou can disable telemetry at any time by using the ${colors_1.default.input('npx cap telemetry off')} command.`;
19
+ /**
20
+ * The option values that are safe to report, keyed by the option's resolved
21
+ * name (commander's `attributeName()`, so `--signing-type` is `signingType`).
22
+ * This is an allowlist by design: a denylist of password-like names would leak
23
+ * the first sensitive option someone adds.
24
+ *
25
+ * The option declarations in `index.ts` spread these same lists into
26
+ * `.choices()`, so an option cannot offer a choice this does not list.
27
+ */
28
+ exports.TELEMETRY_SAFE_CHOICES = {
29
+ androidreleasetype: ['AAB', 'APK'],
30
+ signingType: ['apksigner', 'jarsigner'],
31
+ xcodeExportMethod: [
32
+ 'app-store-connect',
33
+ 'release-testing',
34
+ 'enterprise',
35
+ 'debugging',
36
+ 'developer-id',
37
+ 'mac-application',
38
+ 'validation',
39
+ ],
40
+ xcodeSigningStyle: ['automatic', 'manual'],
41
+ };
42
+ const telemetrySafeChoices = new Map(Object.entries(exports.TELEMETRY_SAFE_CHOICES).map(([name, choices]) => [name, new Set(choices)]));
43
+ /**
44
+ * Positional argument values that are safe to report. Everything else is
45
+ * dropped, since arguments like `cap init`'s app name are user content.
46
+ */
47
+ const TELEMETRY_ARGUMENT_ALLOWLIST = new Set(['android', 'ios', 'web']);
48
+ /**
49
+ * Upper bound on the reported error, so a failed Gradle or Xcode build cannot
50
+ * ship its entire log as a metric.
51
+ */
52
+ const TELEMETRY_ERROR_MAX_LENGTH = 500;
53
+ /**
54
+ * Shortest value worth scrubbing out of a reported error. An empty string would
55
+ * match between every character, and a one- or two-character option value--a
56
+ * port, a target index--appears throughout unrelated output, so redacting
57
+ * either destroys the error instead of sanitizing it.
58
+ */
59
+ const TELEMETRY_REDACTION_MIN_LENGTH = 3;
60
+ /**
61
+ * The characters a dependency spec may contain to be reported as-is. A version
62
+ * range has none of the `:/@` a URL or path spec needs, so specs such as
63
+ * `git+https://x-access-token:<token>@github.com/acme/plugin.git` and
64
+ * `file:/Users/<name>/dev/plugin` are reported as {@link NON_REGISTRY_VERSION}
65
+ * instead of shipping a credential or a local path. The pnpm and yarn workspace
66
+ * protocols are allowed through as the only exception, since they resolve
67
+ * within the repo and so cannot carry either.
68
+ */
69
+ const TELEMETRY_SAFE_VERSION_PATTERN = /^(?:(?:workspace|catalog):)?[\w.\-+^~><=|* ]*$/;
70
+ const NON_REGISTRY_VERSION = 'non-registry';
18
71
  function telemetryAction(config, action) {
19
72
  return async (...actionArgs) => {
20
73
  const start = new Date();
@@ -22,7 +75,6 @@ function telemetryAction(config, action) {
22
75
  // element or second to last if there are additional options (via `.allowUnknownOption()`)
23
76
  const lastArg = actionArgs[actionArgs.length - 1];
24
77
  const cmd = lastArg instanceof commander_1.Command ? lastArg : actionArgs[actionArgs.length - 2];
25
- const command = getFullCommandName(cmd);
26
78
  let error;
27
79
  try {
28
80
  await action(...actionArgs);
@@ -31,66 +83,263 @@ function telemetryAction(config, action) {
31
83
  error = e;
32
84
  }
33
85
  const end = new Date();
34
- const duration = end.getTime() - start.getTime();
35
- const packages = Object.entries({
36
- ...config.app.package.devDependencies,
37
- ...config.app.package.dependencies,
38
- });
39
- // Only collect packages in the capacitor org:
40
- // https://www.npmjs.com/org/capacitor
41
- const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor/'));
42
- const versions = capacitorPackages.map(([k, v]) => [
43
- `${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`,
44
- v,
45
- ]);
46
- const data = {
47
- app_id: await getAppIdentifier(config),
48
- command,
49
- arguments: cmd.args.join(' '),
50
- options: JSON.stringify(cmd.opts()),
51
- duration,
52
- error: error ? (error.message ? error.message : String(error)) : null,
53
- node_version: process.version,
54
- os: config.cli.os,
55
- ios_package_manager: await getIOSPackageManager(config),
56
- ...Object.fromEntries(versions),
57
- };
58
- debug('metric payload: %O', data);
59
- if ((0, term_1.isInteractive)()) {
60
- let sysconfig = await (0, sysconfig_1.readConfig)();
61
- if (!error && typeof sysconfig.telemetry === 'undefined') {
62
- // Telemetry is opt-out; turn telemetry on then inform the user how to opt-out.
63
- sysconfig = { ...sysconfig, telemetry: true };
64
- await (0, sysconfig_1.writeConfig)(sysconfig);
65
- log_1.output.write(THANK_YOU);
66
- }
67
- await sendMetric(sysconfig, 'capacitor_cli_command', data);
68
- }
86
+ await reportCommand(config, cmd, end.getTime() - start.getTime(), error);
69
87
  if (error) {
70
88
  throw error;
71
89
  }
72
90
  };
73
91
  }
74
92
  exports.telemetryAction = telemetryAction;
93
+ /**
94
+ * Build and deliver the metric for one completed command.
95
+ */
96
+ async function reportCommand(config, cmd, duration, error) {
97
+ // Read before detection and before stored consent, so the opt-out holds in
98
+ // environments with no filesystem to have stored one.
99
+ if (isTelemetryDisabledByEnv(process.env)) {
100
+ debug('CAP_TELEMETRY_DISABLED is set--not collecting');
101
+ return;
102
+ }
103
+ const runContext = (0, run_context_1.detectRunContext)();
104
+ let sysconfig = await (0, sysconfig_1.readConfig)();
105
+ // The notice and the persisted default only mean anything where a person can
106
+ // see them, so a headless run leaves the choice unmade rather than
107
+ // committing one it was never offered.
108
+ if ((0, term_1.isInteractive)() && !error && typeof sysconfig.telemetry === 'undefined') {
109
+ // Telemetry is opt-out; turn telemetry on then inform the user how to opt-out.
110
+ sysconfig = { ...sysconfig, telemetry: true };
111
+ await (0, sysconfig_1.writeConfig)(sysconfig);
112
+ log_1.output.write(THANK_YOU);
113
+ }
114
+ // Settled before the payload is built rather than at delivery: identifying
115
+ // the project walks the whole commit graph.
116
+ if (!isTelemetryEnabled(process.env, sysconfig, runContext)) {
117
+ debug('Telemetry is off for this run--not collecting');
118
+ return;
119
+ }
120
+ const packages = Object.entries({
121
+ ...config.app.package.devDependencies,
122
+ ...config.app.package.dependencies,
123
+ });
124
+ // Only collect packages in the capacitor org:
125
+ // https://www.npmjs.com/org/capacitor
126
+ const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor/'));
127
+ const versions = capacitorPackages.map(([k, v]) => [
128
+ `${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`,
129
+ getTelemetryVersion(v),
130
+ ]);
131
+ // Both identifiers derive from this, and `git rev-list --max-parents=0`
132
+ // walks the whole history — so it is fetched once for the pair.
133
+ const firstCommit = await getFirstCommit(config);
134
+ const options = getTelemetryOptions(cmd);
135
+ const data = {
136
+ app_id: await getAppIdentifier(firstCommit),
137
+ ...(await getProjectIdentity(firstCommit, runContext)),
138
+ command: getFullCommandName(cmd),
139
+ arguments: getTelemetryArguments(cmd),
140
+ options: JSON.stringify(options),
141
+ duration,
142
+ error: getTelemetryError(cmd, error, config, options),
143
+ node_version: process.version,
144
+ os: config.cli.os,
145
+ ios_package_manager: await getIOSPackageManager(config),
146
+ ...runContextFields(runContext),
147
+ ...Object.fromEntries(versions),
148
+ };
149
+ debug('metric payload: %O', data);
150
+ await sendMetric(sysconfig, 'capacitor_cli_command', data, runContext);
151
+ }
152
+ /**
153
+ * Build the reported option map: every option name the command resolved, but
154
+ * only the values that cannot carry a secret. Booleans are kept because they
155
+ * cannot encode one, allowlisted options are kept when the value is one this
156
+ * file lists, and every other value--keystore passwords, paths, signing
157
+ * identities--becomes `null`.
158
+ */
159
+ function getTelemetryOptions(cmd) {
160
+ return Object.fromEntries(Object.entries(cmd.opts()).map(([name, value]) => [name, isTelemetrySafeValue(name, value) ? value : null]));
161
+ }
162
+ exports.getTelemetryOptions = getTelemetryOptions;
163
+ function isTelemetrySafeValue(name, value) {
164
+ var _a, _b;
165
+ if (typeof value === 'boolean') {
166
+ return true;
167
+ }
168
+ return typeof value === 'string' && ((_b = (_a = telemetrySafeChoices.get(name)) === null || _a === void 0 ? void 0 : _a.has(value)) !== null && _b !== void 0 ? _b : false);
169
+ }
170
+ /**
171
+ * The signing credentials `capacitor.config` can supply. `cap build` falls back
172
+ * to the config for each of these when the flag is absent, and the resolved
173
+ * value still reaches the `xcodebuild` and `jarsigner` command lines--so it can
174
+ * come back in their output without ever having appeared in `cmd.opts()`.
175
+ */
176
+ function getConfiguredCredentials(config) {
177
+ return [
178
+ config.android.buildOptions.keystorePath,
179
+ config.android.buildOptions.keystorePassword,
180
+ config.android.buildOptions.keystoreAlias,
181
+ config.android.buildOptions.keystoreAliasPassword,
182
+ config.ios.buildOptions.teamId,
183
+ config.ios.buildOptions.signingCertificate,
184
+ config.ios.buildOptions.provisioningProfile,
185
+ ].filter((value) => typeof value === 'string');
186
+ }
187
+ /**
188
+ * Build the reported error, scrubbed of every option value and positional
189
+ * argument the rest of the payload drops, plus the signing credentials the
190
+ * config can supply in place of a flag, then truncated.
191
+ *
192
+ * Failed subprocesses rethrow the tool's combined output, which echoes back
193
+ * the very values that were redacted out of the option map--`xcodebuild`
194
+ * prints its command-line build settings, `jarsigner` prints the keystore it
195
+ * was pointed at.
196
+ */
197
+ function getTelemetryError(cmd, error, config, options = getTelemetryOptions(cmd)) {
198
+ if (!error) {
199
+ return null;
200
+ }
201
+ const values = cmd.opts();
202
+ const redacted = [
203
+ ...Object.entries(options)
204
+ .filter(([, value]) => value === null)
205
+ .map(([name]) => values[name]),
206
+ ...cmd.args.filter((arg) => !TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)),
207
+ ...getConfiguredCredentials(config),
208
+ ]
209
+ .filter((value) => typeof value === 'string' && value.length >= TELEMETRY_REDACTION_MIN_LENGTH)
210
+ .sort((a, b) => b.length - a.length);
211
+ const message = redacted.reduce((message, value) => message.replaceAll(value, '<redacted>'), error.message ? error.message : String(error));
212
+ return message.slice(0, TELEMETRY_ERROR_MAX_LENGTH);
213
+ }
214
+ exports.getTelemetryError = getTelemetryError;
215
+ /**
216
+ * Build the reported positional arguments, keeping only allowlisted values.
217
+ */
218
+ function getTelemetryArguments(cmd) {
219
+ return cmd.args.filter((arg) => TELEMETRY_ARGUMENT_ALLOWLIST.has(arg)).join(' ');
220
+ }
221
+ exports.getTelemetryArguments = getTelemetryArguments;
222
+ /**
223
+ * Build the reported version of a Capacitor dependency, keeping the spec only
224
+ * when it is a version range. Specs that point somewhere--a git URL, a local
225
+ * path--are reported as being just that, since they can carry a token or a
226
+ * username.
227
+ */
228
+ function getTelemetryVersion(spec) {
229
+ return spec && TELEMETRY_SAFE_VERSION_PATTERN.test(spec) ? spec : NON_REGISTRY_VERSION;
230
+ }
231
+ exports.getTelemetryVersion = getTelemetryVersion;
75
232
  /**
76
233
  * If telemetry is enabled, send a metric via IPC to a forked process for uploading.
77
234
  */
78
- async function sendMetric(sysconfig, name, data) {
79
- if (sysconfig.telemetry && (0, term_1.isInteractive)()) {
80
- const message = {
81
- name,
82
- timestamp: new Date().toISOString(),
83
- session_id: sysconfig.machine,
84
- source: 'capacitor_cli',
85
- value: data,
86
- };
87
- await (0, ipc_1.send)({ type: 'telemetry', data: message });
235
+ async function sendMetric(sysconfig, name, data, runContext) {
236
+ if (!isTelemetryEnabled(process.env, sysconfig, runContext)) {
237
+ debug('Telemetry is off (user choice, unshown notice, opt-out variable, or CI)--not sending metric');
238
+ return;
239
+ }
240
+ const message = {
241
+ name,
242
+ timestamp: new Date().toISOString(),
243
+ session_id: sysconfig.machine,
244
+ source: 'capacitor_cli',
245
+ value: data,
246
+ };
247
+ if (needsAwaitedDelivery(runContext)) {
248
+ await (0, ipc_1.deliver)(message);
88
249
  }
89
250
  else {
90
- debug('Telemetry is off (user choice, non-interactive terminal, or CI)--not sending metric');
251
+ await (0, ipc_1.send)({ type: 'telemetry', data: message });
91
252
  }
92
253
  }
93
254
  exports.sendMetric = sendMetric;
255
+ /**
256
+ * Whether the POST has to finish before the CLI exits.
257
+ *
258
+ * Delivery normally goes to a forked process and costs the command nothing.
259
+ * That process is not `detached` and still has to cold-start Node before it
260
+ * reaches the network, so an environment reclaimed the moment the CLI returns
261
+ * takes it with it mid-request. Anything that outlives the command — a
262
+ * harness on a developer's machine included — detaches like a human run.
263
+ */
264
+ function needsAwaitedDelivery(runContext) {
265
+ return (0, run_context_1.isAgentDriven)(runContext) && runContext.surface === 'cloud-sandbox';
266
+ }
267
+ exports.needsAwaitedDelivery = needsAwaitedDelivery;
268
+ function isTelemetryDisabledByEnv(env) {
269
+ return Boolean(env.CAP_TELEMETRY_DISABLED);
270
+ }
271
+ exports.isTelemetryDisabledByEnv = isTelemetryDisabledByEnv;
272
+ /**
273
+ * Whether a metric may be delivered for this run.
274
+ *
275
+ * An explicit choice always wins. Only the default for an unmade choice
276
+ * depends on the run: the notice is printed at a terminal and nowhere else, so
277
+ * a first run inside a Docker build or a git hook has had no chance to show it
278
+ * and reporting there would put the data ahead of the disclosure. An agent
279
+ * sandbox never reaches that path at all, which is why `undefined` means yes
280
+ * there and no everywhere else.
281
+ */
282
+ function isTelemetryEnabled(env, sysconfig, runContext) {
283
+ if (isTelemetryDisabledByEnv(env)) {
284
+ return false;
285
+ }
286
+ if (!runContext.report) {
287
+ return false;
288
+ }
289
+ if (typeof sysconfig.telemetry === 'boolean') {
290
+ return sysconfig.telemetry;
291
+ }
292
+ return (0, run_context_1.isAgentDriven)(runContext);
293
+ }
294
+ exports.isTelemetryEnabled = isTelemetryEnabled;
295
+ /**
296
+ * Map the run context onto payload fields. Agent keys are omitted rather than
297
+ * nulled when no agent was detected, so "no agent" and "an agent we couldn't
298
+ * name" stay distinguishable downstream.
299
+ */
300
+ function runContextFields(runContext) {
301
+ return {
302
+ surface: runContext.surface,
303
+ confidence: runContext.confidence,
304
+ ...(runContext.agentId ? { agent_id: runContext.agentId } : {}),
305
+ ...(runContext.agentVersion ? { agent_version: runContext.agentVersion } : {}),
306
+ ...(runContext.agentDetectedBy ? { agent_detected_by: runContext.agentDetectedBy } : {}),
307
+ };
308
+ }
309
+ exports.runContextFields = runContextFields;
310
+ /**
311
+ * Identify the project this run belongs to, so repeated runs against one app
312
+ * count once however many machines they came from.
313
+ *
314
+ * The repository's first commit is the same on every machine, which makes it
315
+ * the preferred key — though where there is one this is `app_id` under a
316
+ * different hash, and `project_id_source` is what tells the two apart. The
317
+ * case this exists for is a freshly scaffolded app with no commit yet, where
318
+ * the harness session identifier stands in — hashed here, never transmitted,
319
+ * which is why the run context carries only the variable's name.
320
+ */
321
+ async function getProjectIdentity(firstCommit, runContext, env = process.env) {
322
+ if (firstCommit) {
323
+ return { project_id: await hash(firstCommit), project_id_source: 'repository' };
324
+ }
325
+ const session = runContext.sessionVar ? env[runContext.sessionVar] : undefined;
326
+ if (session) {
327
+ return { project_id: await hash(session), project_id_source: 'session' };
328
+ }
329
+ debug('Could not obtain a project identifier');
330
+ return { project_id: null, project_id_source: 'none' };
331
+ }
332
+ exports.getProjectIdentity = getProjectIdentity;
333
+ async function hash(value) {
334
+ const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
335
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
336
+ }
337
+ async function getFirstCommit(config) {
338
+ const output = await (0, subprocess_1.getCommandOutput)('git', ['rev-list', '--max-parents=0', 'HEAD'], {
339
+ cwd: config.app.rootDir,
340
+ });
341
+ return (output === null || output === void 0 ? void 0 : output.split('\n')[0]) || undefined;
342
+ }
94
343
  /**
95
344
  * Resolve the iOS dependency manager for telemetry. Returns 'unknown' for
96
345
  * non-iOS projects or when detection throws, so callers never have to handle
@@ -110,20 +359,17 @@ async function getIOSPackageManager(config) {
110
359
  }
111
360
  exports.getIOSPackageManager = getIOSPackageManager;
112
361
  /**
113
- * Get a unique anonymous identifier for this app.
362
+ * Get a unique anonymous identifier for this app from the repository's first
363
+ * commit, which should be universally unique.
114
364
  */
115
- async function getAppIdentifier(config) {
116
- const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
117
- // get the first commit hash, which should be universally unique
118
- const output = await (0, subprocess_1.getCommandOutput)('git', ['rev-list', '--max-parents=0', 'HEAD'], { cwd: config.app.rootDir });
119
- const firstLine = output === null || output === void 0 ? void 0 : output.split('\n')[0];
120
- if (!firstLine) {
365
+ async function getAppIdentifier(firstCommit) {
366
+ if (!firstCommit) {
121
367
  debug('Could not obtain unique app identifier');
122
368
  return null;
123
369
  }
370
+ const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
124
371
  // use sha1 to create a one-way hash to anonymize
125
- const id = createHash('sha1').update(firstLine).digest('hex');
126
- return id;
372
+ return createHash('sha1').update(firstCommit).digest('hex');
127
373
  }
128
374
  /**
129
375
  * Walk through the command's parent tree and construct a space-separated name.