iranti-control-plane 0.4.1 → 0.4.3
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/README.md +18 -1
- package/bin/iranti-cp.js +516 -1
- package/dist/server/bundle.cjs +512 -55
- package/package.json +1 -1
- package/public/control-plane/assets/index-Dmr1zIws.css +1 -0
- package/public/control-plane/assets/index-Frqi9R2P.js +77 -0
- package/public/control-plane/index.html +2 -2
- package/public/control-plane/assets/index-OSHzCl4w.css +0 -1
- package/public/control-plane/assets/index-XZAadN0I.js +0 -77
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Local-first operator dashboard for [Iranti](https://github.com/nfemmanuel/iranti
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
Current package version: `0.4.
|
|
7
|
+
Current package version: `0.4.3`.
|
|
8
8
|
The operator surface is live and under active UX hardening.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
@@ -18,6 +18,21 @@ iranti-cp
|
|
|
18
18
|
|
|
19
19
|
That path uses the bundled server and picks the first free port in `3000-3010` unless `CONTROL_PLANE_PORT` is set.
|
|
20
20
|
|
|
21
|
+
Useful packaged CLI commands:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
iranti-cp open
|
|
25
|
+
iranti-cp start --port 3010
|
|
26
|
+
iranti-cp stop --port 3010
|
|
27
|
+
iranti-cp status
|
|
28
|
+
iranti-cp uninstall
|
|
29
|
+
iranti-cp doctor --instance my_instance
|
|
30
|
+
iranti-cp upgrade self
|
|
31
|
+
iranti-cp upgrade iranti --all --dry-run
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `iranti-cp --help` to see the full command list.
|
|
35
|
+
|
|
21
36
|
## Quick Start
|
|
22
37
|
|
|
23
38
|
**Prerequisites**: Node.js 20+, a running Iranti instance with a PostgreSQL database.
|
|
@@ -128,4 +143,6 @@ The control plane is a standalone Express server + React SPA that connects to th
|
|
|
128
143
|
See `docs/specs/control-plane-api.md` for the full API spec and `docs/prd/control-plane.md` for product requirements.
|
|
129
144
|
|
|
130
145
|
For release and manual publish checks, see [`docs/guides/releasing.md`](docs/guides/releasing.md).
|
|
146
|
+
|
|
147
|
+
If npm publish is run from GitHub Actions, the repo `NPM_TOKEN` secret must be an npm **Automation token**. A standard token that still requires OTP will fail with `EOTP`.
|
|
131
148
|
|
package/bin/iranti-cp.js
CHANGED
|
@@ -1,3 +1,518 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
require(
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn, spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const BUNDLE = path.join(ROOT, 'dist', 'server', 'bundle.cjs');
|
|
9
|
+
const PACKAGE_JSON = path.join(ROOT, 'package.json');
|
|
10
|
+
const PACKAGE_NAME = 'iranti-control-plane';
|
|
11
|
+
|
|
12
|
+
function readPackageVersion() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8')).version || '0.0.0';
|
|
15
|
+
} catch {
|
|
16
|
+
return '0.0.0';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`iranti-cp v${readPackageVersion()}
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
iranti-cp
|
|
25
|
+
iranti-cp open [--port <n>]
|
|
26
|
+
iranti-cp start [--port <n>]
|
|
27
|
+
iranti-cp stop [--port <n>]
|
|
28
|
+
iranti-cp status [--port <n>] [--json]
|
|
29
|
+
iranti-cp uninstall
|
|
30
|
+
iranti-cp version
|
|
31
|
+
iranti-cp doctor [iranti doctor args...]
|
|
32
|
+
iranti-cp upgrade [self]
|
|
33
|
+
iranti-cp upgrade iranti [iranti upgrade args...]
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
open Open an existing Control Plane if one is running, otherwise start it in the background.
|
|
37
|
+
start Start the Control Plane in the foreground without auto-opening the browser.
|
|
38
|
+
stop Stop one or more running local Control Plane servers.
|
|
39
|
+
status Show the installed Control Plane version and any running local Control Plane servers.
|
|
40
|
+
uninstall Stop running Control Plane servers, then remove the global npm package.
|
|
41
|
+
version Print the installed iranti-control-plane version.
|
|
42
|
+
doctor Proxy to "iranti doctor".
|
|
43
|
+
upgrade Upgrade iranti-control-plane itself, or proxy to "iranti upgrade" for core Iranti.
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--port <n> Prefer a specific Control Plane port for open/start/status.
|
|
47
|
+
--json Emit machine-readable output for status.
|
|
48
|
+
-h, --help Show this help.
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
const positionals = [];
|
|
54
|
+
let port = null;
|
|
55
|
+
let json = false;
|
|
56
|
+
let help = false;
|
|
57
|
+
let seenCommand = false;
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
60
|
+
const arg = argv[index];
|
|
61
|
+
if ((arg === '--help' || arg === '-h') && !seenCommand) {
|
|
62
|
+
help = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === '--json') {
|
|
66
|
+
json = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === '--port') {
|
|
70
|
+
const value = argv[index + 1];
|
|
71
|
+
if (!value) {
|
|
72
|
+
throw new Error('--port requires a value');
|
|
73
|
+
}
|
|
74
|
+
port = value;
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith('--port=')) {
|
|
79
|
+
port = arg.slice('--port='.length);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
positionals.push(arg);
|
|
83
|
+
seenCommand = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { help, port, json, positionals };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function preferredPorts(explicitPort) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
const ordered = [];
|
|
92
|
+
const add = (value) => {
|
|
93
|
+
if (!value) return;
|
|
94
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
95
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return;
|
|
96
|
+
if (seen.has(parsed)) return;
|
|
97
|
+
seen.add(parsed);
|
|
98
|
+
ordered.push(parsed);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
add(explicitPort);
|
|
102
|
+
add(process.env.CONTROL_PLANE_PORT);
|
|
103
|
+
for (let port = 3000; port <= 3010; port += 1) add(port);
|
|
104
|
+
add(3002);
|
|
105
|
+
return ordered;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function fetchJson(url, timeoutMs = 1500) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
115
|
+
}
|
|
116
|
+
return response.json();
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function findRunningControlPlanes(explicitPort) {
|
|
123
|
+
const checks = preferredPorts(explicitPort).map(async (port) => {
|
|
124
|
+
try {
|
|
125
|
+
const health = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/health`, 2500);
|
|
126
|
+
let instances = null;
|
|
127
|
+
try {
|
|
128
|
+
const body = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/instances`, 1500);
|
|
129
|
+
instances = Array.isArray(body) ? body.length : null;
|
|
130
|
+
} catch {
|
|
131
|
+
instances = null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
port,
|
|
135
|
+
url: `http://localhost:${port}/control-plane`,
|
|
136
|
+
version: typeof health.version === 'string' ? health.version : null,
|
|
137
|
+
instances,
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const resolved = await Promise.all(checks);
|
|
145
|
+
return resolved.filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readLines(command, args) {
|
|
149
|
+
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true });
|
|
150
|
+
if (result.status !== 0) return [];
|
|
151
|
+
return String(result.stdout || '')
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findPidsForPort(port) {
|
|
158
|
+
const portText = String(port);
|
|
159
|
+
|
|
160
|
+
if (process.platform === 'win32') {
|
|
161
|
+
const lines = readLines('netstat', ['-ano', '-p', 'tcp']);
|
|
162
|
+
const pids = new Set();
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
if (!/\bLISTENING\b/i.test(line)) continue;
|
|
165
|
+
const parts = line.split(/\s+/);
|
|
166
|
+
if (parts.length < 5) continue;
|
|
167
|
+
const localAddress = parts[1];
|
|
168
|
+
const state = parts[3];
|
|
169
|
+
const pid = Number.parseInt(parts[4], 10);
|
|
170
|
+
if (!Number.isFinite(pid) || !/\bLISTENING\b/i.test(state)) continue;
|
|
171
|
+
if (localAddress.endsWith(`:${portText}`)) pids.add(pid);
|
|
172
|
+
}
|
|
173
|
+
return [...pids];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lsofPids = readLines('lsof', ['-ti', `tcp:${portText}`])
|
|
177
|
+
.map((value) => Number.parseInt(value, 10))
|
|
178
|
+
.filter((value) => Number.isFinite(value));
|
|
179
|
+
if (lsofPids.length > 0) return [...new Set(lsofPids)];
|
|
180
|
+
|
|
181
|
+
const ssLines = readLines('ss', ['-ltnp']);
|
|
182
|
+
const pids = new Set();
|
|
183
|
+
for (const line of ssLines) {
|
|
184
|
+
if (!line.includes(`:${portText}`)) continue;
|
|
185
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
186
|
+
if (!pidMatch) continue;
|
|
187
|
+
const pid = Number.parseInt(pidMatch[1], 10);
|
|
188
|
+
if (Number.isFinite(pid)) pids.add(pid);
|
|
189
|
+
}
|
|
190
|
+
return [...pids];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function openUrl(url) {
|
|
194
|
+
if (process.platform === 'win32') {
|
|
195
|
+
return spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
196
|
+
}
|
|
197
|
+
if (process.platform === 'darwin') {
|
|
198
|
+
return spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
199
|
+
}
|
|
200
|
+
return spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function spawnControlPlane({ port, openBrowser, detached }) {
|
|
204
|
+
const env = { ...process.env };
|
|
205
|
+
if (port) env.CONTROL_PLANE_PORT = String(port);
|
|
206
|
+
if (!openBrowser) env.IRANTI_CP_NO_OPEN = '1';
|
|
207
|
+
|
|
208
|
+
const child = spawn(process.execPath, [BUNDLE], {
|
|
209
|
+
env,
|
|
210
|
+
stdio: detached ? 'ignore' : 'inherit',
|
|
211
|
+
detached: Boolean(detached),
|
|
212
|
+
windowsHide: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (detached) child.unref();
|
|
216
|
+
return child;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function terminatePid(pid) {
|
|
220
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
221
|
+
throw new Error(`invalid pid "${pid}"`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (process.platform === 'win32') {
|
|
225
|
+
const code = await runChild('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
|
|
226
|
+
if (code !== 0) {
|
|
227
|
+
throw new Error(`taskkill exited with status ${code}`);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
process.kill(pid, 'SIGTERM');
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error && error.code === 'ESRCH') return;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const deadline = Date.now() + 2000;
|
|
240
|
+
while (Date.now() < deadline) {
|
|
241
|
+
try {
|
|
242
|
+
process.kill(pid, 0);
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error && error.code === 'ESRCH') return;
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
process.kill(pid, 'SIGKILL');
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error && error.code === 'ESRCH') return;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveIrantiCommand() {
|
|
259
|
+
const explicit = process.env.IRANTI_CLI_PATH || process.env.IRANTI_CP_IRANTI_CLI;
|
|
260
|
+
if (explicit && explicit.trim()) {
|
|
261
|
+
return normalizeExecutable(explicit.trim());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const locator = process.platform === 'win32' ? 'where' : 'which';
|
|
265
|
+
const located = spawnSync(locator, ['iranti'], { encoding: 'utf8', windowsHide: true });
|
|
266
|
+
if (located.status === 0) {
|
|
267
|
+
const first = String(located.stdout || '')
|
|
268
|
+
.split(/\r?\n/)
|
|
269
|
+
.map((line) => line.trim())
|
|
270
|
+
.find(Boolean);
|
|
271
|
+
if (first) return normalizeExecutable(first);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const repoLocal = [
|
|
275
|
+
path.resolve(process.cwd(), '..', 'iranti', 'bin', 'iranti.js'),
|
|
276
|
+
path.resolve(process.cwd(), '..', '..', 'iranti', 'bin', 'iranti.js'),
|
|
277
|
+
path.resolve(process.cwd(), 'node_modules', 'iranti', 'bin', 'iranti.js'),
|
|
278
|
+
].find((candidate) => fs.existsSync(candidate));
|
|
279
|
+
if (repoLocal) return { command: process.execPath, args: [repoLocal] };
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeExecutable(candidate) {
|
|
284
|
+
let normalized = path.resolve(candidate);
|
|
285
|
+
let lower = normalized.toLowerCase();
|
|
286
|
+
|
|
287
|
+
if (process.platform === 'win32' && !path.extname(lower)) {
|
|
288
|
+
for (const suffix of ['.cmd', '.exe', '.bat', '.ps1']) {
|
|
289
|
+
const sibling = `${normalized}${suffix}`;
|
|
290
|
+
if (fs.existsSync(sibling)) {
|
|
291
|
+
normalized = sibling;
|
|
292
|
+
lower = normalized.toLowerCase();
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (lower.endsWith('.js') || lower.endsWith('.cjs') || lower.endsWith('.mjs')) {
|
|
299
|
+
return { command: process.execPath, args: [normalized] };
|
|
300
|
+
}
|
|
301
|
+
if (process.platform === 'win32' && lower.endsWith('.cmd')) {
|
|
302
|
+
const cliEntry = path.join(path.dirname(normalized), 'node_modules', 'iranti', 'bin', 'iranti.js');
|
|
303
|
+
if (fs.existsSync(cliEntry)) {
|
|
304
|
+
return { command: process.execPath, args: [cliEntry] };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { command: normalized, args: [] };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function runChild(command, args, options = {}) {
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const child = spawn(command, args, {
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
windowsHide: true,
|
|
315
|
+
...options,
|
|
316
|
+
});
|
|
317
|
+
child.on('error', reject);
|
|
318
|
+
child.on('exit', (code) => resolve(code ?? 0));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function runIrantiProxy(args) {
|
|
323
|
+
const resolved = resolveIrantiCommand();
|
|
324
|
+
if (!resolved) {
|
|
325
|
+
console.error('iranti-cp: could not resolve the core iranti CLI from PATH or IRANTI_CLI_PATH.');
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
return runChild(resolved.command, [...resolved.args, ...args]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function handleStatus(port, asJson) {
|
|
332
|
+
const running = await findRunningControlPlanes(port);
|
|
333
|
+
const payload = {
|
|
334
|
+
package: 'iranti-control-plane',
|
|
335
|
+
version: readPackageVersion(),
|
|
336
|
+
running,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (asJson) {
|
|
340
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(`iranti-control-plane v${payload.version}`);
|
|
345
|
+
if (running.length === 0) {
|
|
346
|
+
console.log('No local Control Plane server detected on the default port range.');
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log(`Detected ${running.length} running Control Plane server${running.length === 1 ? '' : 's'}:`);
|
|
351
|
+
for (const server of running) {
|
|
352
|
+
const instanceText = server.instances === null ? 'instances unavailable' : `${server.instances} instance${server.instances === 1 ? '' : 's'}`;
|
|
353
|
+
console.log(`- ${server.url} (server v${server.version ?? 'unknown'}, ${instanceText})`);
|
|
354
|
+
}
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function handleOpen(port) {
|
|
359
|
+
const running = await findRunningControlPlanes(port);
|
|
360
|
+
if (running.length > 0) {
|
|
361
|
+
openUrl(running[0].url);
|
|
362
|
+
console.log(`Opened existing Control Plane at ${running[0].url}`);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
spawnControlPlane({ port, openBrowser: true, detached: true });
|
|
367
|
+
console.log('Started Control Plane in the background and asked it to open the browser.');
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function handleStart(port) {
|
|
372
|
+
const running = await findRunningControlPlanes(port);
|
|
373
|
+
if (running.length > 0) {
|
|
374
|
+
console.log(`Control Plane is already running at ${running[0].url}`);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const child = spawnControlPlane({ port, openBrowser: false, detached: false });
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
child.on('error', reject);
|
|
381
|
+
child.on('exit', (code) => resolve(code ?? 0));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function handleStop(port) {
|
|
386
|
+
const running = await findRunningControlPlanes(port);
|
|
387
|
+
if (running.length === 0) {
|
|
388
|
+
console.log(port
|
|
389
|
+
? `No local Control Plane server detected on port ${port}.`
|
|
390
|
+
: 'No local Control Plane server is running.');
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let failures = 0;
|
|
395
|
+
for (const server of running) {
|
|
396
|
+
const pids = findPidsForPort(server.port);
|
|
397
|
+
if (pids.length === 0) {
|
|
398
|
+
console.error(`Could not resolve a process ID for Control Plane on port ${server.port}.`);
|
|
399
|
+
failures += 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let stopped = false;
|
|
404
|
+
for (const pid of pids) {
|
|
405
|
+
try {
|
|
406
|
+
await terminatePid(pid);
|
|
407
|
+
console.log(`Stopped Control Plane on port ${server.port} (PID ${pid}).`);
|
|
408
|
+
stopped = true;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(`Failed to stop Control Plane on port ${server.port} (PID ${pid}): ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!stopped) failures += 1;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return failures === 0 ? 0 : 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function launchDetachedUninstall() {
|
|
421
|
+
if (process.platform === 'win32') {
|
|
422
|
+
const child = spawn('cmd', ['/d', '/s', '/c', `ping 127.0.0.1 -n 2 >nul && npm.cmd uninstall -g ${PACKAGE_NAME}`], {
|
|
423
|
+
detached: true,
|
|
424
|
+
stdio: 'ignore',
|
|
425
|
+
windowsHide: true,
|
|
426
|
+
});
|
|
427
|
+
child.unref();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const child = spawn('sh', ['-lc', `sleep 1 && npm uninstall -g ${PACKAGE_NAME}`], {
|
|
432
|
+
detached: true,
|
|
433
|
+
stdio: 'ignore',
|
|
434
|
+
});
|
|
435
|
+
child.unref();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function handleUninstall() {
|
|
439
|
+
const stopCode = await handleStop(null);
|
|
440
|
+
if (stopCode !== 0) {
|
|
441
|
+
console.error('Continuing with uninstall even though one or more Control Plane processes could not be stopped cleanly.');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
launchDetachedUninstall();
|
|
445
|
+
console.log(`Started global uninstall for ${PACKAGE_NAME}.`);
|
|
446
|
+
console.log('If your shell still sees the old command for a moment, open a new terminal after uninstall finishes.');
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function handleUpgrade(target, trailingArgs) {
|
|
451
|
+
if (!target || target === 'self') {
|
|
452
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
453
|
+
return runChild(npmCommand, ['install', '-g', `${PACKAGE_NAME}@latest`]);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (target === 'iranti') {
|
|
457
|
+
return runIrantiProxy(['upgrade', ...trailingArgs]);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.error(`iranti-cp: unknown upgrade target "${target}". Use "self" or "iranti".`);
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function main() {
|
|
465
|
+
let parsed;
|
|
466
|
+
try {
|
|
467
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error(`iranti-cp: ${error.message}`);
|
|
470
|
+
printHelp();
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (parsed.help) {
|
|
475
|
+
printHelp();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const [command = 'open', ...rest] = parsed.positionals;
|
|
480
|
+
|
|
481
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
482
|
+
console.log(readPackageVersion());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (command === 'help') {
|
|
487
|
+
printHelp();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let exitCode = 0;
|
|
492
|
+
if (command === 'open') {
|
|
493
|
+
exitCode = await handleOpen(parsed.port);
|
|
494
|
+
} else if (command === 'start') {
|
|
495
|
+
exitCode = await handleStart(parsed.port);
|
|
496
|
+
} else if (command === 'stop') {
|
|
497
|
+
exitCode = await handleStop(parsed.port);
|
|
498
|
+
} else if (command === 'status') {
|
|
499
|
+
exitCode = await handleStatus(parsed.port, parsed.json);
|
|
500
|
+
} else if (command === 'uninstall') {
|
|
501
|
+
exitCode = await handleUninstall();
|
|
502
|
+
} else if (command === 'doctor') {
|
|
503
|
+
exitCode = await runIrantiProxy(['doctor', ...rest]);
|
|
504
|
+
} else if (command === 'upgrade') {
|
|
505
|
+
exitCode = await handleUpgrade(rest[0] || 'self', rest.slice(1));
|
|
506
|
+
} else {
|
|
507
|
+
console.error(`iranti-cp: unknown command "${command}".`);
|
|
508
|
+
printHelp();
|
|
509
|
+
exitCode = 1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
process.exit(exitCode);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
main().catch((error) => {
|
|
516
|
+
console.error(`iranti-cp: ${error instanceof Error ? error.message : String(error)}`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
});
|