@hot-updater/cli-tools 1.2.0-rc.12 → 1.2.0-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -509,12 +509,6 @@ interface CredentialResolver {
509
509
  */
510
510
  dispose(): Promise<void>;
511
511
  }
512
- /**
513
- * A command to print, broken at spaces with ` \` once a line would pass
514
- * 74 columns, each continuation indented two spaces, so a terminal never
515
- * wraps it mid-word and it still pastes as one command.
516
- */
517
- declare const formatCommand: (command: string) => string;
518
512
  /** The credentials Hot Updater issues itself after a sign-in. */
519
513
  type IssuedCredentialKind = "asc-api-key";
520
514
  /** The credentials `secrets put <name> <file>` stores from one file. */
@@ -920,8 +914,11 @@ declare const nonInteractiveReason: ({ env, input }?: {
920
914
  //#region src/interfaceFingerprint.d.ts
921
915
  /**
922
916
  * The engine fingerprint.json records for the interface fingerprint. Change
923
- * it with any change to what is hashed or how, so the new hashes are reported
924
- * as an engine change rather than a native one.
917
+ * it with any change to what is hashed or how that moves every project's
918
+ * hash, so the new hashes are reported as an engine change rather than a
919
+ * native one. The name is part of every digest, so changing it moves them all:
920
+ * tracking an input nothing tracked before keeps the name, moves only the
921
+ * projects that have that input, and is reported as a change to its files.
925
922
  */
926
923
  declare const INTERFACE_FINGERPRINT_ENGINE = "hot-updater/interface@1";
927
924
  /**
@@ -1093,6 +1090,13 @@ declare const loadPlatformConfigs: <TPlatform extends Platform>(platforms: reado
1093
1090
  readonly platform: TPlatform;
1094
1091
  readonly config: ConfigResponse;
1095
1092
  }[]>;
1093
+ /**
1094
+ * The names the config's build credentials reference, such as
1095
+ * `release-keystore`, which builds read from the `secrets` adapter for their
1096
+ * kind: `signing`'s, and each scheme's `signing.ascApiKey` and
1097
+ * `signing.keystore`.
1098
+ */
1099
+ declare const getBuildSecretNames: (config: ConfigResponse) => readonly string[];
1096
1100
  /**
1097
1101
  * The names the config's `secret()`s reference for its adapters, such as
1098
1102
  * `HOT_UPDATER_CLOUDFLARE_API_TOKEN`, in the order the config created
@@ -1196,6 +1200,32 @@ declare const createMigrator: ({ database, plugins }: ToolingSource) => Migrator
1196
1200
  /** The schema file `hot-updater db generate` writes for a definition's database. */
1197
1201
  declare const generateSchema: ({ database, plugins }: ToolingSource, version: Parameters<SchemaGenerator>[0], name?: Parameters<SchemaGenerator>[1]) => ReturnType<SchemaGenerator>;
1198
1202
  //#endregion
1203
+ //#region src/expoSdk.d.ts
1204
+ /**
1205
+ * The lowest Expo SDK Hot Updater supports, the floor of `@hot-updater/expo`'s
1206
+ * `expo` peer range. doctor, the config plugin and the build adapter read it.
1207
+ */
1208
+ declare const MINIMUM_EXPO_SDK = 55;
1209
+ /** The command that moves a project to the minimum Expo SDK. */
1210
+ declare const EXPO_SDK_UPGRADE_COMMAND = "npx expo install expo@^55.0.0 --fix";
1211
+ interface ExpoSdk {
1212
+ /** The SDK's major version. */
1213
+ readonly sdk: number;
1214
+ /** The installed version, or the range's lowest when Expo isn't installed. */
1215
+ readonly version: string;
1216
+ readonly installed: boolean;
1217
+ }
1218
+ /**
1219
+ * A project's Expo SDK: the installed `expo` the project resolves, else the
1220
+ * lowest version its declared range allows. Null without either.
1221
+ */
1222
+ declare const readExpoSdk: (cwd: string) => ExpoSdk | null;
1223
+ /**
1224
+ * Refuses a project whose installed Expo is below the minimum SDK. A project
1225
+ * without Expo installed passes: nothing could build it yet.
1226
+ */
1227
+ declare const assertSupportedExpoSdk: (cwd: string) => void;
1228
+ //#endregion
1199
1229
  //#region src/log.d.ts
1200
1230
  type LogMessage = string | number | null | undefined;
1201
1231
  declare const log: {
@@ -1212,6 +1242,11 @@ type EnvVarValue = string | {
1212
1242
  comment: string;
1213
1243
  value: string;
1214
1244
  };
1245
+ /**
1246
+ * Replaces `filePath` whole or not at all, through a new 0600 file beside it,
1247
+ * so the result is 0600 whatever the mode of the file it replaces.
1248
+ */
1249
+ declare const writeEnvAtomically: (filePath: string, content: string) => Promise<void>;
1215
1250
  declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: string, options?: {
1216
1251
  readonly preserveKeys?: readonly string[];
1217
1252
  readonly removeKeys?: readonly string[];
@@ -1219,10 +1254,29 @@ declare const makeEnv: (newEnvVars: Record<string, EnvVarValue>, filePath?: stri
1219
1254
  readonly expectedContent?: string;
1220
1255
  }) => Promise<string>;
1221
1256
  //#endregion
1257
+ //#region src/mergeEnvFile.d.ts
1258
+ /**
1259
+ * `value` as an env file's value, in the first form `parseLoadedEnv` reads
1260
+ * back exactly: bare, single-quoted on one line, double-quoted with its
1261
+ * newlines as `\n`, in backticks, or single-quoted across lines. Undefined
1262
+ * for a value no form holds, such as one with every quote mark and a newline.
1263
+ */
1264
+ declare const formatEnvValue: (value: string) => string | undefined;
1265
+ /**
1266
+ * `content`, an env file, with `values` set, already as `formatEnvValue`
1267
+ * gives them: a variable it assigns has only that value replaced, the last
1268
+ * assignment when it repeats, which is the one that counts, and a new one
1269
+ * goes at the end. Every other byte stays: other lines, comments, an
1270
+ * `export`, an inline comment. Undefined when `parseLoadedEnv` wouldn't read
1271
+ * exactly that back, so a file this can't follow is never rewritten.
1272
+ */
1273
+ declare const mergeEnvContent: (content: string, values: ReadonlyMap<string, string>) => string | undefined;
1274
+ //#endregion
1222
1275
  //#region src/messageLines.d.ts
1223
1276
  /**
1224
- * The widest line of a message the CLI prints: clack puts it after its
1225
- * 3-column gutter (`│ `), and the whole line stays within 76 columns.
1277
+ * Where the CLI wraps a message's prose, so it reads well after clack's
1278
+ * 3-column gutter. It is a default for readability: a command, a URL, or a
1279
+ * path stays whole, however long.
1226
1280
  */
1227
1281
  declare const MESSAGE_WIDTH = 73;
1228
1282
  /**
@@ -1236,12 +1290,33 @@ declare const displayWidth: (text: string) => number;
1236
1290
  */
1237
1291
  declare const truncateLine: (text: string, width: number) => string;
1238
1292
  /**
1239
- * A message of `lines`, each at most `MESSAGE_WIDTH` columns, broken at
1240
- * spaces, so a name, a path, or a list it embeds never widens a line. A
1241
- * word longer than a line, such as a URL or a path, gets a line of its
1242
- * own. Lines that fit, such as an indented command, stay as they are.
1293
+ * Whether a line is a command to copy, by the program it runs, rather than
1294
+ * prose: the next word is an argument, so "aws CLI not found" is prose.
1295
+ */
1296
+ declare const isCommandLine: (line: string) => boolean;
1297
+ /**
1298
+ * What the CLI prints for people, readable: prose wraps at spaces around
1299
+ * `width` display columns, and a command to copy, a table's row, and a word
1300
+ * longer than a line, such as a URL or a path, stay whole on their lines.
1301
+ * A line that fits stays as it is, so fitting twice changes nothing. `p`
1302
+ * applies it to all it prints; text printed another way, such as to stderr
1303
+ * under `--json`, calls it.
1304
+ */
1305
+ declare const fitText: (text: string, width?: number) => string;
1306
+ /**
1307
+ * A message of `lines`, each fitted as `fitText` does, so a name, a path, or
1308
+ * a list it embeds doesn't run on.
1243
1309
  */
1244
1310
  declare const fitMessage: (lines: readonly string[]) => string;
1311
+ /**
1312
+ * `text` with each JSON body in it, such as the response an SDK's error
1313
+ * embeds, replaced with the message the body says, as people read an error:
1314
+ * Cloudflare's `errors[].message`, or a `message` or an `error`. A body that
1315
+ * says none is cut. Printed errors go through it; `--json` keeps the body.
1316
+ * A registered value is masked before anything is cut, as a cut one no longer
1317
+ * matches, and again after, as a body's string may have escaped it.
1318
+ */
1319
+ declare const withProviderMessages: (message: string) => string;
1245
1320
  //#endregion
1246
1321
  //#region src/minBundleId.d.ts
1247
1322
  /**
@@ -2468,10 +2543,20 @@ declare const taskLog: (opts: TaskLogOptions) => {
2468
2543
  };
2469
2544
  //#endregion
2470
2545
  //#region src/prompts.d.ts
2546
+ /**
2547
+ * What a tool wrote, such as a build's output: masked, but printed as the
2548
+ * tool wrote it, while everything the CLI writes goes through `p`.
2549
+ */
2550
+ declare const toolOutput: {
2551
+ error: (message: string, options?: LogMessageOptions | undefined) => void;
2552
+ note: (message: string, title?: string, options?: Parameters<typeof note>[2]) => void;
2553
+ };
2471
2554
  /**
2472
2555
  * `@clack/prompts` with CI-safe spinners, with every message a registered
2473
- * credential value appears in masked (`registerSecretValue`), and with
2474
- * prompts that refuse to ask without a terminal.
2556
+ * credential value appears in masked (`registerSecretValue`) and its prose
2557
+ * wrapped (`fitText`), with a provider's JSON body in an error or a warning
2558
+ * replaced with its message, and with prompts that refuse to ask without a
2559
+ * terminal.
2475
2560
  */
2476
2561
  declare const p: typeof index_d_exports;
2477
2562
  type PromptProgress = ReturnType<typeof p.progress>;
@@ -2518,7 +2603,10 @@ declare const resolveHotUpdaterServerVersion: (currentPackageName: string, optio
2518
2603
  * masked.
2519
2604
  */
2520
2605
  declare const registerSecretValue: (value: string) => void;
2521
- /** Replaces every registered credential value in `text` with a mask. */
2606
+ /**
2607
+ * Replaces every registered credential value in `text`, and any JSON Web Token,
2608
+ * with a mask.
2609
+ */
2522
2610
  declare const maskSecretValues: (text: string) => string;
2523
2611
  //#endregion
2524
2612
  //#region src/localKeyStore.d.ts
@@ -2857,4 +2945,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
2857
2945
  */
2858
2946
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
2859
2947
  //#endregion
2860
- export { AddSigningSecretResult, AppClientCredential, AppClientPlugin, AssembleServerOptions, AssembledServer, BUNDLE_SIGNING_KEY_SECRET, BuildFailedError, BuildFailureSummaryConfig, BuildLogger, BuildLoggerConfig, BuildPhaseReader, BuildType, BundleSigningKeyError, BundleSigningOptions, BundleSigningSession, BundleSigningState, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ClientAuthSpec, ClientCredentialSpec, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateCredentialResolverOptions, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, type CredentialFiles, CredentialKind, CredentialRequest, CredentialResolver, CredentialUnavailableError, Credentials, FileCredentialKind, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, INTERFACE_FINGERPRINT_ENGINE, INVALID_SIGNING_MESSAGE, INVALID_SIGNING_NEXT_STEP, ISSUE_SIGNING_KEY_COMMAND, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, IssuedCredentialKind, LOCAL_SECRETS_NAME, LegacyInfrastructureError, LocalSecretsOptions, LocalSecretsStore, MANAGED_CLIENT_CREDENTIAL_ENV, MESSAGE_WIDTH, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, NativeDependency, NativeInterface, PUT_SIGNING_KEY_COMMAND, PromptProgress, PromptSpinner, ProviderConfig, ProvisionedClientCredential, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, SecretsScope, ServerClientEndpoint, ServerDefinition, ServerDefinitionError, Step, StepLevel, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, WriteHotUpdaterConfigResult, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, bundleSigningPutCommand, bundleSigningSecretRef, clientAuthOf, typedColors as colors, configSetsTeamSecrets, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createStep, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeMissingSecret, describeSecret, describeStoredSecret, displayWidth, encryptJson, ensureInstallPackages, failActiveStep, fitMessage, formatCommand, formatDuration, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getDeploySecretNames, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isSecretRef, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readBundleSigningState, readHotUpdaterInitEnv, readLoadedEnvFile, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, readSecrets, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secret, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, startStep, storeEnvFileSecrets, stripAnsi, transformEnv, transformTemplate, truncateLine, withCredentialResolver, withDuration, withSecretsScope, withStep, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
2948
+ export { AddSigningSecretResult, AppClientCredential, AppClientPlugin, AssembleServerOptions, AssembledServer, BUNDLE_SIGNING_KEY_SECRET, BuildFailedError, BuildFailureSummaryConfig, BuildLogger, BuildLoggerConfig, BuildPhaseReader, BuildType, BundleSigningKeyError, BundleSigningOptions, BundleSigningSession, BundleSigningState, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ClientAuthSpec, ClientCredentialSpec, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateCredentialResolverOptions, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, type CredentialFiles, CredentialKind, CredentialRequest, CredentialResolver, CredentialUnavailableError, Credentials, EXPO_SDK_UPGRADE_COMMAND, ExpoSdk, FileCredentialKind, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, INTERFACE_FINGERPRINT_ENGINE, INVALID_SIGNING_MESSAGE, INVALID_SIGNING_NEXT_STEP, ISSUE_SIGNING_KEY_COMMAND, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, IssuedCredentialKind, LOCAL_SECRETS_NAME, LegacyInfrastructureError, LocalSecretsOptions, LocalSecretsStore, MANAGED_CLIENT_CREDENTIAL_ENV, MESSAGE_WIDTH, MINIMUM_EXPO_SDK, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, NativeDependency, NativeInterface, PUT_SIGNING_KEY_COMMAND, PromptProgress, PromptSpinner, ProviderConfig, ProvisionedClientCredential, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, SecretsScope, ServerClientEndpoint, ServerDefinition, ServerDefinitionError, Step, StepLevel, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, WriteHotUpdaterConfigResult, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertSupportedExpoSdk, assertUnreservedEnv, autolinkingConfigCommand, banner, bundleSigningPutCommand, bundleSigningSecretRef, clientAuthOf, typedColors as colors, configSetsTeamSecrets, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createStep, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeMissingSecret, describeSecret, describeStoredSecret, displayWidth, encryptJson, ensureInstallPackages, failActiveStep, fitMessage, fitText, formatDuration, formatEnvValue, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBuildSecretNames, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getDeploySecretNames, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isCommandLine, isReservedVariableName, isSecretName, isSecretRef, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, mergeEnvContent, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readBundleSigningState, readExpoSdk, readHotUpdaterInitEnv, readLoadedEnvFile, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, readSecrets, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secret, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, startStep, storeEnvFileSecrets, stripAnsi, toolOutput, transformEnv, transformTemplate, truncateLine, withCredentialResolver, withDuration, withProviderMessages, withSecretsScope, withStep, writeEnvAtomically, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
package/dist/index.mjs CHANGED
@@ -22584,14 +22584,221 @@ const registerSecretValue = (value) => {
22584
22584
  registry.secretValuesLongestFirst = longestFirst(secretValues);
22585
22585
  registry.shortSecretPattern = shortSecretValues.size > 0 ? new RegExp(`${SHORT_SECRET_BEFORE}(?:${longestFirst(shortSecretValues).map(escapeRegExp).join("|")})${SHORT_SECRET_AFTER}`, "g") : void 0;
22586
22586
  };
22587
- /** Replaces every registered credential value in `text` with a mask. */
22587
+ /**
22588
+ * A JSON Web Token, whether or not it was registered: Xcode mints its own from
22589
+ * the App Store Connect API key and may print it, and a build's log is uploaded
22590
+ * when the build fails.
22591
+ */
22592
+ const JSON_WEB_TOKEN = /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}/g;
22593
+ /**
22594
+ * Replaces every registered credential value in `text`, and any JSON Web Token,
22595
+ * with a mask.
22596
+ */
22588
22597
  const maskSecretValues = (text) => {
22589
22598
  let masked = text;
22590
22599
  for (const value of registry.secretValuesLongestFirst) if (masked.includes(value)) masked = masked.split(value).join(SECRET_MASK);
22600
+ masked = masked.replace(JSON_WEB_TOKEN, SECRET_MASK);
22591
22601
  const { shortSecretPattern } = registry;
22592
22602
  return shortSecretPattern ? masked.replace(shortSecretPattern, SECRET_MASK) : masked;
22593
22603
  };
22594
22604
  //#endregion
22605
+ //#region src/messageLines.ts
22606
+ /**
22607
+ * Where the CLI wraps a message's prose, so it reads well after clack's
22608
+ * 3-column gutter. It is a default for readability: a command, a URL, or a
22609
+ * path stays whole, however long.
22610
+ */
22611
+ const MESSAGE_WIDTH = 73;
22612
+ const graphemes = new Intl.Segmenter();
22613
+ /** Combining marks, format and control characters: they take no column. */
22614
+ const ZERO_WIDTH = /^[\p{M}\p{Cf}\p{Cc}]+$/u;
22615
+ /** East Asian wide and fullwidth characters, and emoji: two columns each. */
22616
+ const WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{20000}-\u{3FFFD}]|\p{Emoji_Presentation}/u;
22617
+ /** The columns one grapheme takes. */
22618
+ const segmentWidth = (segment) => ZERO_WIDTH.test(segment) ? 0 : WIDE.test(segment) ? 2 : 1;
22619
+ /**
22620
+ * The columns `text` takes in a terminal, without its colors: `│`, `■`,
22621
+ * and `…` take one each, though each is several bytes.
22622
+ */
22623
+ const displayWidth = (text) => {
22624
+ let width = 0;
22625
+ for (const { segment } of graphemes.segment(stripVTControlCharacters(text))) width += segmentWidth(segment);
22626
+ return width;
22627
+ };
22628
+ /**
22629
+ * `text` cut to `width` display columns, with `…` for the rest, never
22630
+ * inside a character: a cut line loses its colors.
22631
+ */
22632
+ const truncateLine = (text, width) => {
22633
+ const plain = stripVTControlCharacters(text);
22634
+ if (displayWidth(plain) <= width) return text;
22635
+ let cut = "";
22636
+ let used = 0;
22637
+ for (const { segment } of graphemes.segment(plain)) {
22638
+ used += segmentWidth(segment);
22639
+ if (used > width - 1) break;
22640
+ cut += segment;
22641
+ }
22642
+ return `${cut.trimEnd()}…`;
22643
+ };
22644
+ const COLOR_CODE = /\u001B\[(\d+)(?:;\d+)*m/gu;
22645
+ /** The code that ends a color or style, by the code that starts it. */
22646
+ const closingCode = (code) => {
22647
+ if (code >= 30 && code <= 38 || code >= 90 && code <= 97) return 39;
22648
+ if (code >= 40 && code <= 48 || code >= 100 && code <= 107) return 49;
22649
+ return {
22650
+ 1: 22,
22651
+ 2: 22,
22652
+ 3: 23,
22653
+ 4: 24,
22654
+ 7: 27,
22655
+ 8: 28,
22656
+ 9: 29
22657
+ }[code];
22658
+ };
22659
+ /**
22660
+ * `lines` with each color that runs past a line's end closed there and
22661
+ * reopened on the next, as clack's gutter, colored between them, ends it.
22662
+ */
22663
+ const carryColors = (lines) => {
22664
+ const open = /* @__PURE__ */ new Map();
22665
+ return lines.map((line) => {
22666
+ const reopened = [...open.values()].join("");
22667
+ for (const match of line.matchAll(COLOR_CODE)) {
22668
+ const code = Number(match[1]);
22669
+ const closing = closingCode(code);
22670
+ if (code === 0) open.clear();
22671
+ else if (closing === void 0) open.delete(code);
22672
+ else open.set(closing, match[0]);
22673
+ }
22674
+ return `${reopened}${line}${[...open.keys()].reverse().map((code) => `\u001B[${code}m`).join("")}`;
22675
+ });
22676
+ };
22677
+ /** A line's words, with a color code on its own joined to the next word. */
22678
+ const wordsOf = (line) => {
22679
+ const words = [];
22680
+ let colors = "";
22681
+ for (const word of line.match(/\S+/gu) ?? []) if (displayWidth(word) === 0) colors += word;
22682
+ else {
22683
+ words.push(`${colors}${word}`);
22684
+ colors = "";
22685
+ }
22686
+ if (colors && words.length > 0) words[words.length - 1] += colors;
22687
+ return words;
22688
+ };
22689
+ /**
22690
+ * Whether a line is a command to copy, by the program it runs, rather than
22691
+ * prose: the next word is an argument, so "aws CLI not found" is prose.
22692
+ */
22693
+ const isCommandLine = (line) => /^\s*(?:\$ )?(?:npx|pnpx|bunx|pnpm|npm|yarn|bun|hot-updater|aws|az|gcloud|gh|git)\s+[a-z0-9@./<-]/u.test(stripVTControlCharacters(line));
22694
+ /** A row of a bordered table, which a line break would garble. */
22695
+ const isTableRow = (line) => /^\s*[┌├└│]/u.test(stripVTControlCharacters(line));
22696
+ /** Prose's lines, broken at spaces, each with the line's indentation. */
22697
+ const wrapProse = (line, width) => {
22698
+ const indent = /^\s*/u.exec(stripVTControlCharacters(line))?.[0] ?? "";
22699
+ const lines = [];
22700
+ let current = "";
22701
+ for (const word of wordsOf(line)) if (current && displayWidth(`${indent}${current} ${word}`) > width) {
22702
+ lines.push(`${indent}${current}`);
22703
+ current = word;
22704
+ } else current = current ? `${current} ${word}` : word;
22705
+ return [...lines, `${indent}${current}`];
22706
+ };
22707
+ /**
22708
+ * What the CLI prints for people, readable: prose wraps at spaces around
22709
+ * `width` display columns, and a command to copy, a table's row, and a word
22710
+ * longer than a line, such as a URL or a path, stay whole on their lines.
22711
+ * A line that fits stays as it is, so fitting twice changes nothing. `p`
22712
+ * applies it to all it prints; text printed another way, such as to stderr
22713
+ * under `--json`, calls it.
22714
+ */
22715
+ const fitText = (text, width = 73) => {
22716
+ const lines = text.split("\n").flatMap((line) => displayWidth(line) <= width || isCommandLine(line) || isTableRow(line) ? [line] : wrapProse(line, width));
22717
+ return (text.includes("\x1B[") ? carryColors(lines) : lines).join("\n");
22718
+ };
22719
+ /**
22720
+ * A message of `lines`, each fitted as `fitText` does, so a name, a path, or
22721
+ * a list it embeds doesn't run on.
22722
+ */
22723
+ const fitMessage = (lines) => fitText(lines.join("\n"));
22724
+ /** The most a provider's message takes: two lines of a message. */
22725
+ const PROVIDER_MESSAGE_WIDTH = 146;
22726
+ /** What a response with no message it says shows of itself. */
22727
+ const UNREAD_BODY_WIDTH = 40;
22728
+ /** Where an error body says what went wrong, most specific first. */
22729
+ const MESSAGE_KEYS = [
22730
+ "errors",
22731
+ "error_description",
22732
+ "message",
22733
+ "error",
22734
+ "detail",
22735
+ "title",
22736
+ "msg"
22737
+ ];
22738
+ const messagesIn = (body) => {
22739
+ if (typeof body === "string") return body.trim() ? [body.trim()] : [];
22740
+ if (Array.isArray(body)) return body.flatMap(messagesIn);
22741
+ if (!body || typeof body !== "object") return [];
22742
+ for (const key of MESSAGE_KEYS) {
22743
+ const messages = messagesIn(body[key]);
22744
+ if (messages.length > 0) return messages;
22745
+ }
22746
+ return [];
22747
+ };
22748
+ /** Where the JSON value that opens at `start` closes, or -1. */
22749
+ const closingIndex = (text, start) => {
22750
+ let depth = 0;
22751
+ let inString = false;
22752
+ for (let index = start; index < text.length; index += 1) {
22753
+ const char = text[index];
22754
+ if (inString) {
22755
+ if (char === "\\") index += 1;
22756
+ else if (char === "\"") inString = false;
22757
+ } else if (char === "\"") inString = true;
22758
+ else if (char === "{" || char === "[") depth += 1;
22759
+ else if (char === "}" || char === "]") {
22760
+ depth -= 1;
22761
+ if (depth === 0) return index;
22762
+ }
22763
+ }
22764
+ return -1;
22765
+ };
22766
+ const parseBody = (text) => {
22767
+ try {
22768
+ const value = JSON.parse(text);
22769
+ return value && typeof value === "object" && (!Array.isArray(value) || value.every((entry) => entry && typeof entry === "object")) ? value : void 0;
22770
+ } catch {
22771
+ return;
22772
+ }
22773
+ };
22774
+ /**
22775
+ * `text` with each JSON body in it, such as the response an SDK's error
22776
+ * embeds, replaced with the message the body says, as people read an error:
22777
+ * Cloudflare's `errors[].message`, or a `message` or an `error`. A body that
22778
+ * says none is cut. Printed errors go through it; `--json` keeps the body.
22779
+ * A registered value is masked before anything is cut, as a cut one no longer
22780
+ * matches, and again after, as a body's string may have escaped it.
22781
+ */
22782
+ const withProviderMessages = (message) => {
22783
+ const text = maskSecretValues(message);
22784
+ let result = "";
22785
+ let index = 0;
22786
+ while (index < text.length) {
22787
+ const char = text[index];
22788
+ const end = char === "{" || char === "[" ? closingIndex(text, index) : -1;
22789
+ const body = end === -1 ? void 0 : parseBody(text.slice(index, end + 1));
22790
+ if (body === void 0) {
22791
+ result += char;
22792
+ index += 1;
22793
+ continue;
22794
+ }
22795
+ const messages = [...new Set(messagesIn(body))];
22796
+ result += messages.length ? truncateLine(messages.join("; "), PROVIDER_MESSAGE_WIDTH) : truncateLine(text.slice(index, end + 1), UNREAD_BODY_WIDTH);
22797
+ index = end + 1;
22798
+ }
22799
+ return maskSecretValues(result);
22800
+ };
22801
+ //#endregion
22595
22802
  //#region src/stepCapture.ts
22596
22803
  let current;
22597
22804
  const stepCapture = () => current;
@@ -22600,7 +22807,15 @@ const setStepCapture = (capture) => {
22600
22807
  };
22601
22808
  //#endregion
22602
22809
  //#region src/prompts.ts
22603
- const maskMessage = (message) => message === void 0 ? message : maskSecretValues(message);
22810
+ /**
22811
+ * A message as people read it: masked first, so a line break never splits
22812
+ * a value, then its prose wrapped.
22813
+ */
22814
+ const printable = (message) => fitText(maskSecretValues(message));
22815
+ /** An error or a warning, which says what a provider's JSON body says. */
22816
+ const printableFailure = (message) => printable(withProviderMessages(message));
22817
+ const printableMessage = (message) => message === void 0 ? message : printable(message);
22818
+ const printableFailureMessage = (message) => message === void 0 ? message : printableFailure(message);
22604
22819
  /**
22605
22820
  * Prints a log line, or, while a step runs, holds it until the step's line
22606
22821
  * prints. An info line says what the step is doing, so it shows as the
@@ -22623,13 +22838,21 @@ const logThroughStep = (write, { inStep = "hold" } = {}) => (...args) => {
22623
22838
  capture.hold(() => write(...args));
22624
22839
  };
22625
22840
  const log$1 = {
22626
- message: logThroughStep((message, options) => log$2.message(Array.isArray(message) ? message.map(maskSecretValues) : maskMessage(message), options)),
22627
- info: logThroughStep((message, options) => log$2.info(maskSecretValues(message), options), { inStep: "detail" }),
22628
- success: logThroughStep((message, options) => log$2.success(maskSecretValues(message), options)),
22629
- step: logThroughStep((message, options) => log$2.step(maskSecretValues(message), options)),
22630
- warn: logThroughStep((message, options) => log$2.warn(maskSecretValues(message), options)),
22631
- warning: logThroughStep((message, options) => log$2.warning(maskSecretValues(message), options)),
22632
- error: logThroughStep((message, options) => log$2.error(maskSecretValues(message), options), { inStep: "interrupt" })
22841
+ message: logThroughStep((message, options) => log$2.message(Array.isArray(message) ? message.map((line) => printable(line)) : printableMessage(message), options)),
22842
+ info: logThroughStep((message, options) => log$2.info(printable(message), options), { inStep: "detail" }),
22843
+ success: logThroughStep((message, options) => log$2.success(printable(message), options)),
22844
+ step: logThroughStep((message, options) => log$2.step(printable(message), options)),
22845
+ warn: logThroughStep((message, options) => log$2.warn(printableFailure(message), options)),
22846
+ warning: logThroughStep((message, options) => log$2.warning(printableFailure(message), options)),
22847
+ error: logThroughStep((message, options) => log$2.error(printableFailure(message), options), { inStep: "interrupt" })
22848
+ };
22849
+ /**
22850
+ * What a tool wrote, such as a build's output: masked, but printed as the
22851
+ * tool wrote it, while everything the CLI writes goes through `p`.
22852
+ */
22853
+ const toolOutput = {
22854
+ error: logThroughStep((message, options) => log$2.error(maskSecretValues(message), options), { inStep: "interrupt" }),
22855
+ note: (message, title, options) => note(maskSecretValues(message), title === void 0 ? title : maskSecretValues(title), options)
22633
22856
  };
22634
22857
  /** A spinner inside a running step, whose messages are the step's detail. */
22635
22858
  const spinnerInStep = (capture) => ({
@@ -22643,20 +22866,20 @@ const spinnerInStep = (capture) => ({
22643
22866
  return false;
22644
22867
  }
22645
22868
  });
22646
- const maskSpinner = (target) => ({
22647
- start: (message) => target.start(maskMessage(message)),
22648
- stop: (message) => target.stop(maskMessage(message)),
22649
- cancel: (message) => target.cancel(maskMessage(message)),
22650
- error: (message) => target.error(maskMessage(message)),
22651
- message: (message) => target.message(maskMessage(message)),
22869
+ const printableSpinner = (target) => ({
22870
+ start: (message) => target.start(printableMessage(message)),
22871
+ stop: (message) => target.stop(printableMessage(message)),
22872
+ cancel: (message) => target.cancel(printableFailureMessage(message)),
22873
+ error: (message) => target.error(printableFailureMessage(message)),
22874
+ message: (message) => target.message(printableMessage(message)),
22652
22875
  clear: () => target.clear(),
22653
22876
  get isCancelled() {
22654
22877
  return target.isCancelled;
22655
22878
  }
22656
22879
  });
22657
- const maskProgress = (target) => ({
22658
- ...maskSpinner(target),
22659
- advance: (step, message) => target.advance(step, maskMessage(message)),
22880
+ const printableProgress = (target) => ({
22881
+ ...printableSpinner(target),
22882
+ advance: (step, message) => target.advance(step, printableMessage(message)),
22660
22883
  get isCancelled() {
22661
22884
  return target.isCancelled;
22662
22885
  }
@@ -22705,7 +22928,7 @@ const spinner = (options = {}) => {
22705
22928
  if (capture) return spinnerInStep(capture);
22706
22929
  const output = options.output ?? process.stdout;
22707
22930
  const isInteractive = isTTY(output) && !isCI$1();
22708
- return maskSpinner(isInteractive ? spinner$1(options) : createStaticSpinner(options));
22931
+ return printableSpinner(isInteractive ? spinner$1(options) : createStaticSpinner(options));
22709
22932
  };
22710
22933
  const progress = (options = {}) => {
22711
22934
  const capture = stepCapture();
@@ -22719,8 +22942,8 @@ const progress = (options = {}) => {
22719
22942
  }
22720
22943
  };
22721
22944
  const output = options.output ?? process.stdout;
22722
- if (isTTY(output) && !isCI$1()) return maskProgress(progress$1(options));
22723
- const staticSpinner = maskSpinner(createStaticSpinner(options));
22945
+ if (isTTY(output) && !isCI$1()) return printableProgress(progress$1(options));
22946
+ const staticSpinner = printableSpinner(createStaticSpinner(options));
22724
22947
  return {
22725
22948
  ...staticSpinner,
22726
22949
  advance: (_step, message) => staticSpinner.message(message),
@@ -22756,13 +22979,23 @@ const requireTerminal = (prompt) => {
22756
22979
  const message = options.message;
22757
22980
  return Promise.reject(/* @__PURE__ */ new Error(`${typeof message === "string" ? `${message} ` : ""}The CLI asks nothing ${reason}: pass the command's options instead (see --help).`));
22758
22981
  }
22759
- return prompt(options);
22982
+ return prompt(fitPromptMessage(options));
22760
22983
  };
22761
22984
  };
22985
+ /** Prompt `options` whose message, when it has one, is fitted. */
22986
+ const fitPromptMessage = (options) => {
22987
+ const { message } = options;
22988
+ return typeof message === "string" ? {
22989
+ ...options,
22990
+ message: printable(message)
22991
+ } : options;
22992
+ };
22762
22993
  /**
22763
22994
  * `@clack/prompts` with CI-safe spinners, with every message a registered
22764
- * credential value appears in masked (`registerSecretValue`), and with
22765
- * prompts that refuse to ask without a terminal.
22995
+ * credential value appears in masked (`registerSecretValue`) and its prose
22996
+ * wrapped (`fitText`), with a provider's JSON body in an error or a warning
22997
+ * replaced with its message, and with prompts that refuse to ask without a
22998
+ * terminal.
22766
22999
  */
22767
23000
  const p = {
22768
23001
  ...dist_exports,
@@ -22779,10 +23012,10 @@ const p = {
22779
23012
  selectKey: requireTerminal(selectKey),
22780
23013
  text: requireTerminal(text),
22781
23014
  log: log$1,
22782
- cancel: (message, options) => cancel(maskMessage(message), options),
22783
- intro: (title, options) => intro(maskMessage(title), options),
22784
- note: (message, title, options) => note(maskMessage(message), maskMessage(title), options),
22785
- outro: (message, options) => outro(maskMessage(message), options),
23015
+ cancel: (message, options) => cancel(printableFailureMessage(message), options),
23016
+ intro: (title, options) => intro(printableMessage(title), options),
23017
+ note: (message, title, options) => note(message === void 0 ? message : maskSecretValues(message), title === void 0 ? title : maskSecretValues(title), options),
23018
+ outro: (message, options) => outro(printableMessage(message), options),
22786
23019
  progress,
22787
23020
  spinner,
22788
23021
  tasks
@@ -22886,61 +23119,6 @@ const createLogWriter = async ({ prefix, logFilePath }) => {
22886
23119
  }
22887
23120
  };
22888
23121
  //#endregion
22889
- //#region src/messageLines.ts
22890
- /**
22891
- * The widest line of a message the CLI prints: clack puts it after its
22892
- * 3-column gutter (`│ `), and the whole line stays within 76 columns.
22893
- */
22894
- const MESSAGE_WIDTH = 73;
22895
- const graphemes = new Intl.Segmenter();
22896
- /** Combining marks, format and control characters: they take no column. */
22897
- const ZERO_WIDTH = /^[\p{M}\p{Cf}\p{Cc}]+$/u;
22898
- /** East Asian wide and fullwidth characters, and emoji: two columns each. */
22899
- const WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{20000}-\u{3FFFD}]|\p{Emoji_Presentation}/u;
22900
- /** The columns one grapheme takes. */
22901
- const segmentWidth = (segment) => ZERO_WIDTH.test(segment) ? 0 : WIDE.test(segment) ? 2 : 1;
22902
- /**
22903
- * The columns `text` takes in a terminal, without its colors: `│`, `■`,
22904
- * and `…` take one each, though each is several bytes.
22905
- */
22906
- const displayWidth = (text) => {
22907
- let width = 0;
22908
- for (const { segment } of graphemes.segment(stripVTControlCharacters(text))) width += segmentWidth(segment);
22909
- return width;
22910
- };
22911
- /**
22912
- * `text` cut to `width` display columns, with `…` for the rest, never
22913
- * inside a character: a cut line loses its colors.
22914
- */
22915
- const truncateLine = (text, width) => {
22916
- const plain = stripVTControlCharacters(text);
22917
- if (displayWidth(plain) <= width) return text;
22918
- let cut = "";
22919
- let used = 0;
22920
- for (const { segment } of graphemes.segment(plain)) {
22921
- used += segmentWidth(segment);
22922
- if (used > width - 1) break;
22923
- cut += segment;
22924
- }
22925
- return `${cut.trimEnd()}…`;
22926
- };
22927
- /**
22928
- * A message of `lines`, each at most `MESSAGE_WIDTH` columns, broken at
22929
- * spaces, so a name, a path, or a list it embeds never widens a line. A
22930
- * word longer than a line, such as a URL or a path, gets a line of its
22931
- * own. Lines that fit, such as an indented command, stay as they are.
22932
- */
22933
- const fitMessage = (lines) => lines.flatMap((line) => {
22934
- if (displayWidth(line) <= 73) return [line];
22935
- const fitted = [];
22936
- let current = "";
22937
- for (const word of line.split(" ").filter(Boolean)) if (current && displayWidth(current) + 1 + displayWidth(word) > 73) {
22938
- fitted.push(current);
22939
- current = word;
22940
- } else current = current ? `${current} ${word}` : word;
22941
- return [...fitted, current];
22942
- }).join("\n");
22943
- //#endregion
22944
23122
  //#region src/colors.ts
22945
23123
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
22946
23124
  let p = process || {};
@@ -23013,8 +23191,6 @@ var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((
23013
23191
  const typedColors = import_picocolors.default;
23014
23192
  //#endregion
23015
23193
  //#region src/step.ts
23016
- /** The widest a finished step's line is, after clack's 3-column gutter. */
23017
- const STEP_WIDTH = 73;
23018
23194
  /** The room the spinner's symbol and its `[12m 34s]` timer take. */
23019
23195
  const LIVE_LINE_CHROME = 13;
23020
23196
  /** `4.2s`, `38s`, `6m41s`, `1h02m`. */
@@ -23030,7 +23206,7 @@ const formatDuration = (ms) => {
23030
23206
  * `message` with `duration` at the right edge of a step's line, the message
23031
23207
  * cut with `…` when the line would be wider.
23032
23208
  */
23033
- const withDuration = (message, ms, width = STEP_WIDTH) => {
23209
+ const withDuration = (message, ms, width = 73) => {
23034
23210
  const duration = formatDuration(ms);
23035
23211
  const text = truncateLine(message, width - duration.length - 2);
23036
23212
  const gap = Math.max(2, width - displayWidth(text) - duration.length);
@@ -23127,7 +23303,7 @@ const createStep = (title, { output = process.stdout } = {}) => {
23127
23303
  return;
23128
23304
  }
23129
23305
  finish(() => {
23130
- log$2.error(maskSecretValues(withDuration(message, elapsed)), logOptions());
23306
+ log$2.error(maskSecretValues(withDuration(withProviderMessages(message), elapsed)), logOptions());
23131
23307
  });
23132
23308
  },
23133
23309
  get elapsedMs() {
@@ -23266,7 +23442,7 @@ var BuildLogger = class {
23266
23442
  const first = lines.findIndex((line) => line.trim());
23267
23443
  const last = lines.findLastIndex((line) => line.trim());
23268
23444
  const summary = first === -1 ? [] : lines.slice(first, last + 1);
23269
- return maxWidth ? summary.map((line) => truncateLine(line, maxWidth)) : summary;
23445
+ return maxWidth ? summary.map((line) => truncateLine(maskSecretValues(line), maxWidth)) : summary;
23270
23446
  }
23271
23447
  async consumeSummaryStream(input) {
23272
23448
  const lineReader = createInterface({
@@ -23362,7 +23538,7 @@ var BuildLogger = class {
23362
23538
  const summary = this.failureSummary;
23363
23539
  const logPath = this.logPath;
23364
23540
  const message = [...summary, ...logPath ? [...summary.length ? [""] : [], `Build log: ${logPath}`] : []].join("\n");
23365
- if (message) p.log.error(message, { output: process.stderr });
23541
+ if (message) toolOutput.error(message, { output: process.stderr });
23366
23542
  }
23367
23543
  };
23368
23544
  //#endregion
@@ -25716,12 +25892,7 @@ const assertPrivateDirectory = async (directory) => {
25716
25892
  if (!root) return;
25717
25893
  const top = realpath(root);
25718
25894
  if (isIgnoredByGit(top, real)) return;
25719
- throw new Error(fitMessage([
25720
- `localSecrets() refuses ${directory}: its secrets could be committed.`,
25721
- `The git repository ${top} doesn't ignore it.`,
25722
- `Add ${path$1.relative(top, real).split(path$1.sep).join("/")}/ to its .gitignore,`,
25723
- "or set localSecrets({ path })."
25724
- ]));
25895
+ throw new Error(fitMessage([`localSecrets() refuses ${directory}: its secrets could be committed.`, `Add ${path$1.relative(top, real).split(path$1.sep).join("/")}/ to ${top}'s .gitignore, or set localSecrets({ path }).`]));
25725
25896
  };
25726
25897
  /** Makes `directory` 0700, and every directory it creates on the way. */
25727
25898
  const makePrivateDirectory = async (directory) => {
@@ -26071,11 +26242,7 @@ const readSecretRef = async (name, scope) => {
26071
26242
  return fromEnv;
26072
26243
  }
26073
26244
  const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
26074
- if (secrets === void 0) throw new Error(fitMessage([
26075
- `secret("${name}") has no value.`,
26076
- `Set ${variable} in the environment or .env.hotupdater.`,
26077
- "Or set the config's secrets to a team store."
26078
- ]));
26245
+ if (secrets === void 0) throw new Error(fitMessage([`secret("${name}") has no value.`, `Set ${variable} in the environment or .env.hotupdater.`]));
26079
26246
  let values;
26080
26247
  if (scope) {
26081
26248
  scope.read ??= readSecrets(secrets, batchedNames(scope, name, fileEnv), cwd).catch((error) => {
@@ -26105,28 +26272,10 @@ const describeFields = (fields) => {
26105
26272
  return `${described.slice(0, -1).join(", ")}, and ${described.at(-1)}`;
26106
26273
  };
26107
26274
  /**
26108
- * A command to print, broken at spaces with ` \` once a line would pass
26109
- * 74 columns, each continuation indented two spaces, so a terminal never
26110
- * wraps it mid-word and it still pastes as one command.
26275
+ * `lead` and the command it names on one line when they fit, else the
26276
+ * command on the lines after it.
26111
26277
  */
26112
- const formatCommand = (command) => {
26113
- const lines = [];
26114
- let line = "";
26115
- for (const word of command.split(" ")) {
26116
- const next = line ? `${line} ${word}` : word;
26117
- if (line && next.length > 72) {
26118
- lines.push(`${line} \\`);
26119
- line = ` ${word}`;
26120
- } else line = next;
26121
- }
26122
- lines.push(line);
26123
- return lines.join("\n");
26124
- };
26125
- /**
26126
- * `lead` and the command it names on one line when they fit 77 columns,
26127
- * else the command on the lines after it.
26128
- */
26129
- const withLead = (lead, command) => lead.length + 1 + command.length <= 77 ? `${lead} ${command}` : `${lead}\n${formatCommand(command)}`;
26278
+ const withLead = (lead, command) => displayWidth(`${lead} ${command}`) <= 73 ? `${lead} ${command}` : `${lead}\n${command}`;
26130
26279
  /**
26131
26280
  * Why a build has no upload keystore, steering away from a second key:
26132
26281
  * Play rejects builds signed with a key it does not have. Under a team's
@@ -26139,7 +26288,7 @@ const describeMissingKeystore = (store, item, name) => {
26139
26288
  if (configured) return [
26140
26289
  `No ${item} in ${describeSecret(configured, name)}.`,
26141
26290
  "Ask whoever set it up to store it there, or import it with:",
26142
- formatCommand(credentialPutCommand("android-keystore", name))
26291
+ credentialPutCommand("android-keystore", name)
26143
26292
  ].join("\n");
26144
26293
  return [
26145
26294
  `No ${item} in the ${store.name} secrets adapter.`,
@@ -26183,7 +26332,7 @@ const credentialStoreCommand = (kind, name = kind) => kind === "asc-api-key" ? c
26183
26332
  * trusted by a shipped app or a store. Under a team's adapter the name or
26184
26333
  * the access is wrong; under `localSecrets()` this machine lacks it.
26185
26334
  */
26186
- const describeMissingSecret = (secrets, name, { action = "import your key", command }) => [fitMessage(getConfiguredAdapter(secrets) ? [`${name} isn't in the ${secrets.name} secrets adapter.`, `Check the name and your access, or ${action}:`] : [`${name} isn't on this machine.`, `Check the name, set ${secretVariableName(name)}, or ${action}:`]), formatCommand(command)].join("\n");
26335
+ const describeMissingSecret = (secrets, name, { action = "import your key", command }) => [fitMessage(getConfiguredAdapter(secrets) ? [`${name} isn't in the ${secrets.name} secrets adapter.`, `Check the name and your access, or ${action}:`] : [`${name} isn't on this machine.`, `Check the name, set ${secretVariableName(name)}, or ${action}:`]), command].join("\n");
26187
26336
  var CredentialUnavailableError = class extends Error {
26188
26337
  kind;
26189
26338
  /** The environment variables that can provide this credential. */
@@ -26432,7 +26581,7 @@ const createCredentialResolver = ({ env = {}, secrets, cwd } = {}) => {
26432
26581
  const reason = error instanceof Error ? error.message : String(error);
26433
26582
  throw new CredentialUnavailableError(kind, `Couldn't read ${item} from the ${store.name} secrets adapter.\n${reason}`, { cause: error });
26434
26583
  }
26435
- if (value === void 0) throw new CredentialUnavailableError(kind, kind === "android-keystore" ? describeMissingKeystore(store, item, name) : `No ${item} in the ${store.name} secrets adapter.\nStore it once with:\n${formatCommand(credentialStoreCommand(kind, name))}`);
26584
+ if (value === void 0) throw new CredentialUnavailableError(kind, kind === "android-keystore" ? describeMissingKeystore(store, item, name) : `No ${item} in the ${store.name} secrets adapter.\nStore it once with:\n${credentialStoreCommand(kind, name)}`);
26436
26585
  registerSecretValue(value);
26437
26586
  return fromSecretValue[kind](value, {
26438
26587
  files,
@@ -46600,8 +46749,11 @@ var import_readYaml = require_readYaml();
46600
46749
  const require$2 = createRequire(import.meta.url);
46601
46750
  /**
46602
46751
  * The engine fingerprint.json records for the interface fingerprint. Change
46603
- * it with any change to what is hashed or how, so the new hashes are reported
46604
- * as an engine change rather than a native one.
46752
+ * it with any change to what is hashed or how that moves every project's
46753
+ * hash, so the new hashes are reported as an engine change rather than a
46754
+ * native one. The name is part of every digest, so changing it moves them all:
46755
+ * tracking an input nothing tracked before keeps the name, moves only the
46756
+ * projects that have that input, and is reported as a change to its files.
46605
46757
  */
46606
46758
  const INTERFACE_FINGERPRINT_ENGINE = "hot-updater/interface@1";
46607
46759
  const GENERATED_DIRS = [
@@ -46648,6 +46800,9 @@ const nativeSourceDir = (platform, config) => {
46648
46800
  if (typeof value !== "string") return void 0;
46649
46801
  return platform === "ios" ? path$1.dirname(value) : value;
46650
46802
  };
46803
+ const isInNodeModules = (dir) => dir.split(path$1.sep).includes("node_modules");
46804
+ /** A folder of native code that exists, outside what an install wrote. */
46805
+ const isLinkedSourceDir = (dir) => !isInNodeModules(dir) && existsSync(dir);
46651
46806
  const readManifest = async (dir) => {
46652
46807
  try {
46653
46808
  return JSON.parse(await fs$2.readFile(path$1.join(dir, "package.json"), "utf8"));
@@ -46662,6 +46817,25 @@ const parseJsonOutput = (stdout, command) => {
46662
46817
  } catch {}
46663
46818
  throw new Error(`\`${command}\` printed no JSON configuration. Check that it runs in this project.`);
46664
46819
  };
46820
+ const EXPO_MODULE_CONFIGS = ["expo-module.config.json", "unimodule.json"];
46821
+ /**
46822
+ * The folder of an Expo module autolinking found outside node_modules: the
46823
+ * nearest one above `anchor` that holds its registration or its package.json.
46824
+ * `create-expo-module --local` writes no package.json, and an empty `name`
46825
+ * reaches here when the module's package.json has none.
46826
+ */
46827
+ const findLocalExpoModuleRoot = async (anchor, name) => {
46828
+ const start = await fs$2.realpath(anchor).catch(() => anchor);
46829
+ if (isInNodeModules(start)) return null;
46830
+ let dir = start;
46831
+ while (true) {
46832
+ if (name && (await readManifest(dir))?.name === name) return dir;
46833
+ for (const config of EXPO_MODULE_CONFIGS) if (existsSync(path$1.join(dir, config))) return dir;
46834
+ const parent = path$1.dirname(dir);
46835
+ if (parent === dir) return start;
46836
+ dir = parent;
46837
+ }
46838
+ };
46665
46839
  const findPackageRoot = async (from, name) => {
46666
46840
  let dir = from;
46667
46841
  while (true) {
@@ -46679,7 +46853,7 @@ const findPackageRoot = async (from, name) => {
46679
46853
  */
46680
46854
  const readExpoModuleConfig = async (root, platform) => {
46681
46855
  let file = null;
46682
- for (const name of ["expo-module.config.json", "unimodule.json"]) {
46856
+ for (const name of EXPO_MODULE_CONFIGS) {
46683
46857
  file = await fs$2.readFile(path$1.join(root, name), "utf8").catch(() => null);
46684
46858
  if (file !== null) break;
46685
46859
  }
@@ -46795,8 +46969,7 @@ const readExpoModules = async (cwd, platform) => {
46795
46969
  };
46796
46970
  };
46797
46971
  const readExpoModuleDependencies = (modules) => Promise.all(modules.flatMap((module) => {
46798
- const name = module.packageName;
46799
- if (!name) return [];
46972
+ const name = module.packageName ?? "";
46800
46973
  const sourceDirs = [
46801
46974
  ...(module.projects ?? []).map(({ sourceDir }) => sourceDir),
46802
46975
  ...(module.plugins ?? []).map(({ sourceDir }) => sourceDir),
@@ -46804,19 +46977,65 @@ const readExpoModuleDependencies = (modules) => Promise.all(modules.flatMap((mod
46804
46977
  ].filter((dir) => Boolean(dir));
46805
46978
  const files = [...(module.projects ?? []).flatMap(({ aarProjects }) => aarProjects ?? []), ...module.aarProjects ?? []].map(({ aarFilePath }) => aarFilePath).filter((file) => Boolean(file));
46806
46979
  return [(async () => {
46807
- const anchor = sourceDirs[0] ?? files[0];
46808
- const root = anchor ? await findPackageRoot(anchor, name) : null;
46809
- return root ? {
46810
- name,
46980
+ const anchor = sourceDirs[0] ?? (files[0] ? path$1.dirname(files[0]) : void 0);
46981
+ const root = (anchor ? await findLocalExpoModuleRoot(anchor, name) : null) ?? (anchor && name ? await findPackageRoot(anchor, name) : null);
46982
+ if (root) return [{
46983
+ name: name || path$1.basename(root),
46811
46984
  root,
46812
46985
  sourceDirs,
46813
46986
  ...files.length > 0 ? { files } : {}
46814
- } : {
46987
+ }];
46988
+ return name ? [{
46815
46989
  name,
46816
46990
  version: module.packageVersion ?? "unknown"
46817
- };
46991
+ }] : [];
46818
46992
  })()];
46819
- }));
46993
+ })).then((dependencies) => dependencies.flat());
46994
+ /** The packages an autolinking config links on the platform. */
46995
+ const linkedDependencies = (config, platform) => Object.entries(config.dependencies ?? {}).flatMap(([key, dependency]) => {
46996
+ const platformConfig = dependency.platforms?.[platform];
46997
+ if (!platformConfig) return [];
46998
+ const sourceDir = nativeSourceDir(platform, platformConfig);
46999
+ if (dependency.name && dependency.root) {
47000
+ const { name, root } = dependency;
47001
+ return [sourceDir ? {
47002
+ name,
47003
+ root,
47004
+ sourceDirs: [sourceDir]
47005
+ } : {
47006
+ name,
47007
+ root
47008
+ }];
47009
+ }
47010
+ return sourceDir && isLinkedSourceDir(sourceDir) ? [{
47011
+ name: key,
47012
+ root: sourceDir,
47013
+ sourceDirs: [sourceDir]
47014
+ }] : [];
47015
+ });
47016
+ /**
47017
+ * The local packages of a bare app with Expo modules that only Expo's
47018
+ * `react-native-config` lists. `install-expo-modules` makes its native project
47019
+ * link through that command, which also reads autolinking's search paths, such
47020
+ * as `modules/`. Packages it lists in node_modules stay out, so an app with no
47021
+ * such package keeps its fingerprint.
47022
+ */
47023
+ const readExpoLocalPackages = async (cwd, platform) => {
47024
+ const { bin, linksReactNative } = resolveExpoAutolinking(cwd);
47025
+ if (!linksReactNative || process.env["EXPO_USE_COMMUNITY_AUTOLINKING"] === "1") return [];
47026
+ const { exitCode, stdout } = await execa(process.execPath, [
47027
+ bin,
47028
+ "react-native-config",
47029
+ "--platform",
47030
+ platform,
47031
+ "--json"
47032
+ ], {
47033
+ cwd,
47034
+ reject: false
47035
+ });
47036
+ if (exitCode !== 0 || !stdout.includes("{")) return [];
47037
+ return linkedDependencies(parseJsonOutput(stdout, "react-native-config"), platform).filter((dependency) => "root" in dependency && !isInNodeModules(dependency.root));
47038
+ };
46820
47039
  const GRADLE_FLAGS = [
46821
47040
  "hermesEnabled",
46822
47041
  "hermesV1Enabled",
@@ -46869,24 +47088,23 @@ const readReactNativeInterface = async ({ cwd, command, args, platform, settings
46869
47088
  const { stdout } = await execa(command, args, { cwd });
46870
47089
  const config = parseJsonOutput(stdout, [path$1.basename(command), ...args].join(" "));
46871
47090
  const reactNative = config.reactNativePath ? await readManifest(config.reactNativePath) : null;
46872
- const dependencies = Object.values(config.dependencies ?? {}).flatMap((dependency) => dependency.name && dependency.root && dependency.platforms?.[platform] ? [{
46873
- name: dependency.name,
46874
- root: dependency.root,
46875
- sourceDir: nativeSourceDir(platform, dependency.platforms[platform])
46876
- }].map(({ sourceDir, ...rest }) => sourceDir ? {
46877
- ...rest,
46878
- sourceDirs: [sourceDir]
46879
- } : rest) : []);
46880
- const expo = dependencies.some(({ name }) => name === "expo") ? await readExpoModules(cwd, platform) : {
47091
+ const dependencies = linkedDependencies(config, platform);
47092
+ const linksExpoModules = dependencies.some(({ name }) => name === "expo");
47093
+ const expo = linksExpoModules ? await readExpoModules(cwd, platform) : {
46881
47094
  dependencies: [],
46882
47095
  output: null
46883
47096
  };
46884
47097
  const expoModules = expo.dependencies;
46885
47098
  const names = new Set(dependencies.map(({ name }) => name));
47099
+ const expoLocalPackages = linksExpoModules && command === process.execPath && !args.includes("react-native-config") ? await readExpoLocalPackages(cwd, platform) : [];
46886
47100
  const settings = declaredSettings ?? (platform === "android" ? await readGradleFlags(config.project?.android?.sourceDir ?? path$1.join(cwd, "android")) : {});
46887
47101
  return {
46888
47102
  runtime: `react-native@${reactNative?.version ?? "unknown"}`,
46889
- dependencies: [...dependencies, ...expoModules.filter(({ name }) => !names.has(name))],
47103
+ dependencies: [
47104
+ ...dependencies,
47105
+ ...expoModules.filter(({ name }) => !names.has(name)),
47106
+ ...expoLocalPackages.filter(({ name }) => !names.has(name) && !expoModules.some((module) => module.name === name))
47107
+ ],
46890
47108
  settings,
46891
47109
  autolinking: {
46892
47110
  reactNative: config,
@@ -46982,6 +47200,18 @@ const findConfiguredPatches = async (root) => {
46982
47200
  return patches;
46983
47201
  };
46984
47202
  /**
47203
+ * Whether the app's package.json installs `name` from a `file:` folder. pnpm,
47204
+ * Yarn, and Bun copy the folder into node_modules where npm links it, and the
47205
+ * version stays the same while its code changes. A tarball has a version of
47206
+ * its own, like a package from a registry or Git.
47207
+ */
47208
+ const isFileFolderDependency = async (cwd, name) => {
47209
+ const manifest = await readManifest(cwd);
47210
+ const specifier = manifest?.dependencies?.[name] ?? manifest?.devDependencies?.[name] ?? manifest?.optionalDependencies?.[name];
47211
+ if (!specifier?.startsWith("file:")) return false;
47212
+ return fs$2.stat(path$1.resolve(cwd, specifier.slice(5))).then((stat) => stat.isDirectory(), () => false);
47213
+ };
47214
+ /**
46985
47215
  * Fingerprints the native interface JavaScript depends on, not the native
46986
47216
  * build: the runtime and its native settings, each registry package as
46987
47217
  * `name@version`, the codegen
@@ -47034,7 +47264,7 @@ const createInterfaceFingerprint = async (cwd, options, nativeInterface) => {
47034
47264
  continue;
47035
47265
  }
47036
47266
  const root = await fs$2.realpath(dependency.root);
47037
- if (root.split(path$1.sep).includes("node_modules")) {
47267
+ if (isInNodeModules(root) && !await isFileFolderDependency(cwd, dependency.name)) {
47038
47268
  const version = (await readManifest(root))?.version ?? "unknown";
47039
47269
  addContents(`dependency:${dependency.name}`, `${dependency.name}@${version}`, "native-dependency");
47040
47270
  continue;
@@ -47419,6 +47649,13 @@ const resolveConfig = (config, scopes) => {
47419
47649
  Object.defineProperty(resolved, DEPLOY_SECRET_NAMES, { value: [...new Set(scopes.flatMap((scope) => [...scope.names ?? []]))].filter((name) => !buildNames.includes(name)) });
47420
47650
  return resolved;
47421
47651
  };
47652
+ /**
47653
+ * The names the config's build credentials reference, such as
47654
+ * `release-keystore`, which builds read from the `secrets` adapter for their
47655
+ * kind: `signing`'s, and each scheme's `signing.ascApiKey` and
47656
+ * `signing.keystore`.
47657
+ */
47658
+ const getBuildSecretNames = (config) => [...new Set(buildSecretNames(config))];
47422
47659
  const DEPLOY_SECRET_NAMES = Symbol.for("@hot-updater/cli-tools/deploy-secret-names");
47423
47660
  /**
47424
47661
  * The names the config's `secret()`s reference for its adapters, such as
@@ -47521,8 +47758,82 @@ const generateSchema = ({ database, plugins }, version, name) => {
47521
47758
  return database.generateSchema(version, name, toolingTargetOf(plugins));
47522
47759
  };
47523
47760
  //#endregion
47761
+ //#region src/expoSdk.ts
47762
+ /**
47763
+ * The lowest Expo SDK Hot Updater supports, the floor of `@hot-updater/expo`'s
47764
+ * `expo` peer range. doctor, the config plugin and the build adapter read it.
47765
+ */
47766
+ const MINIMUM_EXPO_SDK = 55;
47767
+ /** The command that moves a project to the minimum Expo SDK. */
47768
+ const EXPO_SDK_UPGRADE_COMMAND = `npx expo install expo@^55.0.0 --fix`;
47769
+ const readJson = (file) => {
47770
+ try {
47771
+ return JSON.parse(Ei.readFileSync(file, "utf-8"));
47772
+ } catch {
47773
+ return null;
47774
+ }
47775
+ };
47776
+ /**
47777
+ * The `expo` a build in `cwd` loads: the nearest `node_modules/expo` up from
47778
+ * it, as Node resolves a package. `NODE_PATH` isn't read, since package
47779
+ * managers point it at a store that isn't the project's.
47780
+ */
47781
+ const installedExpoVersion = (cwd) => {
47782
+ let dir = path$1.resolve(cwd);
47783
+ while (true) {
47784
+ const manifest = readJson(path$1.join(dir, "node_modules", "expo", "package.json"));
47785
+ if (manifest?.version) return manifest.version;
47786
+ const parent = path$1.dirname(dir);
47787
+ if (parent === dir) return null;
47788
+ dir = parent;
47789
+ }
47790
+ };
47791
+ /**
47792
+ * The lowest version a declared range allows, for an exact version, `^`, `~`,
47793
+ * `>=`, `=` or an x-range. Null for anything else, such as `catalog:sdk55`,
47794
+ * `workspace:*`, `latest` or a union, whose digits name no version.
47795
+ */
47796
+ const lowestOfRange = (range) => {
47797
+ const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:-[0-9A-Za-z.-]+)?\s*$/u.exec(range);
47798
+ if (!match) return null;
47799
+ const part = (value) => value === void 0 || value === "x" || value === "*" ? "0" : value;
47800
+ return `${match[1]}.${part(match[2])}.${part(match[3])}`;
47801
+ };
47802
+ /**
47803
+ * A project's Expo SDK: the installed `expo` the project resolves, else the
47804
+ * lowest version its declared range allows. Null without either.
47805
+ */
47806
+ const readExpoSdk = (cwd) => {
47807
+ const installed = installedExpoVersion(cwd);
47808
+ if (installed !== null) {
47809
+ const sdk = Number.parseInt(installed, 10);
47810
+ return Number.isNaN(sdk) ? null : {
47811
+ sdk,
47812
+ version: installed,
47813
+ installed: true
47814
+ };
47815
+ }
47816
+ const manifest = readJson(path$1.join(cwd, "package.json"));
47817
+ const declared = manifest?.dependencies?.["expo"] ?? manifest?.devDependencies?.["expo"];
47818
+ const lowest = declared === void 0 ? null : lowestOfRange(declared);
47819
+ if (lowest === null) return null;
47820
+ return {
47821
+ sdk: Number.parseInt(lowest, 10),
47822
+ version: lowest,
47823
+ installed: false
47824
+ };
47825
+ };
47826
+ /**
47827
+ * Refuses a project whose installed Expo is below the minimum SDK. A project
47828
+ * without Expo installed passes: nothing could build it yet.
47829
+ */
47830
+ const assertSupportedExpoSdk = (cwd) => {
47831
+ const expo = readExpoSdk(cwd);
47832
+ if (expo?.installed && expo.sdk < 55) throw new Error(`Hot Updater requires Expo SDK 55 or later. Run:\n${EXPO_SDK_UPGRADE_COMMAND}`);
47833
+ };
47834
+ //#endregion
47524
47835
  //#region src/log.ts
47525
- const mask = (message) => typeof message === "string" ? maskSecretValues(message) : message;
47836
+ const mask = (message) => typeof message === "string" ? fitText(maskSecretValues(message)) : message;
47526
47837
  const log = {
47527
47838
  normal: (message) => console.log(mask(message)),
47528
47839
  success: (message) => console.log(import_picocolors.default.green(mask(message))),
@@ -47555,6 +47866,10 @@ const formatComment = (comment) => {
47555
47866
  if (comment.includes("\0") || comment.includes("\r") || comment.includes("\n")) throw new Error("Environment comments cannot contain NUL or newlines.");
47556
47867
  return `# ${comment}`;
47557
47868
  };
47869
+ /**
47870
+ * Replaces `filePath` whole or not at all, through a new 0600 file beside it,
47871
+ * so the result is 0600 whatever the mode of the file it replaces.
47872
+ */
47558
47873
  const writeEnvAtomically = async (filePath, content) => {
47559
47874
  const resolvedPath = path$1.resolve(filePath);
47560
47875
  const temporaryPath = path$1.join(path$1.dirname(resolvedPath), `.${path$1.basename(resolvedPath)}.${randomUUID()}.tmp`);
@@ -47632,11 +47947,96 @@ const makeEnv = async (newEnvVars, filePath = ".env.hotupdater", options) => {
47632
47947
  await writeEnvAtomically(resolvedFilePath, updatedContent);
47633
47948
  return updatedContent;
47634
47949
  } catch (error) {
47635
- console.error("Error while updating .env.hotupdater file:", error);
47950
+ p.log.error(`Error while updating .env.hotupdater file: ${error instanceof Error ? error.message : String(error)}`);
47636
47951
  throw error;
47637
47952
  }
47638
47953
  };
47639
47954
  //#endregion
47955
+ //#region src/mergeEnvFile.ts
47956
+ const BARE_VALUE = /^[A-Za-z0-9_./:@%+,=-]+$/u;
47957
+ /**
47958
+ * `value` as an env file's value, in the first form `parseLoadedEnv` reads
47959
+ * back exactly: bare, single-quoted on one line, double-quoted with its
47960
+ * newlines as `\n`, in backticks, or single-quoted across lines. Undefined
47961
+ * for a value no form holds, such as one with every quote mark and a newline.
47962
+ */
47963
+ const formatEnvValue = (value) => {
47964
+ return [
47965
+ ...BARE_VALUE.test(value) ? [value] : [],
47966
+ ...value.includes("\n") ? [] : [`'${value}'`],
47967
+ `"${value.replaceAll("\n", "\\n")}"`,
47968
+ `\`${value}\``,
47969
+ `'${value}'`
47970
+ ].find((form) => parseLoadedEnv(`V=${form}\n`)["V"] === value);
47971
+ };
47972
+ const ASSIGNMENT = /^\s*(?:export\s+)?([^\s=#]+)[ \t]*=[ \t]*/u;
47973
+ /** The file's assignments in order, read as `parseLoadedEnv` reads them. */
47974
+ const scanEntries = (content) => {
47975
+ const entries = [];
47976
+ const lineEnd = (from) => {
47977
+ const newline = content.indexOf("\n", from);
47978
+ return newline === -1 ? content.length : newline;
47979
+ };
47980
+ let position = 0;
47981
+ while (position < content.length) {
47982
+ const end = lineEnd(position);
47983
+ const match = ASSIGNMENT.exec(content.slice(position, end));
47984
+ if (!match) {
47985
+ position = end + 1;
47986
+ continue;
47987
+ }
47988
+ const start = position + match[0].length;
47989
+ const quote = content[start];
47990
+ const closing = quote === "\"" || quote === "'" || quote === "`" ? content.indexOf(quote, start + 1) : -1;
47991
+ if (closing !== -1) {
47992
+ entries.push({
47993
+ key: match[1],
47994
+ start,
47995
+ end: closing + 1
47996
+ });
47997
+ position = lineEnd(closing + 1) + 1;
47998
+ continue;
47999
+ }
48000
+ const comment = content.indexOf("#", start);
48001
+ const stop = comment !== -1 && comment < end ? comment : end;
48002
+ entries.push({
48003
+ key: match[1],
48004
+ start,
48005
+ end: start + content.slice(start, stop).trimEnd().length
48006
+ });
48007
+ position = end + 1;
48008
+ }
48009
+ return entries;
48010
+ };
48011
+ const sameEnv = (left, right) => Object.keys(left).length === Object.keys(right).length && Object.keys(left).every((key) => Object.hasOwn(right, key) && left[key] === right[key]);
48012
+ /**
48013
+ * `content`, an env file, with `values` set, already as `formatEnvValue`
48014
+ * gives them: a variable it assigns has only that value replaced, the last
48015
+ * assignment when it repeats, which is the one that counts, and a new one
48016
+ * goes at the end. Every other byte stays: other lines, comments, an
48017
+ * `export`, an inline comment. Undefined when `parseLoadedEnv` wouldn't read
48018
+ * exactly that back, so a file this can't follow is never rewritten.
48019
+ */
48020
+ const mergeEnvContent = (content, values) => {
48021
+ const entries = scanEntries(content);
48022
+ const last = new Map(entries.map((entry) => [entry.key, entry]));
48023
+ const replaced = [...values.keys()].flatMap((key) => last.get(key) ?? []).sort((left, right) => right.start - left.start);
48024
+ let merged = content;
48025
+ for (const entry of replaced) {
48026
+ const gap = merged[entry.end] === "#" ? " " : "";
48027
+ merged = `${merged.slice(0, entry.start)}${values.get(entry.key)}${gap}${merged.slice(entry.end)}`;
48028
+ }
48029
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
48030
+ const added = [...values].filter(([key]) => !last.has(key));
48031
+ if (added.length > 0) {
48032
+ const gap = merged === "" || merged.endsWith("\n") ? "" : eol;
48033
+ merged += `${gap}${added.map(([key, value]) => `${key}=${value}${eol}`).join("")}`;
48034
+ }
48035
+ const expected = { ...parseLoadedEnv(content) };
48036
+ for (const [key, value] of values) expected[key] = parseLoadedEnv(`V=${value}\n`)["V"] ?? "";
48037
+ return sameEnv(parseLoadedEnv(merged), expected) ? merged : void 0;
48038
+ };
48039
+ //#endregion
47640
48040
  //#region src/minBundleId.ts
47641
48041
  /**
47642
48042
  * The MIN_BUNDLE_ID a native build compiles in, which is also the ID of its
@@ -47840,4 +48240,4 @@ function transformTemplate(templateString, values) {
47840
48240
  return result;
47841
48241
  }
47842
48242
  //#endregion
47843
- export { BUNDLE_SIGNING_KEY_SECRET, BuildFailedError, BuildLogger, BundleSigningKeyError, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, INVALID_SIGNING_MESSAGE, INVALID_SIGNING_NEXT_STEP, ISSUE_SIGNING_KEY_COMMAND, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MANAGED_CLIENT_CREDENTIAL_ENV, MESSAGE_WIDTH, MissingInitInputsError, PUT_SIGNING_KEY_COMMAND, ServerDefinitionError, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, bundleSigningPutCommand, bundleSigningSecretRef, clientAuthOf, typedColors as colors, configSetsTeamSecrets, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createStep, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeMissingSecret, describeSecret, describeStoredSecret, displayWidth, encryptJson, ensureInstallPackages, failActiveStep, fitMessage, formatCommand, formatDuration, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getDeploySecretNames, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isSecretRef, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readBundleSigningState, readHotUpdaterInitEnv, readLoadedEnvFile, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, readSecrets, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secret, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, startStep, storeEnvFileSecrets, stripAnsi, transformEnv, transformTemplate, truncateLine, withCredentialResolver, withDuration, withSecretsScope, withStep, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
48243
+ export { BUNDLE_SIGNING_KEY_SECRET, BuildFailedError, BuildLogger, BundleSigningKeyError, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, EXPO_SDK_UPGRADE_COMMAND, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, INVALID_SIGNING_MESSAGE, INVALID_SIGNING_NEXT_STEP, ISSUE_SIGNING_KEY_COMMAND, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MANAGED_CLIENT_CREDENTIAL_ENV, MESSAGE_WIDTH, MINIMUM_EXPO_SDK, MissingInitInputsError, PUT_SIGNING_KEY_COMMAND, ServerDefinitionError, UNSIGNED_BUNDLES_NEXT_STEP, UNSIGNED_BUNDLES_WARNING, addSigningSecretToConfig, addSigningSecretToConfigText, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertSupportedExpoSdk, assertUnreservedEnv, autolinkingConfigCommand, banner, bundleSigningPutCommand, bundleSigningSecretRef, clientAuthOf, typedColors as colors, configSetsTeamSecrets, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createStep, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeMissingSecret, describeSecret, describeStoredSecret, displayWidth, encryptJson, ensureInstallPackages, failActiveStep, fitMessage, fitText, formatDuration, formatEnvValue, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBuildSecretNames, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getDeploySecretNames, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isCommandLine, isReservedVariableName, isSecretName, isSecretRef, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, mergeEnvContent, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readBundleSigningState, readExpoSdk, readHotUpdaterInitEnv, readLoadedEnvFile, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, readSecrets, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secret, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, startStep, storeEnvFileSecrets, stripAnsi, toolOutput, transformEnv, transformTemplate, truncateLine, withCredentialResolver, withDuration, withProviderMessages, withSecretsScope, withStep, writeEnvAtomically, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "1.2.0-rc.12",
3
+ "version": "1.2.0-rc.14",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
@@ -49,9 +49,9 @@
49
49
  "oxc-parser": "0.141.0",
50
50
  "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/protocol": "1.2.0-rc.12",
53
- "@hot-updater/plugin-core": "1.2.0-rc.12",
54
- "@hot-updater/server": "1.2.0-rc.12"
52
+ "@hot-updater/protocol": "1.2.0-rc.14",
53
+ "@hot-updater/plugin-core": "1.2.0-rc.14",
54
+ "@hot-updater/server": "1.2.0-rc.14"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@clack/prompts": "1.7.0",
@@ -65,7 +65,7 @@
65
65
  "tar": "^7.5.16",
66
66
  "verkit": "0.4.0",
67
67
  "workspace-tools": "^0.41.7",
68
- "@hot-updater/test-utils": "1.2.0-rc.12"
68
+ "@hot-updater/test-utils": "1.2.0-rc.14"
69
69
  },
70
70
  "inlinedDependencies": {
71
71
  "@babel/code-frame": "7.29.0",