kasy-cli 1.31.6 → 1.31.7
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/bin/kasy.js +20 -3
- package/docs/cli-reference.md +8 -6
- package/lib/commands/codemagic.js +110 -45
- package/lib/commands/new.js +5 -0
- package/lib/commands/reset.js +5 -1
- package/lib/commands/run.js +6 -1
- package/lib/scaffold/engine.js +5 -3
- package/lib/scaffold/generate.js +7 -0
- package/lib/utils/codemagic-release.js +122 -13
- package/lib/utils/env-tools.js +42 -0
- package/lib/utils/flutter-install.js +8 -0
- package/lib/utils/i18n/messages-en.js +18 -7
- package/lib/utils/i18n/messages-es.js +18 -7
- package/lib/utils/i18n/messages-pt.js +18 -7
- package/package.json +1 -1
- package/templates/firebase/codemagic.yaml +213 -0
- package/templates/firebase/docs/codemagic-release.en.md +45 -18
- package/templates/firebase/docs/codemagic-release.es.md +40 -14
- package/templates/firebase/docs/codemagic-release.pt.md +42 -15
package/bin/kasy.js
CHANGED
|
@@ -602,7 +602,22 @@ function buildProgram(language) {
|
|
|
602
602
|
],
|
|
603
603
|
});
|
|
604
604
|
if (sub === 'configure') await runCodemagicConfigure('.', { language });
|
|
605
|
-
else if (sub === 'release')
|
|
605
|
+
else if (sub === 'release') {
|
|
606
|
+
const platform = await ui.select({
|
|
607
|
+
message: t('cli.command.codemagic.release.platformPick'),
|
|
608
|
+
options: [
|
|
609
|
+
{ value: 'both', label: t('cli.command.codemagic.release.platform.both') },
|
|
610
|
+
{ value: 'ios', label: t('cli.command.codemagic.release.platform.ios') },
|
|
611
|
+
{ value: 'android', label: t('cli.command.codemagic.release.platform.android') },
|
|
612
|
+
],
|
|
613
|
+
});
|
|
614
|
+
if (typeof platform === 'string') {
|
|
615
|
+
const relOpts = { language };
|
|
616
|
+
if (platform === 'ios') relOpts.ios = true;
|
|
617
|
+
else if (platform === 'android') relOpts.android = true;
|
|
618
|
+
await runCodemagicRelease('.', relOpts);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
606
621
|
else if (sub === 'status') {
|
|
607
622
|
const buildId = await ui.text({
|
|
608
623
|
message: 'Build ID',
|
|
@@ -625,9 +640,11 @@ function buildProgram(language) {
|
|
|
625
640
|
codemagicCmd
|
|
626
641
|
.command('release')
|
|
627
642
|
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
643
|
+
.option('--ios', t('cli.command.codemagic.release.iosOption'))
|
|
644
|
+
.option('--android', t('cli.command.codemagic.release.androidOption'))
|
|
628
645
|
.description(t('cli.command.codemagic.release.description'))
|
|
629
|
-
.action(async (directory) => {
|
|
630
|
-
await runCodemagicRelease(directory, { language });
|
|
646
|
+
.action(async (directory, opts) => {
|
|
647
|
+
await runCodemagicRelease(directory, { language, ios: opts.ios, android: opts.android });
|
|
631
648
|
}),
|
|
632
649
|
t
|
|
633
650
|
);
|
package/docs/cli-reference.md
CHANGED
|
@@ -121,15 +121,17 @@ Atalho: `make release-ios`
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
-
### `kasy codemagic` — Release iOS (nuvem)
|
|
124
|
+
### `kasy codemagic` — Release iOS/Android (nuvem)
|
|
125
125
|
|
|
126
|
-
Dispara build iOS no Codemagic (ideal sem Mac).
|
|
126
|
+
Dispara build **iOS e Android** no Codemagic (ideal sem Mac), levando as chaves do `.env` no disparo.
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
kasy add ci
|
|
130
|
-
kasy codemagic configure
|
|
131
|
-
kasy codemagic release
|
|
132
|
-
kasy codemagic
|
|
129
|
+
kasy add ci # entrega o codemagic.yaml (se necessário)
|
|
130
|
+
kasy codemagic configure # token API → valida e lista seus apps → branch
|
|
131
|
+
kasy codemagic release # iOS + Android na nuvem
|
|
132
|
+
kasy codemagic release --ios # só iOS
|
|
133
|
+
kasy codemagic release --android # só Android
|
|
134
|
+
kasy codemagic status <id> # status do build
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
Documentação: `docs/codemagic-release.md` (idioma da CLI)
|
|
@@ -11,18 +11,39 @@ const {
|
|
|
11
11
|
codemagicReleaseDocPath,
|
|
12
12
|
writeCodemagicEnv,
|
|
13
13
|
validateCodemagicSetup,
|
|
14
|
+
loadProjectEnv,
|
|
15
|
+
listApps,
|
|
16
|
+
workflowIdFor,
|
|
17
|
+
resolveReleaseVars,
|
|
14
18
|
triggerBuild,
|
|
15
19
|
getBuildStatus,
|
|
16
20
|
URL_CODEMAGIC_APPS,
|
|
17
21
|
URL_CODEMAGIC_API,
|
|
18
22
|
} = require('../utils/codemagic-release');
|
|
19
23
|
|
|
24
|
+
// Human label per platform (proper nouns — same across locales).
|
|
25
|
+
const PLATFORM_LABEL = { ios: 'iOS', android: 'Android' };
|
|
26
|
+
|
|
20
27
|
async function assertProject(projectDir, t) {
|
|
21
28
|
if (!(await isKasyFlutterProject(projectDir))) {
|
|
22
29
|
throw new Error(t('codemagic.error.notProject'));
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Decide which platforms to release. Explicit flags win; with no flag we build
|
|
35
|
+
* both. Returns an ordered list like ['ios', 'android'].
|
|
36
|
+
*/
|
|
37
|
+
function platformsFromOptions(options) {
|
|
38
|
+
const wantIos = !!options.ios;
|
|
39
|
+
const wantAndroid = !!options.android;
|
|
40
|
+
if (!wantIos && !wantAndroid) return ['ios', 'android'];
|
|
41
|
+
const platforms = [];
|
|
42
|
+
if (wantIos) platforms.push('ios');
|
|
43
|
+
if (wantAndroid) platforms.push('android');
|
|
44
|
+
return platforms;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
async function runConfigure(directory, options = {}) {
|
|
27
48
|
const lang = options.language || detectDefaultLanguage();
|
|
28
49
|
const t = createTranslator(lang);
|
|
@@ -35,39 +56,62 @@ async function runConfigure(directory, options = {}) {
|
|
|
35
56
|
ui.log.message(kleur.dim(`${t('codemagic.configure.doc')}: ${codemagicReleaseDocPath(lang)}`));
|
|
36
57
|
ui.log.warn(t('codemagic.configure.checklist'));
|
|
37
58
|
|
|
38
|
-
ui.log.info(t('codemagic.configure.openingLinks'));
|
|
39
|
-
openUrl(URL_CODEMAGIC_API);
|
|
40
|
-
openUrl(URL_CODEMAGIC_APPS);
|
|
41
|
-
|
|
42
59
|
const cancel = () => { ui.cancel(t('codemagic.configure.cancelled')); process.exit(0); };
|
|
43
60
|
const required = (v) => (v && String(v).trim().length > 0 ? undefined : t('codemagic.configure.q.required'));
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
// 1. API token — open the settings page so the user can copy it.
|
|
63
|
+
ui.log.info(t('codemagic.configure.openingToken'));
|
|
64
|
+
openUrl(URL_CODEMAGIC_API);
|
|
65
|
+
const tokenRaw = await ui.password({
|
|
46
66
|
message: t('codemagic.configure.q.token'),
|
|
47
67
|
validate: required,
|
|
48
68
|
onCancel: cancel,
|
|
49
69
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
60
|
-
|
|
70
|
+
const token = String(tokenRaw).trim();
|
|
71
|
+
|
|
72
|
+
// 2. Validate the token by listing the user's apps.
|
|
73
|
+
const spinner = ui.spinner();
|
|
74
|
+
spinner.start(t('codemagic.configure.validating'));
|
|
75
|
+
let apps;
|
|
76
|
+
try {
|
|
77
|
+
apps = await listApps(token);
|
|
78
|
+
spinner.stop(t('codemagic.configure.validated'));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
spinner.stop(`${t('codemagic.configure.tokenInvalid')} (${err.message})`, 2);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Pick the app (or guide the user to connect a repo first).
|
|
86
|
+
let appId;
|
|
87
|
+
if (!apps.length) {
|
|
88
|
+
ui.log.warn(t('codemagic.configure.noApps'));
|
|
89
|
+
openUrl(URL_CODEMAGIC_APPS);
|
|
90
|
+
const manual = await ui.text({
|
|
91
|
+
message: t('codemagic.configure.q.appId'),
|
|
92
|
+
validate: required,
|
|
93
|
+
onCancel: cancel,
|
|
94
|
+
});
|
|
95
|
+
appId = String(manual).trim();
|
|
96
|
+
} else {
|
|
97
|
+
appId = await ui.select({
|
|
98
|
+
message: t('codemagic.configure.pickApp'),
|
|
99
|
+
options: apps.map((a) => ({ value: a.id, label: a.name, hint: a.id })),
|
|
100
|
+
});
|
|
101
|
+
if (typeof appId === 'symbol') cancel();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Branch that triggers the build.
|
|
105
|
+
const branchRaw = await ui.text({
|
|
61
106
|
message: t('codemagic.configure.q.branch'),
|
|
62
107
|
initialValue: 'main',
|
|
63
108
|
onCancel: cancel,
|
|
64
109
|
});
|
|
65
110
|
|
|
66
111
|
await writeCodemagicEnv(projectDir, {
|
|
67
|
-
token
|
|
68
|
-
appId
|
|
69
|
-
|
|
70
|
-
branch: String(branch).trim() || 'main',
|
|
112
|
+
token,
|
|
113
|
+
appId,
|
|
114
|
+
branch: String(branchRaw).trim() || 'main',
|
|
71
115
|
});
|
|
72
116
|
|
|
73
117
|
ui.log.success(t('codemagic.configure.success'));
|
|
@@ -87,44 +131,64 @@ async function runRelease(directory, options = {}) {
|
|
|
87
131
|
if (validation.issues.includes('missing_yaml')) {
|
|
88
132
|
ui.log.message(kleur.dim(t('codemagic.error.addCi')));
|
|
89
133
|
}
|
|
90
|
-
if (validation.issues.includes('missing_env')) {
|
|
134
|
+
if (validation.issues.includes('missing_env') || validation.issues.includes('missing_token') || validation.issues.includes('missing_app_id')) {
|
|
91
135
|
ui.log.message(kleur.dim(t('codemagic.error.runConfigure')));
|
|
92
136
|
}
|
|
93
137
|
process.exitCode = 1;
|
|
94
138
|
return;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
const platforms = platformsFromOptions(options);
|
|
142
|
+
|
|
143
|
+
// iOS builds need the Google Sign-In URL scheme to be in sync.
|
|
144
|
+
if (platforms.includes('ios')) {
|
|
145
|
+
const googleScheme = await validateGoogleIosUrlScheme(projectDir);
|
|
146
|
+
if (!googleScheme.ok) {
|
|
147
|
+
printCompactHeader(t);
|
|
148
|
+
ui.log.error(t('codemagic.error.googleUrlScheme'));
|
|
149
|
+
ui.log.message(kleur.dim(googleScheme.error));
|
|
150
|
+
ui.log.info(t('ios.error.googleUrlSchemeHint'));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
printCompactHeader(t);
|
|
108
157
|
ui.intro(t('codemagic.release.title'));
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
// Read the project's keys and carry them with the build.
|
|
160
|
+
const projectEnv = await loadProjectEnv(projectDir);
|
|
161
|
+
const variables = resolveReleaseVars(projectEnv);
|
|
162
|
+
|
|
163
|
+
if (platforms.includes('ios') && !variables.RC_IOS_PROD_KEY) {
|
|
164
|
+
ui.log.warn(t('codemagic.release.noIosKey'));
|
|
165
|
+
}
|
|
166
|
+
if (platforms.includes('android') && !variables.RC_ANDROID_PROD_KEY) {
|
|
167
|
+
ui.log.warn(t('codemagic.release.noAndroidKey'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let anyFailed = false;
|
|
171
|
+
for (const platform of platforms) {
|
|
172
|
+
const label = PLATFORM_LABEL[platform];
|
|
173
|
+
const workflowId = workflowIdFor(validation.env, platform);
|
|
174
|
+
const spinner = ui.spinner();
|
|
175
|
+
spinner.start(t('codemagic.release.spinPlatform', { platform: label }));
|
|
176
|
+
try {
|
|
177
|
+
const result = await triggerBuild(validation.env, { workflowId, variables });
|
|
178
|
+
const buildId = result.buildId || result._id;
|
|
179
|
+
spinner.stop(t('codemagic.release.triggeredPlatform', { platform: label }));
|
|
180
|
+
if (buildId) {
|
|
181
|
+
ui.log.info(`Build ID: ${kleur.cyan(buildId)}`);
|
|
182
|
+
ui.log.message(kleur.dim(`https://codemagic.io/app/${validation.env.CODEMAGIC_APP_ID}/build/${buildId}`));
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
anyFailed = true;
|
|
186
|
+
spinner.stop(`${label}: ${err.message}`, 2);
|
|
123
187
|
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
spinner.stop(err.message, 2);
|
|
126
|
-
process.exitCode = 1;
|
|
127
188
|
}
|
|
189
|
+
|
|
190
|
+
if (anyFailed) process.exitCode = 1;
|
|
191
|
+
ui.outro('');
|
|
128
192
|
}
|
|
129
193
|
|
|
130
194
|
async function runStatus(buildId, directory, options = {}) {
|
|
@@ -168,4 +232,5 @@ module.exports = {
|
|
|
168
232
|
runConfigure,
|
|
169
233
|
runRelease,
|
|
170
234
|
runStatus,
|
|
235
|
+
platformsFromOptions,
|
|
171
236
|
};
|
package/lib/commands/new.js
CHANGED
|
@@ -1733,6 +1733,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1733
1733
|
// generation step once choices are made. The success card at the end shows
|
|
1734
1734
|
// the full summary. Cancel any time with Ctrl+C.
|
|
1735
1735
|
|
|
1736
|
+
// Windows: disable native assets before the generator runs pub get / builds,
|
|
1737
|
+
// so a username with a space (C:\Users\John Silva) doesn't break the
|
|
1738
|
+
// objective_c hook compile. No-op elsewhere, idempotent, best-effort.
|
|
1739
|
+
require('../utils/env-tools').disableNativeAssetsWindows();
|
|
1740
|
+
|
|
1736
1741
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1737
1742
|
// Quick: single rolling line that mutates message. Advanced: stack each step.
|
|
1738
1743
|
const stepper = ui.makeQuickStepper({ color: paintLime });
|
package/lib/commands/reset.js
CHANGED
|
@@ -7,7 +7,7 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
|
7
7
|
const { printCompactHeader } = require('../utils/brand');
|
|
8
8
|
const { readBundleId, readPackageName } = require('../utils/mobile-identity');
|
|
9
9
|
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
10
|
-
const { spawnSyncFlutter } = require('../utils/env-tools');
|
|
10
|
+
const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
|
|
11
11
|
|
|
12
12
|
function runCmd(cmd, args) {
|
|
13
13
|
const res = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
@@ -264,6 +264,10 @@ async function runReset(directory, options = {}) {
|
|
|
264
264
|
throw new Error(t('reset.error.notFlutterProject'));
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// Windows: disable native assets before the reinstall rebuild so a username
|
|
268
|
+
// with a space doesn't break the objective_c hook compile. No-op elsewhere.
|
|
269
|
+
disableNativeAssetsWindows();
|
|
270
|
+
|
|
267
271
|
printCompactHeader(t);
|
|
268
272
|
ui.intro(t('reset.title'));
|
|
269
273
|
|
package/lib/commands/run.js
CHANGED
|
@@ -5,7 +5,7 @@ const ui = require('../utils/ui');
|
|
|
5
5
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
6
6
|
const { printCompactHeader, paintLime } = require('../utils/brand');
|
|
7
7
|
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
8
|
-
const { spawnSyncFlutter } = require('../utils/env-tools');
|
|
8
|
+
const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
|
|
9
9
|
|
|
10
10
|
function listFlutterDevices(projectDir) {
|
|
11
11
|
// Shared flutter spawner: exposes a freshly-installed SDK on PATH (the terminal
|
|
@@ -249,6 +249,11 @@ async function runRun(directory, options = {}) {
|
|
|
249
249
|
throw new Error(t('run.error.notFlutterProject'));
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// Windows: turn off native assets before building so a username with a space
|
|
253
|
+
// (C:\Users\John Silva) doesn't break the objective_c hook compile. No-op on
|
|
254
|
+
// macOS/Linux, idempotent, and best-effort — see disableNativeAssetsWindows.
|
|
255
|
+
disableNativeAssetsWindows();
|
|
256
|
+
|
|
252
257
|
// Resolve device flag. If none of the platform shortcuts or -d is set,
|
|
253
258
|
// ask the user when more than one device is available — `flutter run`
|
|
254
259
|
// bails out with "More than one device connected" otherwise.
|
package/lib/scaffold/engine.js
CHANGED
|
@@ -27,12 +27,14 @@ const ALWAYS_EXCLUDE = new Set([
|
|
|
27
27
|
'.cursor',
|
|
28
28
|
'.idea',
|
|
29
29
|
'.claude', // AI assistant instructions — not for the client
|
|
30
|
-
//
|
|
31
|
-
// (these files live in features/ci/ and are applied as a feature patch)
|
|
30
|
+
// GitHub/GitLab CI pipelines are not shipped to generated projects yet.
|
|
32
31
|
'.github',
|
|
33
32
|
'.gitlab',
|
|
34
33
|
'.gitlab-ci.yml',
|
|
35
|
-
|
|
34
|
+
// NOTE: codemagic.yaml is intentionally NOT excluded here. It ships in the
|
|
35
|
+
// base template and generate.js removes it unless the 'ci' module is
|
|
36
|
+
// selected (see the "Phase B" gating). This is what lets `kasy codemagic`
|
|
37
|
+
// work in a generated project.
|
|
36
38
|
]);
|
|
37
39
|
|
|
38
40
|
// File basenames that should NEVER be copied, regardless of directory depth
|
package/lib/scaffold/generate.js
CHANGED
|
@@ -236,6 +236,13 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
236
236
|
// Remove directories of modules that were NOT selected
|
|
237
237
|
await removeModuleDirs(targetDir, modules);
|
|
238
238
|
|
|
239
|
+
// codemagic.yaml ships in the base template (so `kasy codemagic` works out
|
|
240
|
+
// of the box). It belongs to the 'ci' module — strip it when ci was not
|
|
241
|
+
// selected, mirroring how the other optional modules are gated above.
|
|
242
|
+
if (!modules.includes('ci')) {
|
|
243
|
+
await fs.remove(path.join(targetDir, 'codemagic.yaml'));
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
// Write no-op analytics_api.dart if analytics not selected
|
|
240
247
|
if (!modules.includes('analytics')) {
|
|
241
248
|
await writeNoOpAnalyticsApi(targetDir, packageName);
|
|
@@ -8,18 +8,22 @@ const CODEMAGIC_ENV_REL = path.join('.kasy', 'codemagic.env');
|
|
|
8
8
|
const CODEMAGIC_API = 'https://api.codemagic.io';
|
|
9
9
|
|
|
10
10
|
const URL_CODEMAGIC_APPS = 'https://codemagic.io/apps';
|
|
11
|
-
|
|
11
|
+
// Codemagic API token lives in the account settings → Integrations page.
|
|
12
|
+
const URL_CODEMAGIC_API = 'https://codemagic.io/settings';
|
|
13
|
+
|
|
14
|
+
// Default workflow ids defined in the codemagic.yaml shipped by the `ci` feature.
|
|
15
|
+
const DEFAULT_IOS_WORKFLOW = 'ios-workflow';
|
|
16
|
+
const DEFAULT_ANDROID_WORKFLOW = 'android-workflow';
|
|
12
17
|
|
|
13
18
|
function codemagicReleaseDocPath() {
|
|
14
19
|
return 'docs/codemagic-release.md';
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const content = await fs.readFile(envPath, 'utf8');
|
|
22
|
+
/**
|
|
23
|
+
* Parse a simple KEY=VALUE env file into an object. Ignores blanks/comments.
|
|
24
|
+
* Values keep everything after the first '=' (so URLs with ':' survive).
|
|
25
|
+
*/
|
|
26
|
+
function parseEnvContent(content) {
|
|
23
27
|
const env = {};
|
|
24
28
|
for (const line of content.split('\n')) {
|
|
25
29
|
const trimmed = line.trim();
|
|
@@ -31,12 +35,32 @@ async function loadCodemagicEnv(projectDir) {
|
|
|
31
35
|
return env;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
async function
|
|
38
|
+
async function loadCodemagicEnv(projectDir) {
|
|
39
|
+
const envPath = path.join(projectDir, CODEMAGIC_ENV_REL);
|
|
40
|
+
if (!(await fs.pathExists(envPath))) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return parseEnvContent(await fs.readFile(envPath, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the project's runtime `.env` (the app's config). Returns {} when absent.
|
|
48
|
+
*/
|
|
49
|
+
async function loadProjectEnv(projectDir) {
|
|
50
|
+
const envPath = path.join(projectDir, '.env');
|
|
51
|
+
if (!(await fs.pathExists(envPath))) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return parseEnvContent(await fs.readFile(envPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function writeCodemagicEnv(projectDir, { token, appId, iosWorkflowId, androidWorkflowId, branch }) {
|
|
35
58
|
const lines = [
|
|
36
59
|
'# Generated by kasy codemagic configure — do not commit',
|
|
37
60
|
`CODEMAGIC_API_TOKEN=${token}`,
|
|
38
61
|
`CODEMAGIC_APP_ID=${appId}`,
|
|
39
|
-
`
|
|
62
|
+
`CODEMAGIC_IOS_WORKFLOW_ID=${iosWorkflowId || DEFAULT_IOS_WORKFLOW}`,
|
|
63
|
+
`CODEMAGIC_ANDROID_WORKFLOW_ID=${androidWorkflowId || DEFAULT_ANDROID_WORKFLOW}`,
|
|
40
64
|
`CODEMAGIC_BRANCH=${branch || 'main'}`,
|
|
41
65
|
'',
|
|
42
66
|
];
|
|
@@ -83,12 +107,92 @@ function httpsJson(method, urlPath, token, body) {
|
|
|
83
107
|
});
|
|
84
108
|
}
|
|
85
109
|
|
|
86
|
-
|
|
110
|
+
/**
|
|
111
|
+
* List the Codemagic applications the token can access.
|
|
112
|
+
* Returns [{ id, name }] so the configure wizard can let the user pick one
|
|
113
|
+
* instead of pasting a raw Mongo id.
|
|
114
|
+
*/
|
|
115
|
+
async function listApps(token) {
|
|
116
|
+
const result = await httpsJson('GET', '/apps', token);
|
|
117
|
+
const apps = Array.isArray(result) ? result : (result.applications || []);
|
|
118
|
+
return apps
|
|
119
|
+
.map((a) => ({ id: a._id || a.id, name: a.appName || a.name || a.repository?.name || '(unnamed app)' }))
|
|
120
|
+
.filter((a) => a.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve which workflow id to trigger for a platform, honoring custom ids
|
|
125
|
+
* saved at configure time and falling back to the shipped defaults. The legacy
|
|
126
|
+
* single CODEMAGIC_WORKFLOW_ID (older configs) is treated as the iOS workflow.
|
|
127
|
+
*/
|
|
128
|
+
function workflowIdFor(env, platform) {
|
|
129
|
+
if (platform === 'android') {
|
|
130
|
+
return env.CODEMAGIC_ANDROID_WORKFLOW_ID || DEFAULT_ANDROID_WORKFLOW;
|
|
131
|
+
}
|
|
132
|
+
return env.CODEMAGIC_IOS_WORKFLOW_ID || env.CODEMAGIC_WORKFLOW_ID || DEFAULT_IOS_WORKFLOW;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the environment variables to inject at trigger time, read from the
|
|
137
|
+
* project's `.env`. These travel with the build so the cloud has the SAME
|
|
138
|
+
* production keys you use locally — the user does not have to fill the
|
|
139
|
+
* Codemagic variable groups by hand.
|
|
140
|
+
*
|
|
141
|
+
* RevenueCat SDK keys (appl_/goog_/test_) are public keys, safe to send this
|
|
142
|
+
* way. Real secrets (keystore, App Store key, service account) stay in the
|
|
143
|
+
* dashboard and are NOT sent here.
|
|
144
|
+
*/
|
|
145
|
+
function resolveReleaseVars(projectEnv) {
|
|
146
|
+
const vars = { ENV: 'prod' };
|
|
147
|
+
|
|
148
|
+
const passthrough = [
|
|
149
|
+
'BACKEND_URL',
|
|
150
|
+
'RC_TEST_KEY',
|
|
151
|
+
'RC_IOS_PROD_KEY',
|
|
152
|
+
'RC_ANDROID_PROD_KEY',
|
|
153
|
+
'MIXPANEL_TOKEN',
|
|
154
|
+
'SENTRY_DSN',
|
|
155
|
+
];
|
|
156
|
+
for (const key of passthrough) {
|
|
157
|
+
const value = projectEnv[key];
|
|
158
|
+
if (value && value.trim()) vars[key] = value.trim();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// LLM chat endpoint may be named either way across template versions; the app
|
|
162
|
+
// reads AI_CHAT_ENDPOINT.
|
|
163
|
+
const endpoint = projectEnv.AI_CHAT_ENDPOINT || projectEnv.LLM_CHAT_ENDPOINT;
|
|
164
|
+
if (endpoint && endpoint.trim()) vars.AI_CHAT_ENDPOINT = endpoint.trim();
|
|
165
|
+
|
|
166
|
+
// Legacy single-key projects (pre test/prod split): promote a non-test key
|
|
167
|
+
// into the prod slot so release builds still get a real key.
|
|
168
|
+
if (!vars.RC_IOS_PROD_KEY && projectEnv.RC_IOS_API_KEY && !projectEnv.RC_IOS_API_KEY.startsWith('test_')) {
|
|
169
|
+
vars.RC_IOS_PROD_KEY = projectEnv.RC_IOS_API_KEY.trim();
|
|
170
|
+
}
|
|
171
|
+
if (!vars.RC_ANDROID_PROD_KEY && projectEnv.RC_ANDROID_API_KEY && !projectEnv.RC_ANDROID_API_KEY.startsWith('test_')) {
|
|
172
|
+
vars.RC_ANDROID_PROD_KEY = projectEnv.RC_ANDROID_API_KEY.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Numeric App Store id → APP_ID, used by the iOS workflow for build numbering.
|
|
176
|
+
if (projectEnv.APP_STORE_ID && projectEnv.APP_STORE_ID.trim()) {
|
|
177
|
+
vars.APP_ID = projectEnv.APP_STORE_ID.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return vars;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Trigger one build. `variables` is injected via the build's environment so the
|
|
185
|
+
* cloud build carries the project's production keys.
|
|
186
|
+
*/
|
|
187
|
+
async function triggerBuild(env, { workflowId, branch, variables }) {
|
|
87
188
|
const body = {
|
|
88
189
|
appId: env.CODEMAGIC_APP_ID,
|
|
89
|
-
workflowId
|
|
90
|
-
branch: env.CODEMAGIC_BRANCH || 'main',
|
|
190
|
+
workflowId,
|
|
191
|
+
branch: branch || env.CODEMAGIC_BRANCH || 'main',
|
|
91
192
|
};
|
|
193
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
194
|
+
body.environment = { variables };
|
|
195
|
+
}
|
|
92
196
|
return httpsJson('POST', '/builds', env.CODEMAGIC_API_TOKEN, body);
|
|
93
197
|
}
|
|
94
198
|
|
|
@@ -109,7 +213,6 @@ async function validateCodemagicSetup(projectDir) {
|
|
|
109
213
|
}
|
|
110
214
|
if (!env.CODEMAGIC_API_TOKEN) issues.push('missing_token');
|
|
111
215
|
if (!env.CODEMAGIC_APP_ID) issues.push('missing_app_id');
|
|
112
|
-
if (!env.CODEMAGIC_WORKFLOW_ID) issues.push('missing_workflow');
|
|
113
216
|
return { ok: issues.length === 0, issues, env };
|
|
114
217
|
}
|
|
115
218
|
|
|
@@ -118,9 +221,15 @@ module.exports = {
|
|
|
118
221
|
CODEMAGIC_API,
|
|
119
222
|
URL_CODEMAGIC_APPS,
|
|
120
223
|
URL_CODEMAGIC_API,
|
|
224
|
+
DEFAULT_IOS_WORKFLOW,
|
|
225
|
+
DEFAULT_ANDROID_WORKFLOW,
|
|
121
226
|
codemagicReleaseDocPath,
|
|
122
227
|
loadCodemagicEnv,
|
|
228
|
+
loadProjectEnv,
|
|
123
229
|
writeCodemagicEnv,
|
|
230
|
+
listApps,
|
|
231
|
+
workflowIdFor,
|
|
232
|
+
resolveReleaseVars,
|
|
124
233
|
triggerBuild,
|
|
125
234
|
getBuildStatus,
|
|
126
235
|
validateCodemagicSetup,
|
package/lib/utils/env-tools.js
CHANGED
|
@@ -201,6 +201,47 @@ function flutterSpawn(spawnFn, args, options = {}) {
|
|
|
201
201
|
const spawnFlutter = (args, options) => flutterSpawn(spawn, args, options);
|
|
202
202
|
const spawnSyncFlutter = (args, options) => flutterSpawn(spawnSync, args, options);
|
|
203
203
|
|
|
204
|
+
// Run the native-assets disable at most once per process — the setting is
|
|
205
|
+
// persisted in Flutter's global config, so re-running it every command would
|
|
206
|
+
// just add ~1s of cold flutter startup for nothing.
|
|
207
|
+
let nativeAssetsHandled = false;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Disable Flutter "native assets" compilation — Windows only.
|
|
211
|
+
*
|
|
212
|
+
* Why: `path_provider_foundation` (the Apple impl of `path_provider`, pulled in
|
|
213
|
+
* transitively by almost everything — google_fonts, home_widget,
|
|
214
|
+
* flutter_secure_storage…) ships a native-assets build hook for the
|
|
215
|
+
* `objective_c` package. On Windows, Dart's hook runner builds that compile
|
|
216
|
+
* command WITHOUT quoting the SDK/project path, so a username containing a
|
|
217
|
+
* SPACE (e.g. `C:\Users\John Silva`) breaks it — the shell reads only up to the
|
|
218
|
+
* space: `'C:\Users\John' is not recognized as a command`. It even fires for a
|
|
219
|
+
* Chrome/web run, where the Apple-only hook is pure dead weight.
|
|
220
|
+
*
|
|
221
|
+
* The hook is irrelevant on Windows (no iOS/macOS builds ever happen there), so
|
|
222
|
+
* turning native assets off removes the broken step at zero cost. `flutter
|
|
223
|
+
* config` is per-machine and global, so this never touches a Mac's iOS build.
|
|
224
|
+
*
|
|
225
|
+
* Idempotent and best-effort: it must never block `kasy run` / `kasy new`.
|
|
226
|
+
*
|
|
227
|
+
* @returns {{ ok: boolean, skipped?: boolean, error?: string }}
|
|
228
|
+
*/
|
|
229
|
+
function disableNativeAssetsWindows() {
|
|
230
|
+
if (!isWindows) return { ok: false, skipped: true };
|
|
231
|
+
if (nativeAssetsHandled) return { ok: true, skipped: true };
|
|
232
|
+
nativeAssetsHandled = true;
|
|
233
|
+
try {
|
|
234
|
+
const r = spawnSyncFlutter(['config', '--no-enable-native-assets'], {
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
encoding: 'utf8',
|
|
237
|
+
timeout: 120_000,
|
|
238
|
+
});
|
|
239
|
+
return { ok: r.status === 0 };
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return { ok: false, error: e.message };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
204
245
|
module.exports = {
|
|
205
246
|
isWindows,
|
|
206
247
|
homeDir,
|
|
@@ -210,4 +251,5 @@ module.exports = {
|
|
|
210
251
|
augmentedEnv,
|
|
211
252
|
spawnFlutter,
|
|
212
253
|
spawnSyncFlutter,
|
|
254
|
+
disableNativeAssetsWindows,
|
|
213
255
|
};
|
|
@@ -106,6 +106,14 @@ if ($LASTEXITCODE -ne 0) {
|
|
|
106
106
|
throw 'Flutter was installed but its first run failed (Dart SDK bootstrap). Check your internet connection and run this again.'
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
# 3b. Disable native assets. path_provider_foundation (Apple impl of
|
|
110
|
+
# path_provider) ships an objective_c native-assets build hook whose compile
|
|
111
|
+
# command isn't quoted, so a username with a SPACE (C:\\Users\\John Silva)
|
|
112
|
+
# breaks every build — even Chrome/web, where the Apple hook is dead weight.
|
|
113
|
+
# No iOS/macOS builds happen on Windows, so turning it off is free. Best
|
|
114
|
+
# effort: never fail the install over it.
|
|
115
|
+
& (Join-Path $bin 'flutter.bat') config --no-enable-native-assets 2>&1 | Out-Null
|
|
116
|
+
|
|
109
117
|
# 4. Persist flutter\\bin on the User PATH so every future terminal finds it.
|
|
110
118
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
111
119
|
if (-not $userPath) { $userPath = '' }
|