@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 +8 -0
- package/dist/commands/apps/builds/create.js +29 -14
- package/dist/commands/apps/certificates/create.js +7 -7
- package/dist/commands/apps/destinations/create.js +7 -7
- package/dist/commands/apps/environments/set.js +7 -7
- package/dist/commands/apps/liveupdates/bundle.js +11 -11
- package/dist/commands/apps/liveupdates/create.js +4 -4
- package/dist/commands/apps/liveupdates/generate-manifest.js +4 -4
- package/dist/commands/apps/liveupdates/generate-manifest.test.js +10 -10
- package/dist/commands/apps/liveupdates/register.js +7 -7
- package/dist/commands/apps/liveupdates/register.test.js +8 -8
- package/dist/commands/apps/liveupdates/upload.js +7 -7
- package/dist/commands/apps/liveupdates/upload.test.js +12 -12
- package/dist/utils/file.js +8 -1
- package/dist/utils/zip.js +24 -1
- package/package.json +1 -1
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 {
|
|
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
|
|
122
|
-
if (!
|
|
123
|
-
consola.error(
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
250
|
-
if (!
|
|
251
|
-
consola.error(`The variable file
|
|
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
|
-
|
|
275
|
-
|
|
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 {
|
|
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
|
|
135
|
-
if (!
|
|
136
|
-
consola.error(`The certificate 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
|
|
146
|
-
if (!
|
|
147
|
-
consola.error(`The provisioning profile file
|
|
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 {
|
|
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
|
|
158
|
-
if (!
|
|
159
|
-
consola.error(`The Google service account key file
|
|
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
|
|
215
|
-
if (!
|
|
216
|
-
consola.error(`The Apple API key file
|
|
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 {
|
|
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
|
|
57
|
-
if (!
|
|
58
|
-
consola.error(`The variable file
|
|
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
|
|
74
|
-
if (!
|
|
75
|
-
consola.error(`The secret file
|
|
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,
|
|
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
|
|
48
|
-
if (!
|
|
49
|
-
consola.error(`
|
|
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
|
|
61
|
-
if (!
|
|
62
|
-
consola.error(`
|
|
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
|
|
82
|
-
if (
|
|
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
|
|
100
|
+
const outputDirExists = await pathExists(outputDir);
|
|
101
101
|
if (!outputDirExists) {
|
|
102
|
-
consola.error(`
|
|
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 {
|
|
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
|
|
192
|
-
if (!
|
|
193
|
-
consola.error(`The variable file
|
|
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,
|
|
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
|
|
31
|
-
if (!
|
|
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,
|
|
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
|
|
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
|
-
|
|
33
|
+
mockIsReadable.mockResolvedValue(true);
|
|
34
34
|
mockIsDirectory.mockResolvedValue(true);
|
|
35
35
|
mockGenerateManifestJson.mockResolvedValue(undefined);
|
|
36
36
|
await generateManifestCommand.action(options, undefined);
|
|
37
|
-
expect(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
162
|
-
if (!
|
|
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
|
|
181
|
-
if (
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
140
|
-
if (!
|
|
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
|
|
220
|
-
if (
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
56
|
+
mockIsReadable.mockResolvedValue(false);
|
|
57
57
|
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
58
|
-
expect(mockConsola.error).toHaveBeenCalledWith(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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' },
|
package/dist/utils/file.js
CHANGED
|
@@ -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
|
|
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: [
|
|
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) {
|