kasy-cli 1.21.6 → 1.21.8
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/lib/commands/new.js +47 -11
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +3 -2
- package/lib/scaffold/backends/supabase/deploy.js +23 -13
- package/lib/scaffold/shared/fcm-service-account.js +15 -5
- package/lib/utils/env-tools.js +25 -0
- package/lib/utils/i18n/messages-en.js +4 -0
- package/lib/utils/i18n/messages-es.js +4 -0
- package/lib/utils/i18n/messages-pt.js +4 -0
- package/package.json +1 -1
package/lib/commands/new.js
CHANGED
|
@@ -131,20 +131,56 @@ async function promptOrganizationIfNeeded(tr, onCancel) {
|
|
|
131
131
|
* bail out with the exact commands to switch.
|
|
132
132
|
*/
|
|
133
133
|
async function confirmIdentities(backend, gcloudAccount, tr, cancel) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
let firebaseAccount = await getFirebaseAccount();
|
|
135
|
+
let supabaseLoggedIn = backend === 'supabase' ? (await checkLoggedIn()).ok : true;
|
|
136
|
+
|
|
137
|
+
const showStatus = () => {
|
|
138
|
+
const notLogged = kleur.yellow(tr('new.accounts.notLoggedIn'));
|
|
139
|
+
const lines = [
|
|
140
|
+
`🔑 Google Cloud: ${gcloudAccount ? kleur.cyan(gcloudAccount) : notLogged}`,
|
|
141
|
+
`🔥 Firebase: ${firebaseAccount ? kleur.cyan(firebaseAccount) : notLogged}`,
|
|
142
|
+
];
|
|
143
|
+
if (backend === 'supabase') {
|
|
144
|
+
lines.push(`🟢 Supabase: ${supabaseLoggedIn ? kleur.cyan(tr('new.accounts.connected')) : notLogged}`);
|
|
145
|
+
}
|
|
146
|
+
ui.note(lines.join('\n'), tr('new.accounts.title'));
|
|
147
|
+
};
|
|
148
|
+
showStatus();
|
|
149
|
+
|
|
150
|
+
// For any required service that's NOT logged in, offer to log in right here:
|
|
151
|
+
// Enter runs the service's own login command (which opens the browser / asks
|
|
152
|
+
// for the token), then we re-check. gcloud was already verified above.
|
|
153
|
+
const offerLogin = async (name, cmd, revalidate) => {
|
|
154
|
+
const doLogin = await ui.confirm({ message: tr('new.accounts.loginPrompt', { name }), initialValue: true, onCancel: cancel });
|
|
155
|
+
if (!doLogin) return;
|
|
156
|
+
ui.log.message(tr('new.accounts.loggingIn', { name }));
|
|
157
|
+
const { spawnSync } = require('node:child_process');
|
|
158
|
+
spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
159
|
+
await revalidate();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (!firebaseAccount) {
|
|
163
|
+
await offerLogin('Firebase', 'firebase login', async () => { firebaseAccount = await getFirebaseAccount(); });
|
|
143
164
|
}
|
|
165
|
+
if (backend === 'supabase' && !supabaseLoggedIn) {
|
|
166
|
+
await offerLogin('Supabase', 'supabase login', async () => { supabaseLoggedIn = (await checkLoggedIn()).ok; });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Still missing a required login (user declined or it failed)? Stop with the
|
|
170
|
+
// exact command so they can do it and re-run.
|
|
171
|
+
const stillMissing = [];
|
|
172
|
+
if (!firebaseAccount) stillMissing.push(`🔥 Firebase: ${kleur.cyan('firebase login')}`);
|
|
173
|
+
if (backend === 'supabase' && !supabaseLoggedIn) stillMissing.push(`🟢 Supabase: ${kleur.cyan('supabase login')}`);
|
|
174
|
+
if (stillMissing.length > 0) {
|
|
175
|
+
ui.log.error(tr('new.accounts.needLogin'));
|
|
176
|
+
ui.note(stillMissing.join('\n'), tr('new.accounts.loginTitle'));
|
|
177
|
+
ui.cancel(tr('new.accounts.rerun'));
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
144
181
|
if (gcloudAccount && firebaseAccount && gcloudAccount !== firebaseAccount) {
|
|
145
|
-
|
|
182
|
+
ui.log.warn(`⚠ ${tr('new.accounts.mismatch')}`);
|
|
146
183
|
}
|
|
147
|
-
ui.note(lines.join('\n'), tr('new.accounts.title'));
|
|
148
184
|
|
|
149
185
|
const ok = await ui.confirm({ message: tr('new.accounts.confirm'), initialValue: true, onCancel: cancel });
|
|
150
186
|
if (!ok) {
|
|
@@ -19,6 +19,7 @@ const path = require('node:path');
|
|
|
19
19
|
const fs = require('fs-extra');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const { getInstallGuide } = require('../../../utils/checks');
|
|
22
|
+
const { keytoolBin } = require('../../../utils/env-tools');
|
|
22
23
|
|
|
23
24
|
const execAsync = promisify(exec);
|
|
24
25
|
|
|
@@ -536,7 +537,7 @@ async function ensureDebugKeystore() {
|
|
|
536
537
|
if (!(await fs.pathExists(DEBUG_KEYSTORE))) {
|
|
537
538
|
await fs.ensureDir(androidDir);
|
|
538
539
|
const createResult = await run(
|
|
539
|
-
|
|
540
|
+
`${keytoolBin()} -genkeypair -v -keystore "${DEBUG_KEYSTORE}" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US" -noprompt`
|
|
540
541
|
);
|
|
541
542
|
if (!createResult.ok) return { ok: false, error: createResult.stderr || createResult.error };
|
|
542
543
|
}
|
|
@@ -552,7 +553,7 @@ async function extractSha1() {
|
|
|
552
553
|
if (!ensureResult.ok) return ensureResult;
|
|
553
554
|
|
|
554
555
|
const result = await run(
|
|
555
|
-
|
|
556
|
+
`${keytoolBin()} -list -v -keystore "${DEBUG_KEYSTORE}" -alias androiddebugkey -storepass android -keypass android`
|
|
556
557
|
);
|
|
557
558
|
if (!result.ok) return { ok: false, error: result.stderr || result.error };
|
|
558
559
|
|
|
@@ -13,16 +13,29 @@ const { exec, execFile } = require('node:child_process');
|
|
|
13
13
|
const { promisify } = require('node:util');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const fs = require('fs-extra');
|
|
16
|
+
const { augmentedEnv } = require('../../../utils/env-tools');
|
|
16
17
|
|
|
17
18
|
const execAsync = promisify(exec);
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parse JSON from a CLI's stdout tolerantly. The Supabase CLI sometimes prints
|
|
23
|
+
* an "update available" notice before the JSON payload (more often on Windows),
|
|
24
|
+
* which would break a strict JSON.parse — so we grab from the first [ or {.
|
|
25
|
+
*/
|
|
26
|
+
function parseJsonLoose(stdout) {
|
|
27
|
+
const s = (stdout || '').trim();
|
|
28
|
+
const start = s.search(/[[{]/);
|
|
29
|
+
if (start === -1) return null;
|
|
30
|
+
try { return JSON.parse(s.slice(start)); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
async function run(cmd, cwd, env = {}) {
|
|
21
34
|
try {
|
|
22
35
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
23
36
|
cwd,
|
|
24
37
|
maxBuffer: 10 * 1024 * 1024,
|
|
25
|
-
env: { ...
|
|
38
|
+
env: { ...augmentedEnv(), ...env },
|
|
26
39
|
});
|
|
27
40
|
return { ok: true, stdout, stderr };
|
|
28
41
|
} catch (err) {
|
|
@@ -40,13 +53,14 @@ async function run(cmd, cwd, env = {}) {
|
|
|
40
53
|
*/
|
|
41
54
|
async function checkLoggedIn() {
|
|
42
55
|
const result = await run('supabase projects list -o json', process.cwd());
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} catch {
|
|
56
|
+
// Trust the exit code: a successful command means we're authenticated. Only a
|
|
57
|
+
// non-zero exit (e.g. "Access token not provided") means login is required.
|
|
58
|
+
// The list itself is parsed best-effort (tolerant of update notices).
|
|
59
|
+
if (!result.ok) {
|
|
48
60
|
return { ok: false, error: 'Supabase login required. Run: supabase login' };
|
|
49
61
|
}
|
|
62
|
+
const data = parseJsonLoose(result.stdout);
|
|
63
|
+
return { ok: true, projects: Array.isArray(data) ? data : [] };
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -69,13 +83,9 @@ async function getProjectsByOrg(orgId) {
|
|
|
69
83
|
async function getOrgsList() {
|
|
70
84
|
const result = await run('supabase orgs list -o json', process.cwd());
|
|
71
85
|
if (!result.ok) return { ok: false, error: result.error };
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return { ok: true, orgs: orgs.map((o) => ({ id: o.id, name: o.name || o.id })) };
|
|
76
|
-
} catch {
|
|
77
|
-
return { ok: false, error: 'Could not parse orgs list. Run: supabase login' };
|
|
78
|
-
}
|
|
86
|
+
const data = parseJsonLoose(result.stdout);
|
|
87
|
+
const orgs = Array.isArray(data) ? data : [];
|
|
88
|
+
return { ok: true, orgs: orgs.map((o) => ({ id: o.id, name: o.name || o.id })) };
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
|
@@ -101,11 +101,21 @@ async function createFcmServiceAccountKey(projectId) {
|
|
|
101
101
|
|
|
102
102
|
const tmpPath = path.join(os.tmpdir(), `kasy-fcm-${Date.now()}.json`);
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
104
|
+
// Right after granting the FCM admin role (and on a brand-new project), the
|
|
105
|
+
// IAM permission takes a moment to propagate — so the first key-create often
|
|
106
|
+
// fails with "permission still propagating". Back off and retry a few times
|
|
107
|
+
// before giving up, instead of leaving push half-configured.
|
|
108
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
109
|
+
let keyResult;
|
|
110
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
111
|
+
keyResult = await run(
|
|
112
|
+
`gcloud iam service-accounts keys create "${tmpPath}"` +
|
|
113
|
+
` --iam-account="${saEmail}"` +
|
|
114
|
+
` --project=${projectId.trim()}`
|
|
115
|
+
);
|
|
116
|
+
if (keyResult.ok) break;
|
|
117
|
+
if (attempt < 4) await sleep(7000);
|
|
118
|
+
}
|
|
109
119
|
|
|
110
120
|
if (!keyResult.ok) {
|
|
111
121
|
await fs.remove(tmpPath).catch(() => {});
|
package/lib/utils/env-tools.js
CHANGED
|
@@ -69,6 +69,30 @@ function pubCacheBin(name) {
|
|
|
69
69
|
return `"${file}"`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Quoted path to `keytool` (used for the Android debug SHA-1). It ships with
|
|
74
|
+
* the JDK and is almost never on PATH on Windows, so we look in JAVA_HOME and
|
|
75
|
+
* Android Studio's bundled JBR before falling back to bare `keytool`.
|
|
76
|
+
*/
|
|
77
|
+
function keytoolBin() {
|
|
78
|
+
const exe = isWindows ? 'keytool.exe' : 'keytool';
|
|
79
|
+
const candidates = [];
|
|
80
|
+
if (process.env.JAVA_HOME) candidates.push(path.join(process.env.JAVA_HOME, 'bin', exe));
|
|
81
|
+
if (isWindows) {
|
|
82
|
+
const pf = process.env.ProgramFiles;
|
|
83
|
+
const local = process.env.LOCALAPPDATA;
|
|
84
|
+
if (pf) candidates.push(path.win32.join(pf, 'Android', 'Android Studio', 'jbr', 'bin', exe));
|
|
85
|
+
if (local) candidates.push(path.win32.join(local, 'Programs', 'Android Studio', 'jbr', 'bin', exe));
|
|
86
|
+
if (pf) candidates.push(path.win32.join(pf, 'Android', 'Android Studio', 'jre', 'bin', exe));
|
|
87
|
+
} else {
|
|
88
|
+
candidates.push('/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/keytool');
|
|
89
|
+
}
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
if (fs.existsSync(candidate)) return `"${candidate}"`;
|
|
92
|
+
}
|
|
93
|
+
return 'keytool';
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/**
|
|
73
97
|
* Well-known install dirs of tools that may not be on the frozen session PATH
|
|
74
98
|
* yet — e.g. the Google Cloud SDK right after it's installed mid-run. Only
|
|
@@ -114,5 +138,6 @@ module.exports = {
|
|
|
114
138
|
homeDir,
|
|
115
139
|
pubCacheBinDir,
|
|
116
140
|
pubCacheBin,
|
|
141
|
+
keytoolBin,
|
|
117
142
|
augmentedEnv,
|
|
118
143
|
};
|
|
@@ -223,6 +223,10 @@ module.exports = {
|
|
|
223
223
|
'new.accounts.confirm': 'Create the project on these accounts?',
|
|
224
224
|
'new.accounts.switchTitle': 'To switch accounts, run and try again:',
|
|
225
225
|
'new.accounts.rerun': 'Switch the account and run `kasy new` again.',
|
|
226
|
+
'new.accounts.needLogin': 'You need to be logged in to continue.',
|
|
227
|
+
'new.accounts.loginTitle': 'Log in and run `kasy new` again:',
|
|
228
|
+
'new.accounts.loginPrompt': 'Log in to {name} now?',
|
|
229
|
+
'new.accounts.loggingIn': 'Opening {name} login…',
|
|
226
230
|
'new.firebase.billing.required': 'You do not have a billing account on Google Cloud yet. Firebase needs the Blaze plan to use Storage and Cloud Functions.',
|
|
227
231
|
'new.firebase.billing.create.steps': 'Opening the billing account creation page. Create the account (credit card required, no charges within the free quota) and come back here:',
|
|
228
232
|
'new.firebase.billing.created.ready': 'I created the billing account, ready to continue?',
|
|
@@ -223,6 +223,10 @@ module.exports = {
|
|
|
223
223
|
'new.accounts.confirm': '¿Crear el proyecto en estas cuentas?',
|
|
224
224
|
'new.accounts.switchTitle': 'Para cambiar de cuenta, ejecuta y prueba de nuevo:',
|
|
225
225
|
'new.accounts.rerun': 'Cambia la cuenta y ejecuta `kasy new` de nuevo.',
|
|
226
|
+
'new.accounts.needLogin': 'Necesitas estar conectado para continuar.',
|
|
227
|
+
'new.accounts.loginTitle': 'Inicia sesión y ejecuta `kasy new` de nuevo:',
|
|
228
|
+
'new.accounts.loginPrompt': '¿Iniciar sesión en {name} ahora?',
|
|
229
|
+
'new.accounts.loggingIn': 'Abriendo el inicio de sesión de {name}…',
|
|
226
230
|
'new.firebase.billing.required': 'Aún no tienes una cuenta de facturación en Google Cloud. Firebase necesita el plan Blaze para usar Storage y Cloud Functions.',
|
|
227
231
|
'new.firebase.billing.create.steps': 'Abriendo la página de creación de cuenta de facturación. Crea la cuenta (tarjeta de crédito requerida, sin cargos dentro de la cuota gratuita) y vuelve aquí:',
|
|
228
232
|
'new.firebase.billing.created.ready': 'Ya creé la cuenta de facturación, ¿puedo continuar?',
|
|
@@ -223,6 +223,10 @@ module.exports = {
|
|
|
223
223
|
'new.accounts.confirm': 'É nessas contas que você quer criar o projeto?',
|
|
224
224
|
'new.accounts.switchTitle': 'Para trocar de conta, rode e tente de novo:',
|
|
225
225
|
'new.accounts.rerun': 'Troque a conta e rode `kasy new` novamente.',
|
|
226
|
+
'new.accounts.needLogin': 'Você precisa estar logado para continuar.',
|
|
227
|
+
'new.accounts.loginTitle': 'Faça login e rode `kasy new` de novo:',
|
|
228
|
+
'new.accounts.loginPrompt': 'Fazer login no {name} agora?',
|
|
229
|
+
'new.accounts.loggingIn': 'Abrindo login do {name}…',
|
|
226
230
|
'new.firebase.billing.required': 'Você ainda não tem uma conta de faturamento no Google Cloud. O Firebase precisa do plano Blaze para usar Storage e Cloud Functions.',
|
|
227
231
|
'new.firebase.billing.create.steps': 'Vou abrir a página de criação de conta de faturamento. Crie a conta (cartão de crédito, sem cobrança até bater a cota gratuita) e volte aqui:',
|
|
228
232
|
'new.firebase.billing.created.ready': 'Já criei a conta de faturamento, pode seguir?',
|