@capawesome/cli 4.9.2 → 4.9.3-dev.47af0d1.1779135155

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [4.9.3](https://github.com/capawesome-team/cli/compare/v4.9.2...v4.9.3) (2026-05-14)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **apps:builds:create:** exclude native build artifact directories when zipping source ([#159](https://github.com/capawesome-team/cli/issues/159)) ([4c2a704](https://github.com/capawesome-team/cli/commit/4c2a7048d59d9e966f48d7c05af2e0f2d8d75379))
11
+ * check file read access to surface permission errors ([#158](https://github.com/capawesome-team/cli/issues/158)) ([242189a](https://github.com/capawesome-team/cli/commit/242189a21f19ecbab72a34d8ce13f23824f1a4bf))
12
+
5
13
  ## [4.9.2](https://github.com/capawesome-team/cli/compare/v4.9.1...v4.9.2) (2026-05-13)
6
14
 
7
15
  ## [4.9.1](https://github.com/capawesome-team/cli/compare/v4.9.0...v4.9.1) (2026-05-07)
@@ -5,8 +5,9 @@ import appCertificatesService from '../../../services/app-certificates.js';
5
5
  import appEnvironmentsService from '../../../services/app-environments.js';
6
6
  import { parseKeyValuePairs } from '../../../utils/app-environments.js';
7
7
  import { withAuth } from '../../../utils/auth.js';
8
+ import { createBufferFromPath } from '../../../utils/buffer.js';
8
9
  import { isInteractive } from '../../../utils/environment.js';
9
- import { fileExistsAtPath } from '../../../utils/file.js';
10
+ import { isDirectory, isReadable } from '../../../utils/file.js';
10
11
  import { waitForJobCompletion } from '../../../utils/job.js';
11
12
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
12
13
  import zip from '../../../utils/zip.js';
@@ -48,7 +49,7 @@ export default defineCommand({
48
49
  .optional()
49
50
  .describe('Download the generated IPA file (iOS only). Optionally provide a file path.'),
50
51
  json: z.boolean().optional().describe('Output in JSON format.'),
51
- path: z.string().optional().describe('Path to local source files to upload.'),
52
+ path: z.string().optional().describe('Path to local source files to upload. Must be a folder or a zip file.'),
52
53
  platform: z
53
54
  .enum(['ios', 'android', 'web'], {
54
55
  message: 'Platform must be either `ios`, `android`, or `web`.',
@@ -118,15 +119,22 @@ export default defineCommand({
118
119
  if (sourcePath) {
119
120
  consola.warn('The --path option is experimental and may change in the future.');
120
121
  const resolvedPath = path.resolve(sourcePath);
121
- const stat = await fs.stat(resolvedPath).catch(() => null);
122
- if (!stat || !stat.isDirectory()) {
123
- consola.error('The --path must point to an existing directory.');
122
+ const pathReadable = await isReadable(resolvedPath);
123
+ if (!pathReadable) {
124
+ consola.error(`The path does not exist or is not accessible: ${resolvedPath}`);
124
125
  process.exit(1);
125
126
  }
126
- const packageJsonPath = path.join(resolvedPath, 'package.json');
127
- const packageJsonStat = await fs.stat(packageJsonPath).catch(() => null);
128
- if (!packageJsonStat || !packageJsonStat.isFile()) {
129
- consola.error('The directory specified by --path must contain a package.json file.');
127
+ const pathIsDirectory = await isDirectory(resolvedPath);
128
+ if (pathIsDirectory) {
129
+ const packageJsonPath = path.join(resolvedPath, 'package.json');
130
+ const packageJsonReadable = await isReadable(packageJsonPath);
131
+ if (!packageJsonReadable) {
132
+ consola.error(`The path must contain a package.json file: ${packageJsonPath}`);
133
+ process.exit(1);
134
+ }
135
+ }
136
+ else if (!zip.isZipped(resolvedPath)) {
137
+ consola.error(`The path must be a folder or a zip file: ${resolvedPath}`);
130
138
  process.exit(1);
131
139
  }
132
140
  }
@@ -246,9 +254,9 @@ export default defineCommand({
246
254
  // Parse ad hoc environment variables from inline and file
247
255
  const variablesMap = new Map();
248
256
  if (options.variableFile) {
249
- const fileExists = await fileExistsAtPath(options.variableFile);
250
- if (!fileExists) {
251
- consola.error(`The variable file was not found or is not accessible: ${options.variableFile}`);
257
+ const variableFileReadable = await isReadable(options.variableFile);
258
+ if (!variableFileReadable) {
259
+ consola.error(`The variable file does not exist or is not accessible: ${options.variableFile}`);
252
260
  process.exit(1);
253
261
  }
254
262
  const fileContent = await fs.readFile(options.variableFile, 'utf-8');
@@ -271,8 +279,15 @@ export default defineCommand({
271
279
  // Upload source files if path is provided
272
280
  if (sourcePath) {
273
281
  const resolvedPath = path.resolve(sourcePath);
274
- consola.start('Zipping source files...');
275
- const buffer = await zip.zipFolderWithGitignore(resolvedPath);
282
+ const sourcePathIsDirectory = await isDirectory(resolvedPath);
283
+ let buffer;
284
+ if (sourcePathIsDirectory) {
285
+ consola.start('Zipping source files...');
286
+ buffer = await zip.zipFolderWithGitignore(resolvedPath);
287
+ }
288
+ else {
289
+ buffer = await createBufferFromPath(resolvedPath);
290
+ }
276
291
  consola.start('Uploading source files...');
277
292
  const appBuildSource = await appBuildSourcesService.createFromFile({
278
293
  appId,
@@ -2,7 +2,7 @@ import appCertificatesService from '../../../services/app-certificates.js';
2
2
  import appProvisioningProfilesService from '../../../services/app-provisioning-profiles.js';
3
3
  import { withAuth } from '../../../utils/auth.js';
4
4
  import { isInteractive } from '../../../utils/environment.js';
5
- import { fileExistsAtPath } from '../../../utils/file.js';
5
+ import { isReadable } from '../../../utils/file.js';
6
6
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
7
7
  import { defineCommand, defineOptions } from '@robingenz/zli';
8
8
  import consola from 'consola';
@@ -131,9 +131,9 @@ export default defineCommand({
131
131
  provisioningProfile = [profilePath];
132
132
  }
133
133
  }
134
- const fileExists = await fileExistsAtPath(file);
135
- if (!fileExists) {
136
- consola.error(`The certificate file was not found or is not accessible: ${file}`);
134
+ const fileReadable = await isReadable(file);
135
+ if (!fileReadable) {
136
+ consola.error(`The certificate file does not exist or is not accessible: ${file}`);
137
137
  process.exit(1);
138
138
  }
139
139
  const buffer = fs.readFileSync(file);
@@ -142,9 +142,9 @@ export default defineCommand({
142
142
  const provisioningProfileIds = [];
143
143
  if (provisioningProfile && provisioningProfile.length > 0) {
144
144
  for (const profilePath of provisioningProfile) {
145
- const profileExists = await fileExistsAtPath(profilePath);
146
- if (!profileExists) {
147
- consola.error(`The provisioning profile file was not found or is not accessible: ${profilePath}`);
145
+ const profileReadable = await isReadable(profilePath);
146
+ if (!profileReadable) {
147
+ consola.error(`The provisioning profile file does not exist or is not accessible: ${profilePath}`);
148
148
  process.exit(1);
149
149
  }
150
150
  const profileBuffer = fs.readFileSync(profilePath);
@@ -3,7 +3,7 @@ import appDestinationsService from '../../../services/app-destinations.js';
3
3
  import appGoogleServiceAccountKeysService from '../../../services/app-google-service-account-keys.js';
4
4
  import { withAuth } from '../../../utils/auth.js';
5
5
  import { isInteractive } from '../../../utils/environment.js';
6
- import { fileExistsAtPath } from '../../../utils/file.js';
6
+ import { isReadable } from '../../../utils/file.js';
7
7
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
8
8
  import { defineCommand, defineOptions } from '@robingenz/zli';
9
9
  import consola from 'consola';
@@ -154,9 +154,9 @@ export default defineCommand({
154
154
  }
155
155
  }
156
156
  // Upload Google service account key file
157
- const googleServiceAccountKeyFileExists = await fileExistsAtPath(googleServiceAccountKeyFile);
158
- if (!googleServiceAccountKeyFileExists) {
159
- consola.error(`The Google service account key file was not found or is not accessible: ${googleServiceAccountKeyFile}`);
157
+ const googleServiceAccountKeyFileReadable = await isReadable(googleServiceAccountKeyFile);
158
+ if (!googleServiceAccountKeyFileReadable) {
159
+ consola.error(`The Google service account key file does not exist or is not accessible: ${googleServiceAccountKeyFile}`);
160
160
  process.exit(1);
161
161
  }
162
162
  const buffer = fs.readFileSync(googleServiceAccountKeyFile);
@@ -211,9 +211,9 @@ export default defineCommand({
211
211
  }
212
212
  }
213
213
  // Upload Apple API key file
214
- const appleApiKeyFileExists = await fileExistsAtPath(appleApiKeyFile);
215
- if (!appleApiKeyFileExists) {
216
- consola.error(`The Apple API key file was not found or is not accessible: ${appleApiKeyFile}`);
214
+ const appleApiKeyFileReadable = await isReadable(appleApiKeyFile);
215
+ if (!appleApiKeyFileReadable) {
216
+ consola.error(`The Apple API key file does not exist or is not accessible: ${appleApiKeyFile}`);
217
217
  process.exit(1);
218
218
  }
219
219
  const buffer = fs.readFileSync(appleApiKeyFile);
@@ -2,7 +2,7 @@ import appEnvironmentsService from '../../../services/app-environments.js';
2
2
  import { parseKeyValuePairs } from '../../../utils/app-environments.js';
3
3
  import { withAuth } from '../../../utils/auth.js';
4
4
  import { isInteractive } from '../../../utils/environment.js';
5
- import { fileExistsAtPath } from '../../../utils/file.js';
5
+ import { isReadable } from '../../../utils/file.js';
6
6
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
7
7
  import { defineCommand, defineOptions } from '@robingenz/zli';
8
8
  import consola from 'consola';
@@ -53,9 +53,9 @@ export default defineCommand({
53
53
  // Parse variables from inline and file
54
54
  const variablesMap = new Map();
55
55
  if (variableFile) {
56
- const fileExists = await fileExistsAtPath(variableFile);
57
- if (!fileExists) {
58
- consola.error(`The variable file was not found or is not accessible: ${variableFile}`);
56
+ const variableFileReadable = await isReadable(variableFile);
57
+ if (!variableFileReadable) {
58
+ consola.error(`The variable file does not exist or is not accessible: ${variableFile}`);
59
59
  process.exit(1);
60
60
  }
61
61
  const fileContent = await fs.promises.readFile(variableFile, 'utf-8');
@@ -70,9 +70,9 @@ export default defineCommand({
70
70
  // Parse secrets from inline and file
71
71
  const secretsMap = new Map();
72
72
  if (secretFile) {
73
- const fileExists = await fileExistsAtPath(secretFile);
74
- if (!fileExists) {
75
- consola.error(`The secret file was not found or is not accessible: ${secretFile}`);
73
+ const secretFileReadable = await isReadable(secretFile);
74
+ if (!secretFileReadable) {
75
+ consola.error(`The secret file does not exist or is not accessible: ${secretFile}`);
76
76
  process.exit(1);
77
77
  }
78
78
  const fileContent = await fs.promises.readFile(secretFile, 'utf-8');
@@ -1,5 +1,5 @@
1
1
  import { isInteractive } from '../../../utils/environment.js';
2
- import { directoryContainsSourceMaps, directoryContainsSymlinks, fileExistsAtPath, isDirectory } from '../../../utils/file.js';
2
+ import { directoryContainsSourceMaps, directoryContainsSymlinks, isDirectory, isReadable, pathExists, } from '../../../utils/file.js';
3
3
  import { generateManifestJson } from '../../../utils/manifest.js';
4
4
  import { prompt } from '../../../utils/prompt.js';
5
5
  import zip from '../../../utils/zip.js';
@@ -44,9 +44,9 @@ export default defineCommand({
44
44
  // Convert to absolute path
45
45
  inputPath = pathModule.resolve(inputPath);
46
46
  // Validate input path exists
47
- const inputExists = await fileExistsAtPath(inputPath);
48
- if (!inputExists) {
49
- consola.error(`Input path does not exist: ${inputPath}`);
47
+ const inputPathReadable = await isReadable(inputPath);
48
+ if (!inputPathReadable) {
49
+ consola.error(`The input path does not exist or is not accessible: ${inputPath}`);
50
50
  process.exit(1);
51
51
  }
52
52
  // Validate input is a directory
@@ -57,9 +57,9 @@ export default defineCommand({
57
57
  }
58
58
  // Validate directory contains index.html
59
59
  const indexHtmlPath = pathModule.join(inputPath, 'index.html');
60
- const hasIndexHtml = await fileExistsAtPath(indexHtmlPath);
61
- if (!hasIndexHtml) {
62
- consola.error(`Directory must contain an index.html file: ${inputPath}`);
60
+ const indexHtmlReadable = await isReadable(indexHtmlPath);
61
+ if (!indexHtmlReadable) {
62
+ consola.error(`The index.html file does not exist or is not accessible: ${indexHtmlPath}`);
63
63
  process.exit(1);
64
64
  }
65
65
  // Check for symlinks
@@ -78,8 +78,8 @@ export default defineCommand({
78
78
  }
79
79
  outputPath = pathModule.resolve(outputPath);
80
80
  // 3. Check if output exists and handle overwrite
81
- const outputExists = await fileExistsAtPath(outputPath);
82
- if (outputExists) {
81
+ const outputPathExists = await pathExists(outputPath);
82
+ if (outputPathExists) {
83
83
  if (!overwrite) {
84
84
  if (!isInteractive()) {
85
85
  consola.error(`Output file already exists: ${outputPath}. Use --overwrite flag to skip confirmation or run in interactive mode.`);
@@ -97,9 +97,9 @@ export default defineCommand({
97
97
  }
98
98
  // Validate parent directory exists
99
99
  const outputDir = pathModule.dirname(outputPath);
100
- const outputDirExists = await fileExistsAtPath(outputDir);
100
+ const outputDirExists = await pathExists(outputDir);
101
101
  if (!outputDirExists) {
102
- consola.error(`Output directory does not exist: ${outputDir}`);
102
+ consola.error(`The output directory does not exist: ${outputDir}`);
103
103
  process.exit(1);
104
104
  }
105
105
  // 4. Generate bundle
@@ -8,7 +8,7 @@ import { parseKeyValuePairs } from '../../../utils/app-environments.js';
8
8
  import { withAuth } from '../../../utils/auth.js';
9
9
  import { parseCustomProperties } from '../../../utils/custom-properties.js';
10
10
  import { isInteractive } from '../../../utils/environment.js';
11
- import { fileExistsAtPath } from '../../../utils/file.js';
11
+ import { isReadable } from '../../../utils/file.js';
12
12
  import { waitForJobCompletion } from '../../../utils/job.js';
13
13
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
14
14
  import zip from '../../../utils/zip.js';
@@ -188,9 +188,9 @@ export default defineCommand({
188
188
  // Parse ad hoc environment variables from inline and file
189
189
  const variablesMap = new Map();
190
190
  if (options.variableFile) {
191
- const fileExists = await fileExistsAtPath(options.variableFile);
192
- if (!fileExists) {
193
- consola.error(`The variable file was not found or is not accessible: ${options.variableFile}`);
191
+ const variableFileReadable = await isReadable(options.variableFile);
192
+ if (!variableFileReadable) {
193
+ consola.error(`The variable file does not exist or is not accessible: ${options.variableFile}`);
194
194
  process.exit(1);
195
195
  }
196
196
  const fileContent = await fs.readFile(options.variableFile, 'utf-8');
@@ -1,5 +1,5 @@
1
1
  import { isInteractive } from '../../../utils/environment.js';
2
- import { directoryContainsSourceMaps, directoryContainsSymlinks, fileExistsAtPath, isDirectory } from '../../../utils/file.js';
2
+ import { directoryContainsSourceMaps, directoryContainsSymlinks, isReadable, isDirectory } from '../../../utils/file.js';
3
3
  import { generateManifestJson } from '../../../utils/manifest.js';
4
4
  import { prompt } from '../../../utils/prompt.js';
5
5
  import { defineCommand, defineOptions } from '@robingenz/zli';
@@ -27,9 +27,9 @@ export default defineCommand({
27
27
  }
28
28
  }
29
29
  // Check if the path exists
30
- const pathExists = await fileExistsAtPath(path);
31
- if (!pathExists) {
32
- consola.error(`The path does not exist.`);
30
+ const pathReadable = await isReadable(path);
31
+ if (!pathReadable) {
32
+ consola.error(`The path does not exist or is not accessible: ${path}`);
33
33
  process.exit(1);
34
34
  }
35
35
  // Check if the path is a directory
@@ -1,4 +1,4 @@
1
- import { directoryContainsSymlinks, fileExistsAtPath, isDirectory } from '../../../utils/file.js';
1
+ import { directoryContainsSymlinks, isReadable, isDirectory } from '../../../utils/file.js';
2
2
  import { generateManifestJson } from '../../../utils/manifest.js';
3
3
  import { prompt } from '../../../utils/prompt.js';
4
4
  import consola from 'consola';
@@ -13,7 +13,7 @@ vi.mock('@/utils/environment.js', () => ({
13
13
  isInteractive: () => true,
14
14
  }));
15
15
  describe('apps-liveupdates-generatemanifest', () => {
16
- const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
16
+ const mockIsReadable = vi.mocked(isReadable);
17
17
  const mockIsDirectory = vi.mocked(isDirectory);
18
18
  const mockDirectoryContainsSymlinks = vi.mocked(directoryContainsSymlinks);
19
19
  const mockGenerateManifestJson = vi.mocked(generateManifestJson);
@@ -30,25 +30,25 @@ describe('apps-liveupdates-generatemanifest', () => {
30
30
  });
31
31
  it('should generate manifest with provided path', async () => {
32
32
  const options = { path: './dist' };
33
- mockFileExistsAtPath.mockResolvedValue(true);
33
+ mockIsReadable.mockResolvedValue(true);
34
34
  mockIsDirectory.mockResolvedValue(true);
35
35
  mockGenerateManifestJson.mockResolvedValue(undefined);
36
36
  await generateManifestCommand.action(options, undefined);
37
- expect(mockFileExistsAtPath).toHaveBeenCalledWith('./dist');
37
+ expect(mockIsReadable).toHaveBeenCalledWith('./dist');
38
38
  expect(mockGenerateManifestJson).toHaveBeenCalledWith('./dist');
39
39
  expect(mockConsola.success).toHaveBeenCalledWith('Manifest file generated.');
40
40
  });
41
41
  it('should prompt for path when not provided', async () => {
42
42
  const options = {};
43
43
  mockPrompt.mockResolvedValueOnce('./www');
44
- mockFileExistsAtPath.mockResolvedValue(true);
44
+ mockIsReadable.mockResolvedValue(true);
45
45
  mockIsDirectory.mockResolvedValue(true);
46
46
  mockGenerateManifestJson.mockResolvedValue(undefined);
47
47
  await generateManifestCommand.action(options, undefined);
48
48
  expect(mockPrompt).toHaveBeenCalledWith('Enter the path to the web assets folder (e.g., `dist` or `www`):', {
49
49
  type: 'text',
50
50
  });
51
- expect(mockFileExistsAtPath).toHaveBeenCalledWith('./www');
51
+ expect(mockIsReadable).toHaveBeenCalledWith('./www');
52
52
  expect(mockGenerateManifestJson).toHaveBeenCalledWith('./www');
53
53
  expect(mockConsola.success).toHaveBeenCalledWith('Manifest file generated.');
54
54
  });
@@ -60,20 +60,20 @@ describe('apps-liveupdates-generatemanifest', () => {
60
60
  });
61
61
  it('should handle nonexistent path', async () => {
62
62
  const options = { path: './nonexistent' };
63
- mockFileExistsAtPath.mockResolvedValue(false);
63
+ mockIsReadable.mockResolvedValue(false);
64
64
  await expect(generateManifestCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
65
- expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist.');
65
+ expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist or is not accessible: ./nonexistent');
66
66
  });
67
67
  it('should handle non-directory path', async () => {
68
68
  const options = { path: './file.txt' };
69
- mockFileExistsAtPath.mockResolvedValue(true);
69
+ mockIsReadable.mockResolvedValue(true);
70
70
  mockIsDirectory.mockResolvedValue(false);
71
71
  await expect(generateManifestCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
72
72
  expect(mockConsola.error).toHaveBeenCalledWith('The path is not a directory.');
73
73
  });
74
74
  it('should warn when symlinks are detected', async () => {
75
75
  const options = { path: './dist' };
76
- mockFileExistsAtPath.mockResolvedValue(true);
76
+ mockIsReadable.mockResolvedValue(true);
77
77
  mockIsDirectory.mockResolvedValue(true);
78
78
  mockDirectoryContainsSymlinks.mockResolvedValue(true);
79
79
  mockGenerateManifestJson.mockResolvedValue(undefined);
@@ -5,7 +5,7 @@ import { withAuth } from '../../../utils/auth.js';
5
5
  import { parseCustomProperties } from '../../../utils/custom-properties.js';
6
6
  import { createBufferFromPath, createBufferFromString, isPrivateKeyContent } from '../../../utils/buffer.js';
7
7
  import { isInteractive } from '../../../utils/environment.js';
8
- import { fileExistsAtPath } from '../../../utils/file.js';
8
+ import { isReadable } from '../../../utils/file.js';
9
9
  import { createHash } from '../../../utils/hash.js';
10
10
  import { formatPrivateKey } from '../../../utils/private-key.js';
11
11
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
@@ -158,9 +158,9 @@ export default defineCommand({
158
158
  process.exit(1);
159
159
  }
160
160
  // Check if the path exists
161
- const pathExists = await fileExistsAtPath(path);
162
- if (!pathExists) {
163
- consola.error(`The path does not exist.`);
161
+ const pathReadable = await isReadable(path);
162
+ if (!pathReadable) {
163
+ consola.error(`The path does not exist or is not accessible: ${path}`);
164
164
  process.exit(1);
165
165
  }
166
166
  // Create the file buffer
@@ -177,15 +177,15 @@ export default defineCommand({
177
177
  }
178
178
  else if (privateKey.endsWith('.pem')) {
179
179
  // Handle file path
180
- const fileExists = await fileExistsAtPath(privateKey);
181
- if (fileExists) {
180
+ const privateKeyReadable = await isReadable(privateKey);
181
+ if (privateKeyReadable) {
182
182
  const keyBuffer = await createBufferFromPath(privateKey);
183
183
  const keyContent = keyBuffer.toString('utf8');
184
184
  const formattedPrivateKey = formatPrivateKey(keyContent);
185
185
  privateKeyBuffer = createBufferFromString(formattedPrivateKey);
186
186
  }
187
187
  else {
188
- consola.error('Private key file not found.');
188
+ consola.error(`The private key file does not exist or is not accessible: ${privateKey}`);
189
189
  process.exit(1);
190
190
  }
191
191
  }
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
2
  import authorizationService from '../../../services/authorization-service.js';
3
- import { fileExistsAtPath } from '../../../utils/file.js';
3
+ import { isReadable } from '../../../utils/file.js';
4
4
  import userConfig from '../../../utils/user-config.js';
5
5
  import consola from 'consola';
6
6
  import nock from 'nock';
@@ -20,7 +20,7 @@ vi.mock('consola');
20
20
  describe('apps-liveupdates-register', () => {
21
21
  const mockUserConfig = vi.mocked(userConfig);
22
22
  const mockAuthorizationService = vi.mocked(authorizationService);
23
- const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
23
+ const mockIsReadable = vi.mocked(isReadable);
24
24
  const mockConsola = vi.mocked(consola);
25
25
  beforeEach(() => {
26
26
  vi.clearAllMocks();
@@ -121,7 +121,7 @@ describe('apps-liveupdates-register', () => {
121
121
  rolloutPercentage: 1,
122
122
  yes: true,
123
123
  };
124
- mockFileExistsAtPath.mockResolvedValue(true);
124
+ mockIsReadable.mockResolvedValue(true);
125
125
  // Mock utility functions
126
126
  const mockZip = await import('../../../utils/zip.js');
127
127
  const mockBuffer = await import('../../../utils/buffer.js');
@@ -167,7 +167,7 @@ describe('apps-liveupdates-register', () => {
167
167
  rolloutPercentage: 1,
168
168
  yes: true,
169
169
  };
170
- mockFileExistsAtPath.mockImplementation((path) => {
170
+ mockIsReadable.mockImplementation((path) => {
171
171
  if (path === privateKeyPath)
172
172
  return Promise.resolve(true);
173
173
  if (path === bundlePath)
@@ -226,7 +226,7 @@ describe('apps-liveupdates-register', () => {
226
226
  rolloutPercentage: 1,
227
227
  yes: true,
228
228
  };
229
- mockFileExistsAtPath.mockResolvedValue(true);
229
+ mockIsReadable.mockResolvedValue(true);
230
230
  // Mock utility functions
231
231
  const mockZip = await import('../../../utils/zip.js');
232
232
  const mockBuffer = await import('../../../utils/buffer.js');
@@ -273,7 +273,7 @@ describe('apps-liveupdates-register', () => {
273
273
  privateKey: privateKeyPath,
274
274
  rolloutPercentage: 1,
275
275
  };
276
- mockFileExistsAtPath.mockImplementation((path) => {
276
+ mockIsReadable.mockImplementation((path) => {
277
277
  if (path === privateKeyPath)
278
278
  return Promise.resolve(false);
279
279
  return Promise.resolve(true);
@@ -284,7 +284,7 @@ describe('apps-liveupdates-register', () => {
284
284
  vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
285
285
  vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
286
286
  await expect(registerCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
287
- expect(mockConsola.error).toHaveBeenCalledWith('Private key file not found.');
287
+ expect(mockConsola.error).toHaveBeenCalledWith(`The private key file does not exist or is not accessible: ${privateKeyPath}`);
288
288
  });
289
289
  it('should validate path must be a zip file', async () => {
290
290
  const appId = 'app-123';
@@ -296,7 +296,7 @@ describe('apps-liveupdates-register', () => {
296
296
  path: bundlePath,
297
297
  rolloutPercentage: 1,
298
298
  };
299
- mockFileExistsAtPath.mockResolvedValue(true);
299
+ mockIsReadable.mockResolvedValue(true);
300
300
  // Mock zip utility to return false
301
301
  const mockZip = await import('../../../utils/zip.js');
302
302
  vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
@@ -6,7 +6,7 @@ import { withAuth } from '../../../utils/auth.js';
6
6
  import { parseCustomProperties } from '../../../utils/custom-properties.js';
7
7
  import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
8
8
  import { isInteractive } from '../../../utils/environment.js';
9
- import { directoryContainsSourceMaps, directoryContainsSymlinks, fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory, } from '../../../utils/file.js';
9
+ import { directoryContainsSourceMaps, directoryContainsSymlinks, isReadable, getFilesInDirectoryAndSubdirectories, isDirectory, } from '../../../utils/file.js';
10
10
  import { createHash } from '../../../utils/hash.js';
11
11
  import { generateManifestJson } from '../../../utils/manifest.js';
12
12
  import { formatPrivateKey } from '../../../utils/private-key.js';
@@ -136,9 +136,9 @@ export default defineCommand({
136
136
  }
137
137
  }
138
138
  // Validate the provided path
139
- const pathExists = await fileExistsAtPath(path);
140
- if (!pathExists) {
141
- consola.error(`The path does not exist.`);
139
+ const pathReadable = await isReadable(path);
140
+ if (!pathReadable) {
141
+ consola.error(`The path does not exist or is not accessible: ${path}`);
142
142
  process.exit(1);
143
143
  }
144
144
  // Check if the directory contains an index.html file
@@ -216,15 +216,15 @@ export default defineCommand({
216
216
  }
217
217
  else if (privateKey.endsWith('.pem')) {
218
218
  // Handle file path
219
- const fileExists = await fileExistsAtPath(privateKey);
220
- if (fileExists) {
219
+ const privateKeyReadable = await isReadable(privateKey);
220
+ if (privateKeyReadable) {
221
221
  const keyBuffer = await createBufferFromPath(privateKey);
222
222
  const keyContent = keyBuffer.toString('utf8');
223
223
  const formattedPrivateKey = formatPrivateKey(keyContent);
224
224
  privateKeyBuffer = createBufferFromString(formattedPrivateKey);
225
225
  }
226
226
  else {
227
- consola.error('Private key file not found.');
227
+ consola.error(`The private key file does not exist or is not accessible: ${privateKey}`);
228
228
  process.exit(1);
229
229
  }
230
230
  }
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
2
  import authorizationService from '../../../services/authorization-service.js';
3
3
  import { isInteractive } from '../../../utils/environment.js';
4
- import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
4
+ import { isReadable, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
5
5
  import userConfig from '../../../utils/user-config.js';
6
6
  import consola from 'consola';
7
7
  import nock from 'nock';
@@ -23,7 +23,7 @@ vi.mock('consola');
23
23
  describe('apps-liveupdates-upload', () => {
24
24
  const mockUserConfig = vi.mocked(userConfig);
25
25
  const mockAuthorizationService = vi.mocked(authorizationService);
26
- const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
26
+ const mockIsReadable = vi.mocked(isReadable);
27
27
  const mockGetFilesInDirectoryAndSubdirectories = vi.mocked(getFilesInDirectoryAndSubdirectories);
28
28
  const mockIsDirectory = vi.mocked(isDirectory);
29
29
  const mockIsInteractive = vi.mocked(isInteractive);
@@ -53,9 +53,9 @@ describe('apps-liveupdates-upload', () => {
53
53
  const appId = 'app-123';
54
54
  const nonexistentPath = './nonexistent';
55
55
  const options = { appId, path: nonexistentPath, artifactType: 'zip', rollout: 1 };
56
- mockFileExistsAtPath.mockResolvedValue(false);
56
+ mockIsReadable.mockResolvedValue(false);
57
57
  await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
58
- expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist.');
58
+ expect(mockConsola.error).toHaveBeenCalledWith(`The path does not exist or is not accessible: ${nonexistentPath}`);
59
59
  });
60
60
  it('should validate manifest artifact type requires directory', async () => {
61
61
  const appId = 'app-123';
@@ -66,7 +66,7 @@ describe('apps-liveupdates-upload', () => {
66
66
  artifactType: 'manifest',
67
67
  rollout: 1,
68
68
  };
69
- mockFileExistsAtPath.mockResolvedValue(true);
69
+ mockIsReadable.mockResolvedValue(true);
70
70
  mockIsDirectory.mockResolvedValue(false);
71
71
  // Mock zip utility to return true so path validation passes
72
72
  const mockZip = await import('../../../utils/zip.js');
@@ -86,7 +86,7 @@ describe('apps-liveupdates-upload', () => {
86
86
  artifactType: 'zip',
87
87
  rollout: 1,
88
88
  };
89
- mockFileExistsAtPath.mockResolvedValue(true);
89
+ mockIsReadable.mockResolvedValue(true);
90
90
  mockIsDirectory.mockResolvedValue(true);
91
91
  mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
92
92
  { href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
@@ -140,7 +140,7 @@ describe('apps-liveupdates-upload', () => {
140
140
  rollout: 1,
141
141
  gitRef,
142
142
  };
143
- mockFileExistsAtPath.mockResolvedValue(true);
143
+ mockIsReadable.mockResolvedValue(true);
144
144
  mockIsDirectory.mockResolvedValue(true);
145
145
  mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
146
146
  { href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
@@ -196,7 +196,7 @@ describe('apps-liveupdates-upload', () => {
196
196
  artifactType: 'zip',
197
197
  rollout: 1,
198
198
  };
199
- mockFileExistsAtPath.mockImplementation((path) => {
199
+ mockIsReadable.mockImplementation((path) => {
200
200
  if (path === privateKeyPath)
201
201
  return Promise.resolve(true);
202
202
  if (path === bundlePath)
@@ -255,7 +255,7 @@ describe('apps-liveupdates-upload', () => {
255
255
  artifactType: 'zip',
256
256
  rollout: 1,
257
257
  };
258
- mockFileExistsAtPath.mockImplementation((path) => {
258
+ mockIsReadable.mockImplementation((path) => {
259
259
  if (path === privateKeyPath)
260
260
  return Promise.resolve(false);
261
261
  return Promise.resolve(true);
@@ -268,7 +268,7 @@ describe('apps-liveupdates-upload', () => {
268
268
  const mockBuffer = await import('../../../utils/buffer.js');
269
269
  vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
270
270
  await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
271
- expect(mockConsola.error).toHaveBeenCalledWith('Private key file not found.');
271
+ expect(mockConsola.error).toHaveBeenCalledWith(`The private key file does not exist or is not accessible: ${privateKeyPath}`);
272
272
  });
273
273
  it('should handle invalid private key format', async () => {
274
274
  const appId = 'app-123';
@@ -280,7 +280,7 @@ describe('apps-liveupdates-upload', () => {
280
280
  artifactType: 'zip',
281
281
  rollout: 1,
282
282
  };
283
- mockFileExistsAtPath.mockResolvedValue(true);
283
+ mockIsReadable.mockResolvedValue(true);
284
284
  mockIsDirectory.mockResolvedValue(false);
285
285
  // Mock zip utility to pass path validation
286
286
  const mockZip = await import('../../../utils/zip.js');
@@ -302,7 +302,7 @@ describe('apps-liveupdates-upload', () => {
302
302
  artifactType: 'zip',
303
303
  rollout: 1,
304
304
  };
305
- mockFileExistsAtPath.mockResolvedValue(true);
305
+ mockIsReadable.mockResolvedValue(true);
306
306
  mockIsDirectory.mockResolvedValue(true);
307
307
  mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
308
308
  { href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
@@ -46,7 +46,14 @@ export const directoryContainsSourceMaps = async (path) => {
46
46
  const files = await getFilesInDirectoryAndSubdirectories(path);
47
47
  return files.some((file) => file.name.endsWith('.js.map') || file.name.endsWith('.css.map'));
48
48
  };
49
- export const fileExistsAtPath = async (path) => {
49
+ export const isReadable = async (path) => {
50
+ return new Promise((resolve) => {
51
+ fs.access(path, fs.constants.R_OK, (err) => {
52
+ resolve(!err);
53
+ });
54
+ });
55
+ };
56
+ export const pathExists = async (path) => {
50
57
  return new Promise((resolve) => {
51
58
  fs.access(path, fs.constants.F_OK, (err) => {
52
59
  resolve(!err);
package/dist/utils/zip.js CHANGED
@@ -7,13 +7,36 @@ class ZipImpl {
7
7
  async zipFolder(sourceFolder) {
8
8
  const zip = new AdmZip();
9
9
  zip.addLocalFolder(sourceFolder);
10
+ // Strip external file attributes from every entry. `addLocalFolder` copies
11
+ // the source's Unix mode bits onto each entry, so a directory with e.g.
12
+ // `0500` on a CI checkout ends up in the bundle and makes `zip4j` on
13
+ // Android fail with "Could not create directory" when extracting children
14
+ // into that read-only parent. Clearing attrs lets the extractor fall back
15
+ // to its OS defaults. Mirrors the plugin-side workaround in live-update
16
+ // 8.2.1+ (`FileHeader.setExternalFileAttributes(null)`).
17
+ for (const entry of zip.getEntries()) {
18
+ entry.attr = 0;
19
+ }
10
20
  return zip.toBuffer();
11
21
  }
12
22
  async zipFolderWithGitignore(sourceFolder) {
23
+ // Do NOT apply the `entry.attr = 0` workaround from `zipFolder` here.
24
+ // This method zips full project sources for `apps:builds:create`, which
25
+ // include executables like `gradlew` whose `0755` mode must survive the
26
+ // round-trip so the server-side build can run them.
13
27
  const files = await globby(['**/*'], {
14
28
  cwd: sourceFolder,
15
29
  gitignore: true,
16
- ignore: ['.git/**'],
30
+ ignore: [
31
+ '.git/**',
32
+ '**/node_modules/**',
33
+ '**/ios/DerivedData/**',
34
+ '**/ios/Pods/**',
35
+ '**/ios/build/**',
36
+ '**/android/build/**',
37
+ '**/android/.gradle/**',
38
+ '**/android/app/build/**',
39
+ ],
17
40
  dot: true,
18
41
  });
19
42
  if (files.length > MAX_ZIP_ENTRIES) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.9.2",
3
+ "version": "4.9.3-dev.47af0d1.1779135155",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {