Skip to content

Commit 4f6ea16

Browse files
luoqingmingclaude
andcommitted
fix: 审计修复 — 启动警告、非交互死循环、--no-sourcemap 与安全护栏
- api/runtime/symbolicate: node-fetch 改为按需加载,Node >= 21 下每条命令 不再打印 punycode DEP0040 弃用警告;`help` 启动加载模块 171 -> 129 - utils: 版本横幅只解析 react-native-update 一个依赖,不再为每条命令读取 项目全部依赖的 package.json - app/package/user: chooseApp/choosePackage 在非交互模式下直接报错(此前 question 返回空串导致 while(true) 死循环);login 缺凭据时报错而不是把 空账号密码发到服务端;selectApp 校验 id 为正整数 - bundle: `--no-sourcemap` 真正生效(cli-arguments 以"键存在但为 undefined" 表达 --no-<flag>,之前回落到默认 true);拒绝多余位置参数,避免 `--sourcemap false` 静默保持 sourcemap 开启;移除不可达的 platform 检查 - bundle-runner: 清空中间目录前拒绝项目目录本身/其上级/家目录/根目录 - versions: ppk 上传失败时也清理打包的 source map 临时目录 - symbolicate: 无文件参数且 stdin 为终端时报用法错误,不再挂起 - cli.json: deleteApp 声明 --platform(此前被解析器拒绝为未知选项) - provider: bundle 的 sourcemap 默认值与 CLI/README 一致(默认开启) - types/README: CLIProvider 补 listPackages,README 接口块与 types.ts 同步 - locales: 删除 14 个未使用的文案键;add-gitignore 识别 /.pushy 与 .pushy/ - 删除不可达的 src/index.ts(exports map 未暴露);biome 配置迁移到 preset - tests: e2e 用 bun --no-install 消除 @pnpm/npm-conf require.resolve('npm') 触发 auto-install 造成的偶发失败;新增 12 个用例 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tusm6iL2itjJZDiemujAeL
1 parent 31201ef commit 4f6ea16

30 files changed

Lines changed: 463 additions & 99 deletions

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const publishResult = await provider.publish({
8585
- `hdiffFromApp`: Generate hdiff from APP files
8686
- `hdiffFromIpa`: Generate hdiff from IPA files
8787

88-
Hermes projects: `bundle` always runs hermesc with `-output-source-map`, so the debug info section is stripped from the bytecode (15–40% smaller, same as React Native's own release builds). The Hermes sourcemap stays in the intermediate directory (`.pushy/intermedia/<platform>/<bundle>.map`, never packed into the ppk) and is composed with the packager map — `--sourcemap` is on by default since 2.23 (`--sourcemap false` opts out). When `bundle` publishes, that final map is uploaded and archived with the version (`sourceMapKey`), so `pushy symbolicate` can map crash stacks — including Hermes `address at` frames — back to source later. `pushy publish <ppk> --sourcemap <file.map>` archives a map for a ppk built elsewhere; publishing without a map prints a warning.
88+
Hermes projects: `bundle` always runs hermesc with `-output-source-map`, so the debug info section is stripped from the bytecode (15–40% smaller, same as React Native's own release builds). The Hermes sourcemap stays in the intermediate directory (`.pushy/intermedia/<platform>/<bundle>.map`, never packed into the ppk) and is composed with the packager map — `--sourcemap` is on by default since 2.23 (`--no-sourcemap` opts out). When `bundle` publishes, that final map is uploaded and archived with the version (`sourceMapKey`), so `pushy symbolicate` can map crash stacks — including Hermes `address at` frames — back to source later. `pushy publish <ppk> --sourcemap <file.map>` archives a map for a ppk built elsewhere; publishing without a map prints a warning.
8989

9090
Hermes delta mode (`-base-bytecode`): by default (`--hermesBase auto`) `bundle` compiles against the previous HBC of the same app, which keeps Hermes string IDs stable and makes hot-update patches 5–30× smaller. The base comes from the server (`GET /app/:id/hermesBase`), verified by sha256 and kept in a local cache (`.pushy/cache/<sha256>`, 500 MB / 20 files, `PUSHY_CACHE_DIR` / `--cacheMaxMb` to tune, `pushy cache [clean]` to inspect or clear). `--hermesBase none` disables it; `--hermesBase <file.hbc|.ppk|.apk|.ipa>` uses a local artifact (for example the store build). `--verifyHermesBase` (default on) additionally compiles without the base (concurrently with the base compile) and compares both disassemblies; on any mismatch or failure the CLI silently falls back to the plain compile, so the feature can never block a release. Only hermesc builds that include the upstream delta-mode fix are used (classic `react-native/sdks/hermesc`, or `hermes-compiler` ≥ 250829098). If a base compile fails, the full hermesc output is written to `hermes-base-error.log` next to the intermediate directory. `--resetCache false` skips Metro's `--reset-cache` and reuses its transform cache, which makes repeated bundles much faster.
9191

@@ -131,20 +131,23 @@ Hermes delta mode (`-base-bytecode`): by default (`--hermesBase auto`) `bundle`
131131
interface CLIProvider {
132132
bundle(options: BundleOptions): Promise<CommandResult>;
133133
publish(options: PublishOptions): Promise<CommandResult>;
134+
symbolicate(options: SymbolicateOptions): Promise<CommandResult>;
134135
upload(options: UploadOptions): Promise<CommandResult>;
135136

137+
createApp(name: string, platform: Platform): Promise<CommandResult>;
138+
listApps(platform?: Platform): Promise<CommandResult>;
136139
getSelectedApp(
137140
platform?: Platform,
141+
config?: string,
138142
): Promise<{ appId: string; platform: Platform }>;
139-
listApps(platform?: Platform): Promise<CommandResult>;
140-
createApp(name: string, platform: Platform): Promise<CommandResult>;
141143

142144
listVersions(appId: string): Promise<CommandResult>;
143145
updateVersion(
144146
appId: string,
145147
versionId: string,
146-
updates: Partial<Version>,
148+
updates: UpdateVersionOptions,
147149
): Promise<CommandResult>;
150+
listPackages(appId?: string): Promise<CommandResult>;
148151

149152
getPlatform(platform?: Platform): Promise<Platform>;
150153
loadSession(): Promise<Session>;

README.zh-CN.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const publishResult = await provider.publish({
7676
- `hdiffFromApp`: 基于 APP 文件生成 hdiff
7777
- `hdiffFromIpa`: 基于 IPA 文件生成 hdiff
7878

79-
Hermes 工程:`bundle` 调用 hermesc 时始终带 `-output-source-map`,因此字节码不含 debug info 段(小 15%~40%,与 React Native 自身 release 构建一致)。Hermes sourcemap 保留在中间目录(`.pushy/intermedia/<platform>/<bundle>.map`,不会打进 ppk),并与 packager map 合成——自 2.23 起 `--sourcemap` 默认开启(`--sourcemap false` 关闭)。`bundle` 发布时会把这份最终 map 上传并随版本归档(`sourceMapKey`),之后用 `pushy symbolicate` 即可把崩溃堆栈(含 Hermes 的 `address at` 帧)还原到源码。别处打好的 ppk 可用 `pushy publish <ppk> --sourcemap <file.map>` 归档;不带 map 发布会打印警告。
79+
Hermes 工程:`bundle` 调用 hermesc 时始终带 `-output-source-map`,因此字节码不含 debug info 段(小 15%~40%,与 React Native 自身 release 构建一致)。Hermes sourcemap 保留在中间目录(`.pushy/intermedia/<platform>/<bundle>.map`,不会打进 ppk),并与 packager map 合成——自 2.23 起 `--sourcemap` 默认开启(`--no-sourcemap` 关闭)。`bundle` 发布时会把这份最终 map 上传并随版本归档(`sourceMapKey`),之后用 `pushy symbolicate` 即可把崩溃堆栈(含 Hermes 的 `address at` 帧)还原到源码。别处打好的 ppk 可用 `pushy publish <ppk> --sourcemap <file.map>` 归档;不带 map 发布会打印警告。
8080

8181
Hermes delta 模式(`-base-bytecode`):默认 `--hermesBase auto`,`bundle` 会以同一应用上一版的 HBC 为 base 编译,让 Hermes 字符串 ID 跨版本稳定,热更 patch 可缩小 5~30 倍。base 由服务端(`GET /app/:id/hermesBase`)给出、按 sha256 校验并存入本地缓存(`.pushy/cache/<sha256>`,默认 500 MB / 20 个,可用 `PUSHY_CACHE_DIR` / `--cacheMaxMb` 调整,`pushy cache [clean]` 查看或清空)。`--hermesBase none` 关闭;`--hermesBase <file.hbc|.ppk|.apk|.ipa>` 指定本地文件(比如商店包)作 base。`--verifyHermesBase`(默认开)会并行再做一次普通编译并比对两份反汇编;任何不一致或失败都静默回退到普通编译,不会阻塞发版。只有包含上游 delta 模式修复的 hermesc 才会启用(经典 `react-native/sdks/hermesc`,或 `hermes-compiler` ≥ 250829098)。base 编译失败时,完整的 hermesc 输出会写到中间目录旁边的 `hermes-base-error.log`。`--resetCache false` 可跳过 Metro 的 `--reset-cache`,复用其转换缓存,重复打包会快很多。
8282

@@ -122,20 +122,23 @@ Hermes delta 模式(`-base-bytecode`):默认 `--hermesBase auto`,`bundle
122122
interface CLIProvider {
123123
bundle(options: BundleOptions): Promise<CommandResult>;
124124
publish(options: PublishOptions): Promise<CommandResult>;
125+
symbolicate(options: SymbolicateOptions): Promise<CommandResult>;
125126
upload(options: UploadOptions): Promise<CommandResult>;
126127

128+
createApp(name: string, platform: Platform): Promise<CommandResult>;
129+
listApps(platform?: Platform): Promise<CommandResult>;
127130
getSelectedApp(
128131
platform?: Platform,
132+
config?: string,
129133
): Promise<{ appId: string; platform: Platform }>;
130-
listApps(platform?: Platform): Promise<CommandResult>;
131-
createApp(name: string, platform: Platform): Promise<CommandResult>;
132134

133135
listVersions(appId: string): Promise<CommandResult>;
134136
updateVersion(
135137
appId: string,
136138
versionId: string,
137-
updates: Partial<Version>,
139+
updates: UpdateVersionOptions,
138140
): Promise<CommandResult>;
141+
listPackages(appId?: string): Promise<CommandResult>;
139142

140143
getPlatform(platform?: Platform): Promise<Platform>;
141144
loadSession(): Promise<Session>;

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"linter": {
66
"enabled": true,
77
"rules": {
8-
"recommended": true,
8+
"preset": "recommended",
99
"suspicious": {
1010
"noExplicitAny": "off",
1111
"noAssignInExpressions": "off",

cli.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
}
2424
}
2525
},
26-
"deleteApp": {},
26+
"deleteApp": {
27+
"options": {
28+
"platform": {
29+
"hasValue": true
30+
}
31+
}
32+
},
2733
"selectApp": {
2834
"options": {
2935
"platform": {

src/api.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import filesizeParser from 'filesize-parser';
22
import fs from 'fs';
3-
import fetch from 'node-fetch';
3+
import type {
4+
RequestInit as NodeFetchRequestInit,
5+
Response as NodeFetchResponse,
6+
} from 'node-fetch';
47
import path from 'path';
58
import type ProgressBar from 'progress';
69
import packageJson from '../package.json';
@@ -255,6 +258,17 @@ class UploadTimeoutError extends Error {
255258
}
256259
}
257260

261+
/**
262+
* node-fetch is only needed for the streaming multipart / PUT upload below
263+
* (the built-in fetch cannot stream a form-data body). Loading it eagerly
264+
* pulled whatwg-url → punycode into every command, which Node ≥ 21 greets
265+
* with a DEP0040 deprecation warning on stderr; so it is loaded on first use.
266+
*/
267+
function loadNodeFetch(): typeof import('node-fetch').default {
268+
const mod = require('node-fetch');
269+
return (mod.default ?? mod) as typeof import('node-fetch').default;
270+
}
271+
258272
/**
259273
* Send the file with a size-scaled deadline and one retry on a transient
260274
* network error. `buildRequest` is invoked per attempt with a fresh file
@@ -265,9 +279,10 @@ async function sendUpload(
265279
realUrl: string,
266280
fileSize: number,
267281
bar: ProgressBar,
268-
buildRequest: (fileStream: fs.ReadStream) => fetch.RequestInit,
269-
): Promise<fetch.Response> {
282+
buildRequest: (fileStream: fs.ReadStream) => NodeFetchRequestInit,
283+
): Promise<NodeFetchResponse> {
270284
const timeoutMs = uploadTimeoutMs(fileSize);
285+
const nodeFetch = loadNodeFetch();
271286
for (let attempt = 0; ; attempt++) {
272287
const controller = new AbortController();
273288
let timedOut = false;
@@ -280,7 +295,7 @@ async function sendUpload(
280295
bar.tick(data.length);
281296
});
282297
try {
283-
return await fetch(realUrl, {
298+
return await nodeFetch(realUrl, {
284299
...buildRequest(fileStream),
285300
signal: controller.signal,
286301
});
@@ -361,7 +376,7 @@ export async function uploadFile(
361376

362377
// 自托管节点的 s3 直传:服务端下发预签名 PUT,字节直达用户的对象存储
363378
if (resp.method === 'PUT') {
364-
let putRes: fetch.Response;
379+
let putRes: NodeFetchResponse;
365380
try {
366381
putRes = await sendUpload(fn, realUrl, fileSize, bar, (fileStream) => ({
367382
method: 'PUT',
@@ -385,7 +400,7 @@ export async function uploadFile(
385400
}
386401

387402
const FormData = require('form-data') as typeof import('form-data');
388-
let res: fetch.Response;
403+
let res: NodeFetchResponse;
389404
try {
390405
res = await sendUpload(fn, realUrl, fileSize, bar, (fileStream) => {
391406
const form = new FormData();

src/app.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs';
22
import { doDelete, get, post } from './api';
33
import type { Platform } from './types';
4-
import { loadTtyTable, question } from './utils';
4+
import { isNonInteractive, loadTtyTable, question } from './utils';
55
import { updateJson } from './utils/constants';
66
import { t } from './utils/i18n';
77

@@ -144,6 +144,11 @@ export async function listApp(platform: Platform | '' = '') {
144144
/** Prompt until the user chooses an app belonging to the target platform. */
145145
export async function chooseApp(platform: Platform) {
146146
const list = await listApp(platform);
147+
// without a terminal `question` answers '' and no app has that id: the
148+
// loop below would never end
149+
if (isNonInteractive()) {
150+
throw new Error(t('appIdRequired'));
151+
}
147152

148153
while (true) {
149154
const id = await question(t('enterAppIdQuestion'));
@@ -166,6 +171,9 @@ async function selectApp({
166171
const id = args[0]
167172
? Number.parseInt(args[0], 10)
168173
: (await chooseApp(platform)).id;
174+
if (!Number.isInteger(id) || id <= 0) {
175+
throw new Error(t('invalidId', { id: args[0] }));
176+
}
169177

170178
const configPath = options.config || updateJson;
171179
let updateInfo: Partial<Record<Platform, { appId: number; appKey: string }>> =

src/bundle-runner.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export async function runReactNativeBundleCommand({
230230
reactNativeBundleArgs.push(...envArgs.trim().split(/\s+/));
231231
}
232232

233+
assertSafeToEmpty(outputFolder);
233234
fs.emptyDirSync(outputFolder);
234235

235236
let cliPath = '';
@@ -410,6 +411,30 @@ export async function runReactNativeBundleCommand({
410411
return hermesResult;
411412
}
412413

414+
/**
415+
* The intermediate directory is emptied before every bundle. Refuse to do that
416+
* to the project itself (`--intermediaDir .`), to anything above it, to the
417+
* home directory or to the filesystem root: one typo must not wipe a working
418+
* tree.
419+
*/
420+
export function assertSafeToEmpty(dir: string, cwd = process.cwd()): void {
421+
const target = path.resolve(cwd, dir);
422+
const contains = (parent: string, child: string) => {
423+
const relative = path.relative(parent, child);
424+
return (
425+
relative === '' ||
426+
(!relative.startsWith('..') && !path.isAbsolute(relative))
427+
);
428+
};
429+
if (
430+
target === path.parse(target).root ||
431+
target === path.resolve(os.homedir()) ||
432+
contains(target, path.resolve(cwd))
433+
) {
434+
throw new Error(t('unsafeIntermediateDir', { dir: target }));
435+
}
436+
}
437+
413438
async function detectHermesEnabled(
414439
platform: string,
415440
forceHermes: boolean | undefined,

src/bundle.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ type NormalizedBundleOptions = {
5757
sentryDist?: string;
5858
};
5959

60+
/** `--no-<flag>` as parsed by cli-arguments: the key is present without a value. */
61+
function isClearedFlag(options: Record<string, unknown>, key: string): boolean {
62+
return key in options && options[key] === undefined;
63+
}
64+
6065
/** Parse a positive cache-size option expressed in megabytes. */
6166
function parseCacheMaxMb(value: unknown): number | undefined {
6267
const parsed = typeof value === 'string' ? Number(value) : (value as number);
@@ -117,8 +122,13 @@ export function normalizeBundleOptions(
117122
),
118123
dev: getBooleanOption(translatedOptions, 'dev', false) ? 'true' : 'false',
119124
// On by default since 2.23: the map is archived with the published
120-
// version (pushy symbolicate). --no-sourcemap / --sourcemap=false opts out.
121-
sourcemap: getBooleanOption(translatedOptions, 'sourcemap', true),
125+
// version (pushy symbolicate). `--no-sourcemap` opts out: cli-arguments
126+
// implements `--no-<flag>` by clearing the flag's value, so a key that is
127+
// present but undefined is the opt-out and only a missing key means
128+
// "default" (`--sourcemap false` would leave the flag on, see `bundle`).
129+
sourcemap: isClearedFlag(translatedOptions, 'sourcemap')
130+
? false
131+
: getBooleanOption(translatedOptions, 'sourcemap', true),
122132
taro: getBooleanOption(translatedOptions, 'taro', false),
123133
expo: getBooleanOption(translatedOptions, 'expo', false),
124134
rncli: getBooleanOption(translatedOptions, 'rncli', false),
@@ -209,11 +219,19 @@ async function publishBundleVersion(
209219
export const bundleCommands = {
210220
/** Build a bundle and optionally publish it to one operation-scoped app. */
211221
bundle: async ({
222+
args = [],
212223
options,
213224
}: {
214225
args?: string[];
215226
options: Record<string, unknown>;
216227
}) => {
228+
// Boolean flags take no value: `--sourcemap false` leaves the flag on and
229+
// turns "false" into a stray argument. Refuse it rather than silently
230+
// bundling with the wrong settings (a flag is switched off with
231+
// `--no-<flag>`).
232+
if (args.length > 0) {
233+
throw new Error(t('bundleUnexpectedArgs', { args: args.join(' ') }));
234+
}
217235
const platform = await getPlatform(
218236
typeof options.platform === 'string' ? options.platform : undefined,
219237
);
@@ -276,10 +294,6 @@ export const bundleCommands = {
276294
`${Date.now()}`,
277295
);
278296

279-
if (!platform) {
280-
throw new Error(t('platformRequired'));
281-
}
282-
283297
console.log(t('bundlingWithRN', { version: depVersions['react-native'] }));
284298

285299
const hermesResult = await runReactNativeBundleCommand({

src/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/locales/en.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,12 @@ export default {
5151
bundlingWithRN: 'Bundling with react-native: {{version}}',
5252
cancelled: 'Cancelled',
5353
composingSourceMap: 'Composing source map',
54-
copyFileFailed: 'Failed to copy file: {{error}}',
55-
copyHarmonyBundleError: 'Error copying Harmony bundle: {{error}}',
5654
copyingDebugId: 'Copying debugid',
5755
createAppSuccess: 'App created successfully (id: {{id}})',
5856
deleteFile: 'Delete {{- file}}',
59-
deletingFile: 'Delete {{- file}}',
6057
enterAppIdQuestion: 'Enter AppId:',
6158
enterNativePackageId: 'Enter native package ID:',
62-
errorInHarmonyApp: 'Error in getEntryFromHarmonyApp: {{error}}',
6359
expiredStatus: '(Expired)',
64-
failedToParseIcon: '[Warning] failed to parse icon: {{error}}',
6560
failedToParseUpdateJson:
6661
'Failed to parse file `{{- configPath}}`. Try to remove it manually.',
6762
fileGenerated: '{{- file}} generated.',
@@ -71,7 +66,6 @@ export default {
7166
hermesEnabledCompiling: 'Hermes enabled, now compiling to hermes bytecode:\n',
7267
ipaUploadSuccess:
7368
'Successfully uploaded IPA native package (id: {{id}}, version: {{version}}, buildTime: {{buildTime}})',
74-
keyStrings: 'Key strings:',
7569
latestVersionTag: '(latest: {{version}})',
7670
lockBestPractice: `
7771
Best practices for lock files:
@@ -125,15 +119,9 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
125119
packing: 'Packing',
126120
pausedStatus: '(Paused)',
127121
platform: 'Platform',
128-
platformPrompt: 'Platform (ios/android/harmony):',
129122
platformQuestion: 'Platform(ios/android/harmony):',
130-
platformRequired: 'Platform must be specified.',
131123
pluginDetectionError: 'error while detecting {{name}} plugin: {{error}}',
132124
pluginDetected: 'detected {{name}} plugin',
133-
ppkPackageGenerated: 'ppk package generated and saved to: {{- output}}',
134-
processingError: 'Error processing file: {{error}}',
135-
processingPackage: 'Processing the package {{count}} ...',
136-
processingStringPool: 'Processing the string pool ...',
137125
publishUsage:
138126
'Usage: pushy publish <ppk file> --platform ios|android|harmony',
139127
sourceMapNotFound: 'Source map not found: {{path}}',
@@ -165,7 +153,6 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
165153
sentryReleaseCreated: 'Sentry release created for version: {{version}}',
166154
totalApps: 'Total {{count}} {{platform}} apps',
167155
totalPackages: 'Total {{count}} packages',
168-
typeStrings: 'Type strings:',
169156
unsupportedPlatform: 'Unsupported platform `{{platform}}`',
170157
uploadBundlePrompt: 'Upload this bundle now?(Y/N)',
171158
uploadingSourcemap: 'Uploading sourcemap',
@@ -176,7 +163,6 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
176163
'Usage: cresc extractApk <aab file> [--output <apk file>] [--includeAllSplits] [--splits <split names>]',
177164
usageParseApp: 'Usage: cresc parseApp <app file>',
178165
usageParseIpa: 'Usage: cresc parseIpa <ipa file>',
179-
usageUnderDevelopment: 'Usage is under development now.',
180166
usageUploadApk: 'Usage: cresc uploadApk <apk file>',
181167
usageUploadAab:
182168
'Usage: cresc uploadAab <aab file> [--includeAllSplits] [--splits <split names>]',
@@ -254,4 +240,10 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
254240
'Hermes base: bytecode differs from a plain compile; dropping the base and recompiling',
255241
cacheCleaned: 'Removed {{count}} cached bundle(s)',
256242
cacheStats: 'Bundle cache: {{- dir}} — {{files}} file(s), {{mb}} MB',
243+
loginCredentialsRequired:
244+
'Email and password are required. Usage: {{scriptName}} login <email> <password>',
245+
bundleUnexpectedArgs:
246+
'bundle takes no positional arguments, got: {{- args}}. Boolean flags take no value; switch one off with --no-<flag> (e.g. --no-sourcemap).',
247+
unsafeIntermediateDir:
248+
'Refusing to empty {{- dir}} as the intermediate directory: it is the project directory (or above it). Point --intermediaDir at a dedicated build directory.',
257249
};

0 commit comments

Comments
 (0)