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.
- package/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- 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
|
+
});
|