kadi-deploy 0.19.0

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.
Files changed (52) hide show
  1. package/.env.example +6 -0
  2. package/.prettierrc +6 -0
  3. package/README.md +589 -0
  4. package/agent.json +23 -0
  5. package/index.js +11 -0
  6. package/package.json +42 -0
  7. package/quick-command.txt +92 -0
  8. package/scripts/preflight.js +458 -0
  9. package/scripts/preflight.sh +300 -0
  10. package/src/cli/bid-selector.ts +222 -0
  11. package/src/cli/colors.ts +216 -0
  12. package/src/cli/index.ts +11 -0
  13. package/src/cli/prompts.ts +190 -0
  14. package/src/cli/spinners.ts +165 -0
  15. package/src/commands/deploy-local.ts +475 -0
  16. package/src/commands/deploy.ts +1342 -0
  17. package/src/commands/down.ts +679 -0
  18. package/src/commands/index.ts +10 -0
  19. package/src/commands/lock.ts +571 -0
  20. package/src/config/agent-loader.ts +177 -0
  21. package/src/config/index.ts +9 -0
  22. package/src/display/deployment-info.ts +220 -0
  23. package/src/display/pricing.ts +137 -0
  24. package/src/display/resources.ts +234 -0
  25. package/src/enhanced-registry-manager.ts +892 -0
  26. package/src/index.ts +307 -0
  27. package/src/infrastructure/registry.ts +269 -0
  28. package/src/schemas/profiles.ts +529 -0
  29. package/src/secrets/broker-urls.ts +109 -0
  30. package/src/secrets/handshake.ts +407 -0
  31. package/src/secrets/index.ts +69 -0
  32. package/src/secrets/inject-env.ts +171 -0
  33. package/src/secrets/nonce.ts +31 -0
  34. package/src/secrets/normalize.ts +204 -0
  35. package/src/secrets/prepare.ts +152 -0
  36. package/src/secrets/validate.ts +243 -0
  37. package/src/secrets/vault.ts +80 -0
  38. package/src/types/akash.ts +116 -0
  39. package/src/types/container-registry-ability.d.ts +158 -0
  40. package/src/types/external.ts +49 -0
  41. package/src/types.ts +211 -0
  42. package/src/utils/akt-price.ts +74 -0
  43. package/tests/agent-loader.test.ts +239 -0
  44. package/tests/autonomous.test.ts +244 -0
  45. package/tests/down.test.ts +1143 -0
  46. package/tests/lock.test.ts +1148 -0
  47. package/tests/nonce.test.ts +34 -0
  48. package/tests/normalize.test.ts +270 -0
  49. package/tests/secrets-schema.test.ts +301 -0
  50. package/tests/types.test.ts +198 -0
  51. package/tsconfig.json +18 -0
  52. package/vitest.config.ts +9 -0
@@ -0,0 +1,92 @@
1
+ Those commands are for "TestBackendWallet"
2
+
3
+ provider-services query market bid list \
4
+ --owner=akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
5
+ --node https://rpc.akashnet.net:443 \
6
+ --dseq 9900163 \
7
+ --state=open
8
+
9
+ provider-services tx deployment close \
10
+ --dseq 9900163 \
11
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
12
+ --from TestBackendWallet \
13
+ --node https://rpc.akashnet.net:443 \
14
+ --chain-id akashnet-2 \
15
+ --fees 500uakt --yes
16
+
17
+ provider-services tx deployment close \
18
+ --dseq 22139007 \
19
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
20
+ --from TestBackendWallet \
21
+ --node https://rpc.akashnet.net:443 \
22
+ --chain-id akashnet-2 \
23
+ --fees 500uakt \
24
+ --gas 250000 \
25
+ --yes
26
+
27
+ provider-services tx deployment close \
28
+ --dseq 9943223 \
29
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
30
+ --from TestBackendWallet \
31
+ --node https://rpc.akashnet.net:443 \
32
+ --chain-id akashnet-2 \
33
+ --gas 300000 \
34
+ --fees 800uakt --yes
35
+
36
+ provider-services query deployment list \
37
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
38
+ --node https://rpc.akashnet.net:443 \
39
+ --state active
40
+
41
+ provider-services query deployment list \
42
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
43
+ --node https://rpc.akashnet.net:443 | grep 'open' | wc -l
44
+
45
+ provider-services query deployment list \
46
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
47
+ --node https://rpc.akashnet.net:443 \
48
+ --state active
49
+
50
+ provider-services query deployment list \
51
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
52
+ --node https://rpc.akashnet.net:443 | grep 'open' | wc -l
53
+
54
+ # Brand new certificate Overwrite old one from the command line
55
+ provider-services tx cert generate client --from TestBackendWallet --overwrite
56
+
57
+ provider-services tx cert publish client \
58
+ --from TestBackendWallet \
59
+ --node https://rpc.akashnet.net:443 \
60
+ --chain-id akashnet-2 \
61
+ --fees 500uakt \
62
+ --gas auto
63
+
64
+ provider-services query cert list \
65
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
66
+ --node https://rpc.akashnet.net:443 \
67
+ --state valid
68
+
69
+ provider-services tx cert revoke client \
70
+ --serial <certificate-serial-number> \
71
+ --from TestBackendWallet \
72
+ --node https://rpc.akashnet.net:443 \
73
+ --chain-id akashnet-2 \
74
+ --gas-prices 0.025uakt \
75
+ --gas auto \
76
+ --gas-adjustment 1.5 --yes
77
+
78
+ provider-services query deployment get \
79
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
80
+ --dseq 9900163 \
81
+ --node https://rpc.akashnet.net:443
82
+
83
+ provider-services query market lease list \
84
+ --owner akash1etrf2qpsvuyuhs4g0e6swdn9gk4n9dsslrjdjr \
85
+ --node https://rpc.akashnet.net:443 \
86
+ --dseq 9943255
87
+
88
+ provider-services lease-logs \
89
+ --from TestBackendWallet \
90
+ --dseq 9900163 \
91
+ --provider $PROVIDER_ADDRESS \
92
+ --node https://rpc.akashnet.net:443 -f
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ // ──────────────────────────────────────────────────────────────
3
+ // kadi-deploy preflight script (cross-platform: macOS/Linux/Windows)
4
+ //
5
+ // Runs during `kadi install` to validate prerequisites.
6
+ // Checks for required and optional dependencies:
7
+ // - Node.js 18+ (required — already running if this executes)
8
+ // - Docker or Podman (required for local deploys)
9
+ // - frpc (required for KADI tunnel — Akash deploys)
10
+ //
11
+ // If frpc is missing, attempts auto-install:
12
+ // macOS: Homebrew → direct download
13
+ // Linux: direct download
14
+ // Windows: direct download (.zip)
15
+ // ──────────────────────────────────────────────────────────────
16
+
17
+ import { execFileSync } from 'node:child_process';
18
+ import { existsSync, mkdirSync, chmodSync, renameSync, unlinkSync, readdirSync, createWriteStream } from 'node:fs';
19
+ import { join, delimiter, dirname } from 'node:path';
20
+ import { homedir, platform, arch, tmpdir } from 'node:os';
21
+ import { pipeline } from 'node:stream/promises';
22
+ import { Readable } from 'node:stream';
23
+
24
+ // ── Globals ──────────────────────────────────────────────────
25
+ const PLATFORM = platform(); // 'darwin', 'linux', 'win32'
26
+ const ARCH = arch(); // 'arm64', 'x64', 'arm'
27
+ const HOME = homedir();
28
+ const IS_WIN = PLATFORM === 'win32';
29
+ const FRP_VERSION = '0.67.0';
30
+
31
+ let errors = 0;
32
+ let warnings = 0;
33
+
34
+ // ── Colors ───────────────────────────────────────────────────
35
+ const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
36
+ const c = {
37
+ green: (s) => supportsColor ? `\x1b[32m${s}\x1b[0m` : s,
38
+ yellow: (s) => supportsColor ? `\x1b[33m${s}\x1b[0m` : s,
39
+ red: (s) => supportsColor ? `\x1b[31m${s}\x1b[0m` : s,
40
+ bold: (s) => supportsColor ? `\x1b[1m${s}\x1b[0m` : s,
41
+ };
42
+
43
+ const ok = (msg) => console.log(c.green(' ✓') + ` ${msg}`);
44
+ const warn = (msg) => console.log(c.yellow(' ⚠') + ` ${msg}`);
45
+ const fail = (msg) => console.log(c.red(' ✗') + ` ${msg}`);
46
+ const info = (msg) => console.log(` ${msg}`);
47
+
48
+ // ── PATH augmentation ────────────────────────────────────────
49
+ // Non-interactive shells (SSH, lifecycle runners) may have a
50
+ // minimal PATH. Add common install locations.
51
+ function augmentPath() {
52
+ const extra = [];
53
+
54
+ if (IS_WIN) {
55
+ extra.push(
56
+ join(HOME, '.local', 'bin'),
57
+ join(HOME, 'bin'),
58
+ join(HOME, 'AppData', 'Local', 'Programs', 'frp'),
59
+ 'C:\\Program Files\\frp',
60
+ 'C:\\ProgramData\\chocolatey\\bin',
61
+ join(HOME, 'scoop', 'shims'),
62
+ );
63
+ } else {
64
+ extra.push(
65
+ '/opt/homebrew/bin',
66
+ '/opt/homebrew/sbin',
67
+ '/usr/local/bin',
68
+ '/usr/local/sbin',
69
+ join(HOME, '.local', 'bin'),
70
+ join(HOME, 'bin'),
71
+ );
72
+
73
+ // nvm — find installed node versions
74
+ const nvmDir = join(HOME, '.nvm', 'versions', 'node');
75
+ if (existsSync(nvmDir)) {
76
+ try {
77
+ const versions = readdirSync(nvmDir).filter(v => v.startsWith('v')).sort().reverse();
78
+ for (const v of versions) {
79
+ extra.push(join(nvmDir, v, 'bin'));
80
+ }
81
+ } catch { /* ignore */ }
82
+ }
83
+
84
+ // volta / fnm
85
+ extra.push(join(HOME, '.volta', 'bin'));
86
+ extra.push(join(HOME, '.fnm', 'aliases', 'default', 'bin'));
87
+ }
88
+
89
+ const pathDirs = new Set(process.env.PATH.split(delimiter));
90
+ for (const p of extra) {
91
+ if (existsSync(p) && !pathDirs.has(p)) {
92
+ process.env.PATH = p + delimiter + process.env.PATH;
93
+ pathDirs.add(p);
94
+ }
95
+ }
96
+ }
97
+
98
+ // ── Utility: find a binary ───────────────────────────────────
99
+ function which(name) {
100
+ // Try the OS-native which/where
101
+ try {
102
+ const cmd = IS_WIN ? 'where' : 'which';
103
+ const result = execFileSync(cmd, [name], {
104
+ encoding: 'utf-8',
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ env: process.env,
107
+ }).trim();
108
+ // `where` on Windows can return multiple lines
109
+ return result.split(/\r?\n/)[0];
110
+ } catch { /* not found */ }
111
+
112
+ // Fallback: manually check common locations
113
+ const suffix = IS_WIN ? '.exe' : '';
114
+ const candidates = IS_WIN
115
+ ? [
116
+ join(HOME, '.local', 'bin', name + suffix),
117
+ join(HOME, 'AppData', 'Local', 'Programs', 'frp', name + suffix),
118
+ `C:\\Program Files\\frp\\${name}${suffix}`,
119
+ ]
120
+ : [
121
+ `/usr/local/bin/${name}`,
122
+ `/opt/homebrew/bin/${name}`,
123
+ join(HOME, '.local', 'bin', name),
124
+ join(HOME, 'bin', name),
125
+ ];
126
+
127
+ for (const p of candidates) {
128
+ if (existsSync(p)) return p;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ // ── Utility: run a command and return stdout (or null) ───────
134
+ function run(cmd, args = []) {
135
+ try {
136
+ return execFileSync(cmd, args, {
137
+ encoding: 'utf-8',
138
+ stdio: ['pipe', 'pipe', 'pipe'],
139
+ env: process.env,
140
+ timeout: 15000,
141
+ }).trim();
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // ── Utility: download a file ─────────────────────────────────
148
+ async function download(url, destPath) {
149
+ const resp = await fetch(url, { redirect: 'follow' });
150
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
151
+ const fileStream = createWriteStream(destPath);
152
+ const nodeStream = Readable.fromWeb(resp.body);
153
+ await pipeline(nodeStream, fileStream);
154
+ }
155
+
156
+ // ── Utility: extract frpc from .tar.gz ───────────────────────
157
+ async function extractTarGz(tarPath, binaryName, destPath) {
158
+ // Use `tar` CLI (available on macOS, Linux, and modern Windows)
159
+ const tmpExtract = join(tmpdir(), `frpc-extract-${Date.now()}`);
160
+ mkdirSync(tmpExtract, { recursive: true });
161
+
162
+ try {
163
+ execFileSync('tar', ['xzf', tarPath, '-C', tmpExtract], {
164
+ stdio: ['pipe', 'pipe', 'pipe'],
165
+ timeout: 30000,
166
+ });
167
+
168
+ // Find the frpc binary in the extracted directory
169
+ const extracted = findFile(tmpExtract, binaryName);
170
+ if (!extracted) throw new Error(`${binaryName} not found in archive`);
171
+
172
+ // Ensure dest dir exists
173
+ mkdirSync(dirname(destPath), { recursive: true });
174
+ renameSync(extracted, destPath);
175
+ if (!IS_WIN) chmodSync(destPath, 0o755);
176
+ return true;
177
+ } finally {
178
+ // Cleanup
179
+ try {
180
+ if (IS_WIN) {
181
+ execFileSync('cmd', ['/c', 'rmdir', '/s', '/q', tmpExtract], { stdio: 'pipe' });
182
+ } else {
183
+ execFileSync('rm', ['-rf', tmpExtract], { stdio: 'pipe' });
184
+ }
185
+ } catch { /* ignore */ }
186
+ }
187
+ }
188
+
189
+ // ── Utility: extract frpc from .zip (Windows) ────────────────
190
+ async function extractZip(zipPath, binaryName, destPath) {
191
+ const tmpExtract = join(tmpdir(), `frpc-extract-${Date.now()}`);
192
+ mkdirSync(tmpExtract, { recursive: true });
193
+
194
+ try {
195
+ // PowerShell's Expand-Archive works on all modern Windows
196
+ execFileSync('powershell', [
197
+ '-NoProfile', '-Command',
198
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${tmpExtract}' -Force`
199
+ ], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000 });
200
+
201
+ const extracted = findFile(tmpExtract, binaryName);
202
+ if (!extracted) throw new Error(`${binaryName} not found in archive`);
203
+
204
+ mkdirSync(dirname(destPath), { recursive: true });
205
+ renameSync(extracted, destPath);
206
+ return true;
207
+ } finally {
208
+ try { execFileSync('rmdir', ['/s', '/q', tmpExtract], { stdio: 'pipe', shell: true }); } catch { /* ignore */ }
209
+ }
210
+ }
211
+
212
+ // ── Utility: recursively find a file ─────────────────────────
213
+ function findFile(dir, name) {
214
+ try {
215
+ const entries = readdirSync(dir, { withFileTypes: true });
216
+ for (const entry of entries) {
217
+ const full = join(dir, entry.name);
218
+ if (entry.isFile() && entry.name === name) return full;
219
+ if (entry.isDirectory()) {
220
+ const found = findFile(full, name);
221
+ if (found) return found;
222
+ }
223
+ }
224
+ } catch { /* ignore */ }
225
+ return null;
226
+ }
227
+
228
+ // ── Check: Node.js ───────────────────────────────────────────
229
+ function checkNode() {
230
+ console.log('Checking Node.js...');
231
+ const major = parseInt(process.versions.node.split('.')[0], 10);
232
+ if (major >= 18) {
233
+ ok(`Node.js ${process.versions.node}`);
234
+ } else {
235
+ fail(`Node.js ${process.versions.node} (18+ required)`);
236
+ info('Install from https://nodejs.org/');
237
+ errors++;
238
+ }
239
+ }
240
+
241
+ // ── Check: Container engine ──────────────────────────────────
242
+ function checkContainerEngine() {
243
+ console.log('Checking container engine...');
244
+
245
+ const dockerPath = which('docker');
246
+ if (dockerPath) {
247
+ const ver = run(dockerPath, ['--version']) || 'unknown';
248
+ ok(`Docker: ${ver}`);
249
+ return;
250
+ }
251
+
252
+ const podmanPath = which('podman');
253
+ if (podmanPath) {
254
+ const ver = run(podmanPath, ['--version']) || 'unknown';
255
+ ok(`Podman: ${ver}`);
256
+ return;
257
+ }
258
+
259
+ warn('No container engine found (Docker or Podman)');
260
+ info('Required for local deploys. Not needed for Akash-only deploys.');
261
+ info('Install Docker: https://docs.docker.com/get-docker/');
262
+ warnings++;
263
+ }
264
+
265
+ // ── Check: frpc ──────────────────────────────────────────────
266
+ async function checkFrpc() {
267
+ console.log('Checking frpc (tunnel client)...');
268
+
269
+ // 1. Check if already installed
270
+ let frpcPath = which('frpc');
271
+ if (frpcPath) {
272
+ const ver = run(frpcPath, ['--version']) || 'unknown';
273
+ ok(`frpc ${ver} (${frpcPath})`);
274
+ return;
275
+ }
276
+
277
+ // 2. Not found — attempt auto-install
278
+ let installed = false;
279
+
280
+ // macOS: try Homebrew first
281
+ if (PLATFORM === 'darwin' && !installed) {
282
+ const brewPath = which('brew');
283
+ if (brewPath) {
284
+ warn('frpc not found — attempting install via Homebrew...');
285
+ const brewResult = run(brewPath, ['install', 'frpc']);
286
+ if (brewResult !== null) {
287
+ // Verify the binary actually exists (XProtect may delete it)
288
+ frpcPath = which('frpc');
289
+ if (frpcPath) {
290
+ const ver = run(frpcPath, ['--version']) || 'unknown';
291
+ ok(`frpc ${ver} (installed via Homebrew)`);
292
+ installed = true;
293
+ } else {
294
+ warn('Homebrew installed frpc formula but binary is missing (XProtect may have removed it)');
295
+ }
296
+ } else {
297
+ warn('Homebrew install failed — trying direct download...');
298
+ }
299
+ }
300
+ }
301
+
302
+ // macOS/Linux: direct download .tar.gz
303
+ if ((PLATFORM === 'darwin' || PLATFORM === 'linux') && !installed) {
304
+ const archMap = {
305
+ 'darwin-arm64': 'darwin_arm64',
306
+ 'darwin-x64': 'darwin_amd64',
307
+ 'linux-x64': 'linux_amd64',
308
+ 'linux-arm64': 'linux_arm64',
309
+ 'linux-arm': 'linux_arm',
310
+ };
311
+ const frpArch = archMap[`${PLATFORM}-${ARCH}`];
312
+
313
+ if (frpArch) {
314
+ warn('frpc not found — attempting direct download...');
315
+ const url = `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_${frpArch}.tar.gz`;
316
+ const installDir = join(HOME, '.local', 'bin');
317
+ const destPath = join(installDir, 'frpc');
318
+ const tmpFile = join(tmpdir(), `frpc-${Date.now()}.tar.gz`);
319
+
320
+ try {
321
+ await download(url, tmpFile);
322
+ await extractTarGz(tmpFile, 'frpc', destPath);
323
+
324
+ // macOS: remove quarantine attribute
325
+ if (PLATFORM === 'darwin') {
326
+ run('xattr', ['-d', 'com.apple.quarantine', destPath]);
327
+ }
328
+
329
+ const ver = run(destPath, ['--version']);
330
+ if (ver) {
331
+ ok(`frpc ${ver} (installed to ${destPath})`);
332
+ installed = true;
333
+
334
+ // Add to PATH for this process (and log for user)
335
+ if (!process.env.PATH.split(delimiter).includes(installDir)) {
336
+ process.env.PATH = installDir + delimiter + process.env.PATH;
337
+ warn(`${installDir} added to PATH for this session`);
338
+ const rcFile = PLATFORM === 'darwin' ? '~/.zshrc' : '~/.bashrc';
339
+ info(`Add permanently: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ${rcFile}`);
340
+ }
341
+ } else {
342
+ fail('frpc downloaded but binary won\'t execute');
343
+ if (PLATFORM === 'darwin') {
344
+ info(`Try: xattr -d com.apple.quarantine ${destPath}`);
345
+ }
346
+ }
347
+ } catch (err) {
348
+ fail(`Direct download failed: ${err.message}`);
349
+ } finally {
350
+ try { unlinkSync(tmpFile); } catch { /* ignore */ }
351
+ }
352
+ }
353
+ }
354
+
355
+ // Windows: direct download .zip
356
+ if (PLATFORM === 'win32' && !installed) {
357
+ const archMap = {
358
+ 'x64': 'windows_amd64',
359
+ 'arm64': 'windows_arm64',
360
+ };
361
+ const frpArch = archMap[ARCH];
362
+
363
+ if (frpArch) {
364
+ warn('frpc not found — attempting direct download...');
365
+ const url = `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_${frpArch}.zip`;
366
+ const installDir = join(HOME, '.local', 'bin');
367
+ const destPath = join(installDir, 'frpc.exe');
368
+ const tmpFile = join(tmpdir(), `frpc-${Date.now()}.zip`);
369
+
370
+ try {
371
+ await download(url, tmpFile);
372
+ await extractZip(tmpFile, 'frpc.exe', destPath);
373
+
374
+ const ver = run(destPath, ['--version']);
375
+ if (ver) {
376
+ ok(`frpc ${ver} (installed to ${destPath})`);
377
+ installed = true;
378
+
379
+ if (!process.env.PATH.split(delimiter).includes(installDir)) {
380
+ process.env.PATH = installDir + delimiter + process.env.PATH;
381
+ warn(`${installDir} added to PATH for this session`);
382
+ info(`Add permanently: setx PATH "%PATH%;${installDir}"`);
383
+ }
384
+ } else {
385
+ fail('frpc downloaded but won\'t execute');
386
+ }
387
+ } catch (err) {
388
+ fail(`Direct download failed: ${err.message}`);
389
+ } finally {
390
+ try { unlinkSync(tmpFile); } catch { /* ignore */ }
391
+ }
392
+ }
393
+ }
394
+
395
+ // If all install attempts failed, show manual instructions
396
+ if (!installed) {
397
+ warn('frpc not installed — KADI tunnel will not work');
398
+ console.log('');
399
+ info(c.bold('frpc is required for Akash deployments with local images.'));
400
+ info('Without it, kadi-deploy cannot create a tunnel for providers');
401
+ info('to pull your container images.');
402
+ console.log('');
403
+ info(c.bold('Install frpc:'));
404
+
405
+ if (PLATFORM === 'darwin') {
406
+ info(' brew install frpc');
407
+ info('');
408
+ info('If Homebrew install fails (macOS XProtect may block it):');
409
+ info(' 1. Download from https://github.com/fatedier/frp/releases');
410
+ info(' (choose frp_*_darwin_arm64.tar.gz for Apple Silicon)');
411
+ info(' 2. Extract: tar xzf frp_*.tar.gz');
412
+ info(' 3. Move: sudo mv frp_*/frpc /usr/local/bin/');
413
+ info(' 4. Allow: xattr -d com.apple.quarantine /usr/local/bin/frpc');
414
+ } else if (PLATFORM === 'win32') {
415
+ info(' 1. Download from https://github.com/fatedier/frp/releases');
416
+ info(' (choose frp_*_windows_amd64.zip)');
417
+ info(' 2. Extract frpc.exe to %USERPROFILE%\\.local\\bin\\');
418
+ info(' 3. Add to PATH: setx PATH "%PATH%;%USERPROFILE%\\.local\\bin"');
419
+ } else {
420
+ info(' Download from https://github.com/fatedier/frp/releases');
421
+ info(' Extract and move frpc to ~/.local/bin/ or /usr/local/bin/');
422
+ }
423
+ console.log('');
424
+ info('After installing, re-run: kadi install');
425
+ console.log('');
426
+ warnings++;
427
+ }
428
+ }
429
+
430
+ // ── Main ─────────────────────────────────────────────────────
431
+ async function main() {
432
+ console.log(c.bold('kadi-deploy preflight'));
433
+ console.log('');
434
+
435
+ augmentPath();
436
+ checkNode();
437
+ checkContainerEngine();
438
+ await checkFrpc();
439
+
440
+ // Summary
441
+ console.log('');
442
+ if (errors > 0) {
443
+ fail(c.bold(`Preflight failed with ${errors} error(s)`));
444
+ process.exit(1);
445
+ } else if (warnings > 0) {
446
+ warn(c.bold(`Preflight passed with ${warnings} warning(s)`));
447
+ info('kadi-deploy will work but some features may be limited.');
448
+ process.exit(0);
449
+ } else {
450
+ ok(c.bold('All checks passed'));
451
+ process.exit(0);
452
+ }
453
+ }
454
+
455
+ main().catch((err) => {
456
+ fail(`Preflight error: ${err.message}`);
457
+ process.exit(1);
458
+ });