iranti-control-plane 0.4.2 → 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 +5 -1
- package/bin/iranti-cp.js +161 -3
- 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
|
|
@@ -23,7 +23,9 @@ Useful packaged CLI commands:
|
|
|
23
23
|
```bash
|
|
24
24
|
iranti-cp open
|
|
25
25
|
iranti-cp start --port 3010
|
|
26
|
+
iranti-cp stop --port 3010
|
|
26
27
|
iranti-cp status
|
|
28
|
+
iranti-cp uninstall
|
|
27
29
|
iranti-cp doctor --instance my_instance
|
|
28
30
|
iranti-cp upgrade self
|
|
29
31
|
iranti-cp upgrade iranti --all --dry-run
|
|
@@ -141,4 +143,6 @@ The control plane is a standalone Express server + React SPA that connects to th
|
|
|
141
143
|
See `docs/specs/control-plane-api.md` for the full API spec and `docs/prd/control-plane.md` for product requirements.
|
|
142
144
|
|
|
143
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`.
|
|
144
148
|
|
package/bin/iranti-cp.js
CHANGED
|
@@ -7,6 +7,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
7
7
|
const ROOT = path.resolve(__dirname, '..');
|
|
8
8
|
const BUNDLE = path.join(ROOT, 'dist', 'server', 'bundle.cjs');
|
|
9
9
|
const PACKAGE_JSON = path.join(ROOT, 'package.json');
|
|
10
|
+
const PACKAGE_NAME = 'iranti-control-plane';
|
|
10
11
|
|
|
11
12
|
function readPackageVersion() {
|
|
12
13
|
try {
|
|
@@ -23,7 +24,9 @@ Usage:
|
|
|
23
24
|
iranti-cp
|
|
24
25
|
iranti-cp open [--port <n>]
|
|
25
26
|
iranti-cp start [--port <n>]
|
|
27
|
+
iranti-cp stop [--port <n>]
|
|
26
28
|
iranti-cp status [--port <n>] [--json]
|
|
29
|
+
iranti-cp uninstall
|
|
27
30
|
iranti-cp version
|
|
28
31
|
iranti-cp doctor [iranti doctor args...]
|
|
29
32
|
iranti-cp upgrade [self]
|
|
@@ -32,7 +35,9 @@ Usage:
|
|
|
32
35
|
Commands:
|
|
33
36
|
open Open an existing Control Plane if one is running, otherwise start it in the background.
|
|
34
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.
|
|
35
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.
|
|
36
41
|
version Print the installed iranti-control-plane version.
|
|
37
42
|
doctor Proxy to "iranti doctor".
|
|
38
43
|
upgrade Upgrade iranti-control-plane itself, or proxy to "iranti upgrade" for core Iranti.
|
|
@@ -117,10 +122,10 @@ async function fetchJson(url, timeoutMs = 1500) {
|
|
|
117
122
|
async function findRunningControlPlanes(explicitPort) {
|
|
118
123
|
const checks = preferredPorts(explicitPort).map(async (port) => {
|
|
119
124
|
try {
|
|
120
|
-
const health = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/health`,
|
|
125
|
+
const health = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/health`, 2500);
|
|
121
126
|
let instances = null;
|
|
122
127
|
try {
|
|
123
|
-
const body = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/instances`,
|
|
128
|
+
const body = await fetchJson(`http://127.0.0.1:${port}/api/control-plane/instances`, 1500);
|
|
124
129
|
instances = Array.isArray(body) ? body.length : null;
|
|
125
130
|
} catch {
|
|
126
131
|
instances = null;
|
|
@@ -140,6 +145,51 @@ async function findRunningControlPlanes(explicitPort) {
|
|
|
140
145
|
return resolved.filter(Boolean);
|
|
141
146
|
}
|
|
142
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
|
+
|
|
143
193
|
function openUrl(url) {
|
|
144
194
|
if (process.platform === 'win32') {
|
|
145
195
|
return spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
@@ -166,6 +216,45 @@ function spawnControlPlane({ port, openBrowser, detached }) {
|
|
|
166
216
|
return child;
|
|
167
217
|
}
|
|
168
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
|
+
|
|
169
258
|
function resolveIrantiCommand() {
|
|
170
259
|
const explicit = process.env.IRANTI_CLI_PATH || process.env.IRANTI_CP_IRANTI_CLI;
|
|
171
260
|
if (explicit && explicit.trim()) {
|
|
@@ -293,10 +382,75 @@ async function handleStart(port) {
|
|
|
293
382
|
});
|
|
294
383
|
}
|
|
295
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
|
+
|
|
296
450
|
async function handleUpgrade(target, trailingArgs) {
|
|
297
451
|
if (!target || target === 'self') {
|
|
298
452
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
299
|
-
return runChild(npmCommand, ['install', '-g',
|
|
453
|
+
return runChild(npmCommand, ['install', '-g', `${PACKAGE_NAME}@latest`]);
|
|
300
454
|
}
|
|
301
455
|
|
|
302
456
|
if (target === 'iranti') {
|
|
@@ -339,8 +493,12 @@ async function main() {
|
|
|
339
493
|
exitCode = await handleOpen(parsed.port);
|
|
340
494
|
} else if (command === 'start') {
|
|
341
495
|
exitCode = await handleStart(parsed.port);
|
|
496
|
+
} else if (command === 'stop') {
|
|
497
|
+
exitCode = await handleStop(parsed.port);
|
|
342
498
|
} else if (command === 'status') {
|
|
343
499
|
exitCode = await handleStatus(parsed.port, parsed.json);
|
|
500
|
+
} else if (command === 'uninstall') {
|
|
501
|
+
exitCode = await handleUninstall();
|
|
344
502
|
} else if (command === 'doctor') {
|
|
345
503
|
exitCode = await runIrantiProxy(['doctor', ...rest]);
|
|
346
504
|
} else if (command === 'upgrade') {
|