@plumpslabs/fennec-cli 1.12.3 → 1.13.1
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/dist/index.js +723 -87
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1958,6 +1958,9 @@ function renderKV(key, value, options) {
|
|
|
1958
1958
|
const pad = " ".repeat(indent);
|
|
1959
1959
|
return `${pad}${pc.dim(key + sep)} ${value}`;
|
|
1960
1960
|
}
|
|
1961
|
+
function renderKVColor(key, value, color = pc.bold) {
|
|
1962
|
+
return ` ${pc.dim(key + ":")} ${color(value)}`;
|
|
1963
|
+
}
|
|
1961
1964
|
function createSpinner(text) {
|
|
1962
1965
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1963
1966
|
let i = 0;
|
|
@@ -2010,10 +2013,10 @@ async function selectPrompt(message, options) {
|
|
|
2010
2013
|
console.log(` ${pc.dim("0) Cancel")}
|
|
2011
2014
|
`);
|
|
2012
2015
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2013
|
-
const answer = await new Promise((
|
|
2016
|
+
const answer = await new Promise((resolve12) => {
|
|
2014
2017
|
rl.question(` ${pc.bold("Enter number")} ${pc.dim("(0-" + options.length + ")")}: `, (ans) => {
|
|
2015
2018
|
rl.close();
|
|
2016
|
-
|
|
2019
|
+
resolve12(ans.trim());
|
|
2017
2020
|
});
|
|
2018
2021
|
});
|
|
2019
2022
|
const num = parseInt(answer, 10);
|
|
@@ -2028,11 +2031,11 @@ async function selectPrompt(message, options) {
|
|
|
2028
2031
|
async function confirmPrompt(message, defaultValue = false) {
|
|
2029
2032
|
const hint = defaultValue ? pc.bold("Y") + pc.dim("/n") : pc.dim("y/") + pc.bold("N");
|
|
2030
2033
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2031
|
-
const answer = await new Promise((
|
|
2034
|
+
const answer = await new Promise((resolve12) => {
|
|
2032
2035
|
rl.question(`
|
|
2033
2036
|
${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
|
|
2034
2037
|
rl.close();
|
|
2035
|
-
|
|
2038
|
+
resolve12(ans.trim().toLowerCase());
|
|
2036
2039
|
});
|
|
2037
2040
|
});
|
|
2038
2041
|
if (!answer) return defaultValue;
|
|
@@ -2088,7 +2091,7 @@ function hexColor(color) {
|
|
|
2088
2091
|
}
|
|
2089
2092
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2090
2093
|
var fennecGold = hexColor("#FFB347");
|
|
2091
|
-
var VERSION = "1.
|
|
2094
|
+
var VERSION = "1.13.1";
|
|
2092
2095
|
var cachedBanner = null;
|
|
2093
2096
|
var cachedCompact = null;
|
|
2094
2097
|
function generateBanner() {
|
|
@@ -2681,8 +2684,13 @@ import pc4 from "picocolors";
|
|
|
2681
2684
|
async function psCommand(args2) {
|
|
2682
2685
|
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
2683
2686
|
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
2687
|
+
const jsonFlag = args2.includes("--json");
|
|
2684
2688
|
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
2685
2689
|
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
2690
|
+
if (jsonFlag) {
|
|
2691
|
+
await psJson();
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2686
2694
|
if (watchFlag && systemFlag) {
|
|
2687
2695
|
await watchSystemProcesses(sortBy, 15);
|
|
2688
2696
|
return;
|
|
@@ -2767,9 +2775,28 @@ async function psCommand(args2) {
|
|
|
2767
2775
|
console.error(renderTable(columns, rows));
|
|
2768
2776
|
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
|
|
2769
2777
|
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
|
|
2770
|
-
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec
|
|
2778
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec stop <name>")} ${pc4.dim("to pause an app.")}`);
|
|
2779
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec spawn <name>")} ${pc4.dim("to resume a paused app.")}`);
|
|
2780
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to permanently remove an app.")}`);
|
|
2771
2781
|
console.error();
|
|
2772
2782
|
}
|
|
2783
|
+
async function psJson() {
|
|
2784
|
+
const tracked = readTracked();
|
|
2785
|
+
const data = tracked.map((t) => {
|
|
2786
|
+
const running = isProcessRunning(t.pid);
|
|
2787
|
+
return {
|
|
2788
|
+
name: t.name,
|
|
2789
|
+
pid: t.pid,
|
|
2790
|
+
status: running ? "running" : "stopped",
|
|
2791
|
+
port: t.port ?? null,
|
|
2792
|
+
command: t.command,
|
|
2793
|
+
cwd: t.cwd ?? null,
|
|
2794
|
+
startedAt: t.startedAt,
|
|
2795
|
+
uptime: running ? Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3) : null
|
|
2796
|
+
};
|
|
2797
|
+
});
|
|
2798
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2799
|
+
}
|
|
2773
2800
|
async function statusCommand(_args) {
|
|
2774
2801
|
const watchFlag = _args.includes("-w") || _args.includes("--watch");
|
|
2775
2802
|
const tracked = readTracked();
|
|
@@ -2831,11 +2858,11 @@ ${renderTable(columns, rows, { compact: true })}`;
|
|
|
2831
2858
|
process.stdout.write("\x1B[J");
|
|
2832
2859
|
console.error(render());
|
|
2833
2860
|
}, 3e3);
|
|
2834
|
-
await new Promise((
|
|
2861
|
+
await new Promise((resolve12) => {
|
|
2835
2862
|
process.once("SIGINT", () => {
|
|
2836
2863
|
clearInterval(interval);
|
|
2837
2864
|
process.stdout.write("\x1B[J");
|
|
2838
|
-
|
|
2865
|
+
resolve12();
|
|
2839
2866
|
});
|
|
2840
2867
|
});
|
|
2841
2868
|
}
|
|
@@ -2876,12 +2903,13 @@ async function runCommand(args2) {
|
|
|
2876
2903
|
const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
|
|
2877
2904
|
const cwdIndex = args2.indexOf("--cwd");
|
|
2878
2905
|
const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
|
|
2906
|
+
const restartFlag = args2.includes("--restart");
|
|
2879
2907
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
2880
2908
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
2881
2909
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
2882
2910
|
const cmd = cmdParts.join(" ");
|
|
2883
2911
|
if (!cmd) {
|
|
2884
|
-
console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>]"));
|
|
2912
|
+
console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
|
|
2885
2913
|
process.exit(1);
|
|
2886
2914
|
}
|
|
2887
2915
|
const appName = name ?? cmdParts[0] ?? "app";
|
|
@@ -2904,20 +2932,21 @@ async function runCommand(args2) {
|
|
|
2904
2932
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
2905
2933
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
2906
2934
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
2935
|
+
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc5.green("enabled"))}`);
|
|
2907
2936
|
console.error(` ${divider()}`);
|
|
2908
2937
|
try {
|
|
2909
|
-
|
|
2938
|
+
let currentChild = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
2910
2939
|
cwd,
|
|
2911
2940
|
env: { ...process.env },
|
|
2912
2941
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2913
2942
|
detached: true
|
|
2914
2943
|
});
|
|
2915
|
-
const pid =
|
|
2944
|
+
const pid = currentChild.pid ?? 0;
|
|
2916
2945
|
rotateLogFile(logFilePath);
|
|
2917
2946
|
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
2918
|
-
if (
|
|
2919
|
-
if (
|
|
2920
|
-
|
|
2947
|
+
if (currentChild.stdout) currentChild.stdout.pipe(logStream);
|
|
2948
|
+
if (currentChild.stderr) currentChild.stderr.pipe(logStream);
|
|
2949
|
+
currentChild.unref();
|
|
2921
2950
|
addTracked({
|
|
2922
2951
|
name: appName,
|
|
2923
2952
|
pid,
|
|
@@ -2927,7 +2956,44 @@ async function runCommand(args2) {
|
|
|
2927
2956
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2928
2957
|
});
|
|
2929
2958
|
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
|
|
2930
|
-
|
|
2959
|
+
if (restartFlag) {
|
|
2960
|
+
let setupRestartWatch2 = function(c) {
|
|
2961
|
+
c.on("exit", (code, signal) => {
|
|
2962
|
+
if (code !== 0 && signal !== "SIGTERM" && signal !== "SIGKILL") {
|
|
2963
|
+
console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`exited with code ${code}, restarting...`)}`);
|
|
2964
|
+
const restarted = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
2965
|
+
cwd,
|
|
2966
|
+
env: { ...process.env },
|
|
2967
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2968
|
+
detached: true
|
|
2969
|
+
});
|
|
2970
|
+
const newPid = restarted.pid ?? 0;
|
|
2971
|
+
rotateLogFile(logFilePath);
|
|
2972
|
+
const rs = createWriteStream(logFilePath, { flags: "a" });
|
|
2973
|
+
if (restarted.stdout) restarted.stdout.pipe(rs);
|
|
2974
|
+
if (restarted.stderr) restarted.stderr.pipe(rs);
|
|
2975
|
+
restarted.unref();
|
|
2976
|
+
addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2977
|
+
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
|
|
2978
|
+
currentChild = restarted;
|
|
2979
|
+
setupRestartWatch2(restarted);
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
};
|
|
2983
|
+
var setupRestartWatch = setupRestartWatch2;
|
|
2984
|
+
console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
|
|
2985
|
+
`);
|
|
2986
|
+
setupRestartWatch2(currentChild);
|
|
2987
|
+
await new Promise((resolve12) => {
|
|
2988
|
+
process.once("SIGINT", () => {
|
|
2989
|
+
currentChild.kill("SIGTERM");
|
|
2990
|
+
resolve12();
|
|
2991
|
+
});
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
if (!restartFlag) {
|
|
2995
|
+
await psCommand([]);
|
|
2996
|
+
}
|
|
2931
2997
|
} catch (error) {
|
|
2932
2998
|
console.error(renderError(`Failed to start ${appName}`, String(error)));
|
|
2933
2999
|
process.exit(1);
|
|
@@ -3103,12 +3169,295 @@ async function killAll(signal) {
|
|
|
3103
3169
|
if (failed > 0) console.error(` ${pc6.yellow(`${failed} process(es) could not be killed`)} ${pc6.dim("(try with sudo)")}`);
|
|
3104
3170
|
}
|
|
3105
3171
|
|
|
3106
|
-
// src/commands/
|
|
3172
|
+
// src/commands/stop.ts
|
|
3107
3173
|
import pc7 from "picocolors";
|
|
3108
|
-
|
|
3174
|
+
async function stopCommand(args2) {
|
|
3175
|
+
const stopAll = args2.includes("--all") || args2.includes("-a");
|
|
3176
|
+
if (stopAll) {
|
|
3177
|
+
await stopAllTracked();
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const rawTarget = args2[0];
|
|
3181
|
+
if (!rawTarget) {
|
|
3182
|
+
console.error(renderError("Missing name", "Usage: fennec stop <name|--all>"));
|
|
3183
|
+
process.exit(1);
|
|
3184
|
+
}
|
|
3185
|
+
const tracked = readTracked();
|
|
3186
|
+
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3187
|
+
if (!trackedMatch) {
|
|
3188
|
+
console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc7.cyan("fennec kill <pid>")} to kill a system process.`));
|
|
3189
|
+
process.exit(1);
|
|
3190
|
+
}
|
|
3191
|
+
if (!isProcessRunning(trackedMatch.pid)) {
|
|
3192
|
+
console.error(`
|
|
3193
|
+
${pc7.yellow("\u26A0")} ${pc7.bold(rawTarget)} ${pc7.dim("is already stopped")}`);
|
|
3194
|
+
console.error();
|
|
3195
|
+
process.exit(0);
|
|
3196
|
+
}
|
|
3197
|
+
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
3198
|
+
const confirmed = await confirmPrompt(`Stop ${pc7.bold(displayName)}? ${pc7.dim("It can be re-spawned later via fennec spawn")}`, false);
|
|
3199
|
+
if (!confirmed) {
|
|
3200
|
+
console.error(` ${pc7.dim("Cancelled")}`);
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
3204
|
+
}
|
|
3205
|
+
async function stopAllTracked() {
|
|
3206
|
+
const tracked = readTracked();
|
|
3207
|
+
const running = tracked.filter((t) => isProcessRunning(t.pid));
|
|
3208
|
+
if (running.length === 0) {
|
|
3209
|
+
console.error(`
|
|
3210
|
+
${pc7.dim("No running tracked processes to stop.")}
|
|
3211
|
+
`);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
console.error(`
|
|
3215
|
+
${pc7.yellow("\u26A0")} ${pc7.bold(`Stop all ${running.length} running process(es)?`)} ${pc7.dim("(They can be re-spawned later)")}
|
|
3216
|
+
`);
|
|
3217
|
+
for (const t of running) {
|
|
3218
|
+
console.error(` ${pc7.green("\u25CF")} ${pc7.bold(t.name)} ${pc7.dim(`(PID ${t.pid})`)}`);
|
|
3219
|
+
}
|
|
3220
|
+
console.error();
|
|
3221
|
+
const confirmed = await confirmPrompt("Stop all?", false);
|
|
3222
|
+
if (!confirmed) {
|
|
3223
|
+
console.error(` ${pc7.dim("Cancelled")}
|
|
3224
|
+
`);
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
const spinner = createSpinner(`Stopping ${running.length} process(es)...`);
|
|
3228
|
+
let stopped = 0;
|
|
3229
|
+
let failed = 0;
|
|
3230
|
+
for (const t of running) {
|
|
3231
|
+
if (killProcess(t.pid, "SIGTERM")) {
|
|
3232
|
+
stopped++;
|
|
3233
|
+
} else {
|
|
3234
|
+
failed++;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3238
|
+
spinner.stop();
|
|
3239
|
+
process.stdout.write("\r\x1B[K");
|
|
3240
|
+
if (stopped > 0) {
|
|
3241
|
+
console.error(` ${pc7.green("\u2713")} ${pc7.bold(`Stopped ${stopped} process(es)`)} ${pc7.dim("(kept in tracked.json)")}`);
|
|
3242
|
+
}
|
|
3243
|
+
if (failed > 0) {
|
|
3244
|
+
console.error(` ${pc7.red("\u2717")} ${pc7.bold(`${failed} process(es) failed to stop`)}`);
|
|
3245
|
+
}
|
|
3246
|
+
console.error();
|
|
3247
|
+
await psCommand([]);
|
|
3248
|
+
}
|
|
3249
|
+
async function stopSingleProcess(pid, name) {
|
|
3250
|
+
const displayName = `${name} (PID ${pid})`;
|
|
3251
|
+
const spinner = createSpinner(`Stopping ${displayName}...`);
|
|
3252
|
+
const success = killProcess(pid, "SIGTERM");
|
|
3253
|
+
if (success) {
|
|
3254
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3255
|
+
const stillRunning = isProcessRunning(pid);
|
|
3256
|
+
if (stillRunning) {
|
|
3257
|
+
spinner.warn(`${displayName} did not respond to SIGTERM`);
|
|
3258
|
+
const forceKill = await confirmPrompt(`Send ${pc7.red("SIGKILL")} to force stop?`, true);
|
|
3259
|
+
if (forceKill) {
|
|
3260
|
+
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3261
|
+
killProcess(pid, "SIGKILL");
|
|
3262
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3263
|
+
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3264
|
+
} else {
|
|
3265
|
+
console.error(` ${pc7.yellow("\u26A0")} ${displayName} ${pc7.dim("may still be running")}`);
|
|
3266
|
+
}
|
|
3267
|
+
} else {
|
|
3268
|
+
spinner.succeed(`${displayName} stopped`);
|
|
3269
|
+
}
|
|
3270
|
+
console.error();
|
|
3271
|
+
await psCommand([]);
|
|
3272
|
+
} else {
|
|
3273
|
+
spinner.fail(`Failed to stop ${displayName}`);
|
|
3274
|
+
console.error(renderError("Permission denied", "Try running with sudo or use fennec kill."));
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// src/commands/spawn.ts
|
|
3279
|
+
import pc8 from "picocolors";
|
|
3280
|
+
import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3 } from "fs";
|
|
3109
3281
|
import { resolve as resolve4 } from "path";
|
|
3110
3282
|
import { homedir as homedir3 } from "os";
|
|
3111
3283
|
import { spawn as spawn2 } from "child_process";
|
|
3284
|
+
async function spawnCommand(args2) {
|
|
3285
|
+
const spawnAll = args2.includes("--all") || args2.includes("-a");
|
|
3286
|
+
const name = args2[0];
|
|
3287
|
+
if (spawnAll) {
|
|
3288
|
+
await spawnAllStopped();
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
if (!name) {
|
|
3292
|
+
await spawnList();
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
const tracked = readTracked();
|
|
3296
|
+
const match = tracked.find((t) => t.name === name);
|
|
3297
|
+
if (!match) {
|
|
3298
|
+
console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc8.cyan("fennec spawn")} to see available processes, or ${pc8.cyan("fennec start <command> --name <name>")} to create a new one.`));
|
|
3299
|
+
process.exit(1);
|
|
3300
|
+
}
|
|
3301
|
+
if (isProcessRunning(match.pid)) {
|
|
3302
|
+
console.error(`
|
|
3303
|
+
${pc8.yellow("\u26A0")} ${pc8.bold(name)} ${pc8.dim("is already running (PID:")} ${match.pid}${pc8.dim(")")}`);
|
|
3304
|
+
console.error(` ${pc8.dim("Use")} ${pc8.cyan(`fennec stop ${name}`)} ${pc8.dim("to stop it first.")}`);
|
|
3305
|
+
console.error();
|
|
3306
|
+
process.exit(0);
|
|
3307
|
+
}
|
|
3308
|
+
if (!match.command) {
|
|
3309
|
+
console.error(renderError("No command saved", `"${name}" has no saved command and cannot be re-spawned.`));
|
|
3310
|
+
process.exit(1);
|
|
3311
|
+
}
|
|
3312
|
+
await respawnProcess(match);
|
|
3313
|
+
}
|
|
3314
|
+
async function spawnAllStopped() {
|
|
3315
|
+
const tracked = readTracked();
|
|
3316
|
+
const toSpawn = tracked.filter((t) => !isProcessRunning(t.pid) && t.command);
|
|
3317
|
+
if (toSpawn.length === 0) {
|
|
3318
|
+
console.error(`
|
|
3319
|
+
${pc8.dim("No stopped processes with saved commands to spawn.")}
|
|
3320
|
+
`);
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
console.error(`
|
|
3324
|
+
${symbols.fox} ${pc8.bold(`Spawning ${toSpawn.length} process(es)...`)}
|
|
3325
|
+
`);
|
|
3326
|
+
for (const t of toSpawn) {
|
|
3327
|
+
console.error(` ${pc8.dim("\xB7")} ${pc8.bold(t.name)} ${pc8.dim(`(${t.command})`)}`);
|
|
3328
|
+
}
|
|
3329
|
+
console.error();
|
|
3330
|
+
const spinner = createSpinner("Spawning...");
|
|
3331
|
+
let spawned = 0;
|
|
3332
|
+
let failed = 0;
|
|
3333
|
+
for (const proc of toSpawn) {
|
|
3334
|
+
try {
|
|
3335
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
3336
|
+
const logDir = resolve4(homedir3(), ".fennec", "logs");
|
|
3337
|
+
mkdirSync3(logDir, { recursive: true });
|
|
3338
|
+
const logFilePath = resolve4(logDir, `${proc.name}.log`);
|
|
3339
|
+
const child = spawn2(cmdParts[0], cmdParts.slice(1), {
|
|
3340
|
+
cwd: proc.cwd,
|
|
3341
|
+
env: { ...process.env },
|
|
3342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3343
|
+
detached: true
|
|
3344
|
+
});
|
|
3345
|
+
const pid = child.pid ?? 0;
|
|
3346
|
+
rotateLogFile(logFilePath);
|
|
3347
|
+
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3348
|
+
if (child.stdout) child.stdout.pipe(logStream);
|
|
3349
|
+
if (child.stderr) child.stderr.pipe(logStream);
|
|
3350
|
+
child.unref();
|
|
3351
|
+
addTracked({
|
|
3352
|
+
name: proc.name,
|
|
3353
|
+
pid,
|
|
3354
|
+
command: proc.command,
|
|
3355
|
+
port: proc.port,
|
|
3356
|
+
cwd: proc.cwd,
|
|
3357
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3358
|
+
});
|
|
3359
|
+
spawned++;
|
|
3360
|
+
} catch {
|
|
3361
|
+
failed++;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
spinner.stop();
|
|
3365
|
+
process.stdout.write("\r\x1B[K");
|
|
3366
|
+
if (spawned > 0) {
|
|
3367
|
+
console.error(` ${pc8.green("\u2713")} ${pc8.bold(`Spawned ${spawned} process(es)`)}`);
|
|
3368
|
+
}
|
|
3369
|
+
if (failed > 0) {
|
|
3370
|
+
console.error(` ${pc8.red("\u2717")} ${pc8.bold(`${failed} process(es) failed to spawn`)}`);
|
|
3371
|
+
}
|
|
3372
|
+
console.error();
|
|
3373
|
+
await psCommand([]);
|
|
3374
|
+
}
|
|
3375
|
+
async function spawnList() {
|
|
3376
|
+
const tracked = readTracked();
|
|
3377
|
+
if (tracked.length === 0) {
|
|
3378
|
+
console.error(`
|
|
3379
|
+
${pc8.dim("No tracked processes found.")}`);
|
|
3380
|
+
console.error(` ${pc8.dim("Start an app first:")} ${pc8.cyan("fennec start <command> --name <name>")}`);
|
|
3381
|
+
console.error();
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const stopped = tracked.filter((t) => !isProcessRunning(t.pid));
|
|
3385
|
+
const running = tracked.filter((t) => isProcessRunning(t.pid));
|
|
3386
|
+
if (stopped.length === 0) {
|
|
3387
|
+
console.error(`
|
|
3388
|
+
${pc8.dim("All tracked processes are running. Nothing to spawn.")}`);
|
|
3389
|
+
console.error();
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
console.error(`
|
|
3393
|
+
${symbols.fox} ${pc8.bold("Available to spawn")} ${pc8.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
|
|
3394
|
+
if (running.length > 0) {
|
|
3395
|
+
console.error(` ${pc8.dim("Running (use stop first):")}`);
|
|
3396
|
+
for (const r of running) {
|
|
3397
|
+
console.error(` ${pc8.green("\u25CF")} ${pc8.bold(r.name)} ${pc8.dim(`(PID ${r.pid})`)}`);
|
|
3398
|
+
}
|
|
3399
|
+
console.error();
|
|
3400
|
+
}
|
|
3401
|
+
const selected = await selectPrompt("Select a process to spawn:", stopped.map((t) => ({
|
|
3402
|
+
value: t.name,
|
|
3403
|
+
label: t.name,
|
|
3404
|
+
description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
|
|
3405
|
+
})));
|
|
3406
|
+
if (selected === null) {
|
|
3407
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
const match = stopped.find((t) => t.name === selected);
|
|
3411
|
+
if (match) {
|
|
3412
|
+
await respawnProcess(match);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
async function respawnProcess(proc) {
|
|
3416
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
3417
|
+
const logDir = resolve4(homedir3(), ".fennec", "logs");
|
|
3418
|
+
mkdirSync3(logDir, { recursive: true });
|
|
3419
|
+
const logFilePath = resolve4(logDir, `${proc.name}.log`);
|
|
3420
|
+
console.error(`
|
|
3421
|
+
${symbols.fox} ${pc8.bold("Spawning")} ${renderAppName(proc.name)} ${pc8.dim("(from saved config)")}
|
|
3422
|
+
`);
|
|
3423
|
+
console.error(` ${renderKV("Command", proc.command)}`);
|
|
3424
|
+
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
3425
|
+
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
3426
|
+
try {
|
|
3427
|
+
const child = spawn2(cmdParts[0], cmdParts.slice(1), {
|
|
3428
|
+
cwd: proc.cwd,
|
|
3429
|
+
env: { ...process.env },
|
|
3430
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3431
|
+
detached: true
|
|
3432
|
+
});
|
|
3433
|
+
const pid = child.pid ?? 0;
|
|
3434
|
+
rotateLogFile(logFilePath);
|
|
3435
|
+
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3436
|
+
if (child.stdout) child.stdout.pipe(logStream);
|
|
3437
|
+
if (child.stderr) child.stderr.pipe(logStream);
|
|
3438
|
+
child.unref();
|
|
3439
|
+
addTracked({
|
|
3440
|
+
name: proc.name,
|
|
3441
|
+
pid,
|
|
3442
|
+
command: proc.command,
|
|
3443
|
+
port: proc.port,
|
|
3444
|
+
cwd: proc.cwd,
|
|
3445
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3446
|
+
});
|
|
3447
|
+
console.error(` ${pc8.green("\u2713")} ${pc8.bold(proc.name)} ${pc8.dim(`spawned (PID: ${pid})`)}`);
|
|
3448
|
+
await psCommand([]);
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// src/commands/restart.ts
|
|
3456
|
+
import pc9 from "picocolors";
|
|
3457
|
+
import { mkdirSync as mkdirSync4, createWriteStream as createWriteStream3 } from "fs";
|
|
3458
|
+
import { resolve as resolve5 } from "path";
|
|
3459
|
+
import { homedir as homedir4 } from "os";
|
|
3460
|
+
import { spawn as spawn3 } from "child_process";
|
|
3112
3461
|
async function restartCommand(args2) {
|
|
3113
3462
|
const name = args2[0];
|
|
3114
3463
|
if (!name) {
|
|
@@ -3133,9 +3482,9 @@ async function restartCommand(args2) {
|
|
|
3133
3482
|
targetPid = processes[0].pid;
|
|
3134
3483
|
}
|
|
3135
3484
|
}
|
|
3136
|
-
const confirmed = await confirmPrompt(`Restart ${
|
|
3485
|
+
const confirmed = await confirmPrompt(`Restart ${pc9.bold(`${name} (PID ${targetPid})`)}?`, false);
|
|
3137
3486
|
if (!confirmed) {
|
|
3138
|
-
console.error(` ${
|
|
3487
|
+
console.error(` ${pc9.dim("Cancelled")}`);
|
|
3139
3488
|
return;
|
|
3140
3489
|
}
|
|
3141
3490
|
const spinner = createSpinner(`Stopping PID ${targetPid}...`);
|
|
@@ -3155,17 +3504,17 @@ async function restartCommand(args2) {
|
|
|
3155
3504
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
3156
3505
|
try {
|
|
3157
3506
|
const cmdParts = trackedEntry.command.split(" ");
|
|
3158
|
-
const logDir =
|
|
3159
|
-
|
|
3160
|
-
const logFilePath =
|
|
3161
|
-
const child =
|
|
3507
|
+
const logDir = resolve5(homedir4(), ".fennec", "logs");
|
|
3508
|
+
mkdirSync4(logDir, { recursive: true });
|
|
3509
|
+
const logFilePath = resolve5(logDir, `${trackedEntry.name}.log`);
|
|
3510
|
+
const child = spawn3(cmdParts[0], cmdParts.slice(1), {
|
|
3162
3511
|
cwd: trackedEntry.cwd,
|
|
3163
3512
|
env: { ...process.env },
|
|
3164
3513
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3165
3514
|
detached: true
|
|
3166
3515
|
});
|
|
3167
3516
|
rotateLogFile(logFilePath);
|
|
3168
|
-
const logStream =
|
|
3517
|
+
const logStream = createWriteStream3(logFilePath, { flags: "a" });
|
|
3169
3518
|
if (child.stdout) child.stdout.pipe(logStream);
|
|
3170
3519
|
if (child.stderr) child.stderr.pipe(logStream);
|
|
3171
3520
|
child.unref();
|
|
@@ -3181,35 +3530,41 @@ async function restartCommand(args2) {
|
|
|
3181
3530
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
3182
3531
|
} catch (error) {
|
|
3183
3532
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
3184
|
-
console.error(` ${
|
|
3533
|
+
console.error(` ${pc9.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
|
|
3185
3534
|
}
|
|
3186
3535
|
} else {
|
|
3187
|
-
console.error(` ${
|
|
3536
|
+
console.error(` ${pc9.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
|
|
3188
3537
|
}
|
|
3189
3538
|
}
|
|
3190
3539
|
|
|
3191
3540
|
// src/commands/log.ts
|
|
3192
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
3193
|
-
import { resolve as
|
|
3194
|
-
import { homedir as
|
|
3195
|
-
import { spawn as
|
|
3196
|
-
import
|
|
3541
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
3542
|
+
import { resolve as resolve6 } from "path";
|
|
3543
|
+
import { homedir as homedir5 } from "os";
|
|
3544
|
+
import { spawn as spawn4, execSync as execSync2 } from "child_process";
|
|
3545
|
+
import pc10 from "picocolors";
|
|
3197
3546
|
async function logCommand(args2) {
|
|
3198
3547
|
const target = args2[0];
|
|
3199
3548
|
if (!target) {
|
|
3200
|
-
console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f]"));
|
|
3549
|
+
console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f] [--clear] [--level error|warn|info|debug]"));
|
|
3201
3550
|
process.exit(1);
|
|
3202
3551
|
}
|
|
3552
|
+
const clearFlag = args2.includes("--clear");
|
|
3203
3553
|
const linesIndex = args2.indexOf("--lines");
|
|
3204
3554
|
const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
|
|
3205
3555
|
const followFlag = args2.includes("-f") || args2.includes("--follow");
|
|
3556
|
+
const levelFilter = args2.includes("--level") ? args2[args2.indexOf("--level") + 1]?.toLowerCase() : void 0;
|
|
3557
|
+
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
3558
|
+
console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
}
|
|
3206
3561
|
let logFilePath = null;
|
|
3207
3562
|
let displayName = target;
|
|
3208
3563
|
const tracked = readTracked();
|
|
3209
3564
|
const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
|
|
3210
3565
|
if (trackedMatch) {
|
|
3211
3566
|
displayName = trackedMatch.name;
|
|
3212
|
-
logFilePath =
|
|
3567
|
+
logFilePath = resolve6(homedir5(), ".fennec", "logs", `${trackedMatch.name}.log`);
|
|
3213
3568
|
} else {
|
|
3214
3569
|
const pid = parseInt(target, 10);
|
|
3215
3570
|
if (!isNaN(pid) && String(pid) === target) {
|
|
@@ -3217,6 +3572,24 @@ async function logCommand(args2) {
|
|
|
3217
3572
|
if (existsSync4(procPath)) logFilePath = procPath;
|
|
3218
3573
|
}
|
|
3219
3574
|
}
|
|
3575
|
+
if (clearFlag) {
|
|
3576
|
+
if (!logFilePath || !existsSync4(logFilePath)) {
|
|
3577
|
+
console.error(`
|
|
3578
|
+
${pc10.yellow("\u26A0")} ${pc10.dim("No log file found for")} ${pc10.bold(displayName)}
|
|
3579
|
+
`);
|
|
3580
|
+
process.exit(0);
|
|
3581
|
+
}
|
|
3582
|
+
try {
|
|
3583
|
+
unlinkSync(logFilePath);
|
|
3584
|
+
console.error(`
|
|
3585
|
+
${pc10.green("\u2713")} ${pc10.bold("Log cleared")} ${pc10.dim(`\u2014 ${logFilePath}`)}
|
|
3586
|
+
`);
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
console.error(renderError("Failed to clear log", String(err)));
|
|
3589
|
+
process.exit(1);
|
|
3590
|
+
}
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3220
3593
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
3221
3594
|
try {
|
|
3222
3595
|
let logLines = [];
|
|
@@ -3236,31 +3609,55 @@ async function logCommand(args2) {
|
|
|
3236
3609
|
logLines = ["(no logs available)"];
|
|
3237
3610
|
}
|
|
3238
3611
|
}
|
|
3612
|
+
if (levelFilter) {
|
|
3613
|
+
const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
|
|
3614
|
+
logLines = logLines.filter((line) => levelRegex.test(line));
|
|
3615
|
+
}
|
|
3239
3616
|
spinner.stop();
|
|
3240
3617
|
process.stdout.write("\r\x1B[K");
|
|
3241
3618
|
console.error(`
|
|
3242
|
-
${symbols.fox} ${
|
|
3243
|
-
`);
|
|
3619
|
+
${symbols.fox} ${pc10.bold("Logs")} ${renderAppName(displayName)} ${pc10.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})`)}`);
|
|
3244
3620
|
const sliced = logLines.slice(-lineCount);
|
|
3245
3621
|
for (const line of sliced) {
|
|
3246
3622
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
else
|
|
3623
|
+
const prefix = followFlag ? `${timestamp()} ` : "";
|
|
3624
|
+
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
|
|
3625
|
+
console.error(` ${pc10.red(prefix + display)}`);
|
|
3626
|
+
} else if (line.toLowerCase().includes("warn")) {
|
|
3627
|
+
console.error(` ${pc10.yellow(prefix + display)}`);
|
|
3628
|
+
} else if (line.toLowerCase().includes("info") || line.includes("[")) {
|
|
3629
|
+
console.error(` ${pc10.cyan(prefix + display)}`);
|
|
3630
|
+
} else {
|
|
3631
|
+
console.error(` ${prefix}${display}`);
|
|
3632
|
+
}
|
|
3251
3633
|
}
|
|
3252
3634
|
if (followFlag && logFilePath) {
|
|
3253
|
-
|
|
3254
|
-
|
|
3635
|
+
const tailArgs = ["-n", "0", "-f"];
|
|
3636
|
+
if (levelFilter) {
|
|
3637
|
+
const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3638
|
+
const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
|
|
3639
|
+
if (tail.stdout) tail.stdout.pipe(grep.stdin);
|
|
3640
|
+
await new Promise((resolve12) => {
|
|
3641
|
+
tail.on("exit", () => resolve12());
|
|
3642
|
+
process.once("SIGINT", () => {
|
|
3643
|
+
tail.kill("SIGTERM");
|
|
3644
|
+
grep.kill("SIGTERM");
|
|
3645
|
+
resolve12();
|
|
3646
|
+
});
|
|
3647
|
+
});
|
|
3648
|
+
} else {
|
|
3649
|
+
console.error(`
|
|
3650
|
+
${pc10.dim("Following... (Ctrl+C to stop)")}
|
|
3255
3651
|
`);
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3652
|
+
const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
|
|
3653
|
+
await new Promise((resolve12) => {
|
|
3654
|
+
tail.on("exit", () => resolve12());
|
|
3655
|
+
process.once("SIGINT", () => {
|
|
3656
|
+
tail.kill("SIGTERM");
|
|
3657
|
+
resolve12();
|
|
3658
|
+
});
|
|
3262
3659
|
});
|
|
3263
|
-
}
|
|
3660
|
+
}
|
|
3264
3661
|
}
|
|
3265
3662
|
console.error();
|
|
3266
3663
|
} catch (error) {
|
|
@@ -3271,7 +3668,7 @@ async function logCommand(args2) {
|
|
|
3271
3668
|
|
|
3272
3669
|
// src/commands/attach.ts
|
|
3273
3670
|
import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
|
|
3274
|
-
import
|
|
3671
|
+
import pc11 from "picocolors";
|
|
3275
3672
|
async function attachCommand(args2) {
|
|
3276
3673
|
const port = parseInt(args2[0], 10);
|
|
3277
3674
|
if (isNaN(port)) {
|
|
@@ -3289,7 +3686,7 @@ async function attachCommand(args2) {
|
|
|
3289
3686
|
spinner.succeed(`Attached to :${port}`);
|
|
3290
3687
|
console.error(` ${renderKV("Name", renderAppName(name))}`);
|
|
3291
3688
|
console.error(` ${renderKV("PID", String(info.pid))}`);
|
|
3292
|
-
console.error(` ${renderKV("Command", info.command ||
|
|
3689
|
+
console.error(` ${renderKV("Command", info.command || pc11.dim("unknown"))}`);
|
|
3293
3690
|
} else {
|
|
3294
3691
|
spinner.fail(`No process found on port ${port}`);
|
|
3295
3692
|
process.exit(1);
|
|
@@ -3303,35 +3700,35 @@ async function attachCommand(args2) {
|
|
|
3303
3700
|
|
|
3304
3701
|
// src/commands/sessions.ts
|
|
3305
3702
|
import { SessionStore } from "@plumpslabs/fennec-core";
|
|
3306
|
-
import
|
|
3703
|
+
import pc12 from "picocolors";
|
|
3307
3704
|
async function sessionsCommand() {
|
|
3308
3705
|
const store = new SessionStore("./.fennec/sessions");
|
|
3309
3706
|
const sessions = store.list();
|
|
3310
3707
|
if (sessions.length === 0) {
|
|
3311
3708
|
console.error(`
|
|
3312
|
-
${
|
|
3709
|
+
${pc12.dim("No saved sessions found.")}
|
|
3313
3710
|
`);
|
|
3314
3711
|
return;
|
|
3315
3712
|
}
|
|
3316
3713
|
const columns = [
|
|
3317
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
3714
|
+
{ key: "name", label: "Name", format: (v) => pc12.bold(String(v)) },
|
|
3318
3715
|
{ key: "origin", label: "Origin" },
|
|
3319
|
-
{ key: "savedAt", label: "Saved", format: (v) =>
|
|
3716
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc12.dim(String(v)) }
|
|
3320
3717
|
];
|
|
3321
3718
|
const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
|
|
3322
3719
|
console.error(`
|
|
3323
|
-
${symbols.fox} ${
|
|
3720
|
+
${symbols.fox} ${pc12.bold("Saved Sessions")}
|
|
3324
3721
|
`);
|
|
3325
3722
|
console.error(renderTable(columns, rows));
|
|
3326
|
-
console.error(` ${
|
|
3723
|
+
console.error(` ${pc12.dim(`${sessions.length} session(s)`)}
|
|
3327
3724
|
`);
|
|
3328
3725
|
}
|
|
3329
3726
|
|
|
3330
3727
|
// src/commands/setup.ts
|
|
3331
|
-
import
|
|
3728
|
+
import pc13 from "picocolors";
|
|
3332
3729
|
async function setupCommand() {
|
|
3333
3730
|
console.error(`
|
|
3334
|
-
${symbols.fox} ${
|
|
3731
|
+
${symbols.fox} ${pc13.bold("Fennec Setup")}
|
|
3335
3732
|
`);
|
|
3336
3733
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
3337
3734
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -3340,12 +3737,12 @@ async function setupCommand() {
|
|
|
3340
3737
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
3341
3738
|
]);
|
|
3342
3739
|
if (!mcpClient) {
|
|
3343
|
-
console.error(` ${
|
|
3740
|
+
console.error(` ${pc13.dim("Setup cancelled.")}
|
|
3344
3741
|
`);
|
|
3345
3742
|
return;
|
|
3346
3743
|
}
|
|
3347
3744
|
console.error(`
|
|
3348
|
-
${
|
|
3745
|
+
${pc13.green("\u2713")} Selected: ${pc13.bold(mcpClient)}
|
|
3349
3746
|
`);
|
|
3350
3747
|
const configSnippet = `{
|
|
3351
3748
|
"mcpServers": {
|
|
@@ -3355,24 +3752,24 @@ async function setupCommand() {
|
|
|
3355
3752
|
}
|
|
3356
3753
|
}
|
|
3357
3754
|
}`;
|
|
3358
|
-
console.error(` ${
|
|
3755
|
+
console.error(` ${pc13.bold("Add this to your MCP client config:")}
|
|
3359
3756
|
`);
|
|
3360
|
-
console.error(` ${
|
|
3757
|
+
console.error(` ${pc13.dim("```")}`);
|
|
3361
3758
|
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
3362
|
-
console.error(` ${
|
|
3759
|
+
console.error(` ${pc13.dim("```")}
|
|
3363
3760
|
`);
|
|
3364
|
-
console.error(` ${renderSuccess("Setup complete!")} ${
|
|
3761
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc13.dim("Run")} ${renderCommand("fennec start")} ${pc13.dim("to begin.")}
|
|
3365
3762
|
`);
|
|
3366
3763
|
}
|
|
3367
3764
|
|
|
3368
3765
|
// src/commands/management.ts
|
|
3369
3766
|
import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
3370
|
-
import { resolve as
|
|
3767
|
+
import { resolve as resolve7 } from "path";
|
|
3371
3768
|
import { execSync as execSync3 } from "child_process";
|
|
3372
|
-
import
|
|
3769
|
+
import pc14 from "picocolors";
|
|
3373
3770
|
async function installBrowsersCommand() {
|
|
3374
3771
|
console.error(`
|
|
3375
|
-
${
|
|
3772
|
+
${pc14.bold("Installing Browser Engines")}
|
|
3376
3773
|
`);
|
|
3377
3774
|
const spinner = createSpinner("Installing Chromium...");
|
|
3378
3775
|
try {
|
|
@@ -3380,21 +3777,21 @@ async function installBrowsersCommand() {
|
|
|
3380
3777
|
spinner.succeed("Chromium installed successfully");
|
|
3381
3778
|
} catch {
|
|
3382
3779
|
spinner.fail("Failed to install Chromium");
|
|
3383
|
-
console.error(` ${
|
|
3780
|
+
console.error(` ${pc14.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
3384
3781
|
}
|
|
3385
3782
|
console.error(`
|
|
3386
|
-
${
|
|
3783
|
+
${pc14.green("\u2713")} Browser installation complete.
|
|
3387
3784
|
`);
|
|
3388
3785
|
}
|
|
3389
3786
|
async function initCommand() {
|
|
3390
3787
|
console.error(`
|
|
3391
|
-
${
|
|
3788
|
+
${pc14.bold("Initialize Fennec Configuration")}
|
|
3392
3789
|
`);
|
|
3393
|
-
const configFile =
|
|
3790
|
+
const configFile = resolve7("./fennec.config.yaml");
|
|
3394
3791
|
if (existsSync5(configFile)) {
|
|
3395
|
-
const overwrite = await confirmPrompt(`${
|
|
3792
|
+
const overwrite = await confirmPrompt(`${pc14.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
|
|
3396
3793
|
if (!overwrite) {
|
|
3397
|
-
console.error(` ${
|
|
3794
|
+
console.error(` ${pc14.dim("Cancelled.")}
|
|
3398
3795
|
`);
|
|
3399
3796
|
return;
|
|
3400
3797
|
}
|
|
@@ -3474,21 +3871,21 @@ logging:
|
|
|
3474
3871
|
file: null
|
|
3475
3872
|
`;
|
|
3476
3873
|
writeFileSync2(configFile, config, "utf-8");
|
|
3477
|
-
spinner.succeed(`Configuration written to ${
|
|
3874
|
+
spinner.succeed(`Configuration written to ${pc14.bold(configFile)}`);
|
|
3478
3875
|
console.error(`
|
|
3479
|
-
${
|
|
3876
|
+
${pc14.dim("Edit the file to customize Fennec behavior.")}
|
|
3480
3877
|
`);
|
|
3481
3878
|
}
|
|
3482
3879
|
|
|
3483
3880
|
// src/commands/health.ts
|
|
3484
3881
|
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3485
3882
|
import { execSync as execSync4 } from "child_process";
|
|
3486
|
-
import { resolve as
|
|
3487
|
-
import { homedir as
|
|
3488
|
-
import
|
|
3883
|
+
import { resolve as resolve8 } from "path";
|
|
3884
|
+
import { homedir as homedir6 } from "os";
|
|
3885
|
+
import pc15 from "picocolors";
|
|
3489
3886
|
async function healthCommand() {
|
|
3490
3887
|
printBanner();
|
|
3491
|
-
console.error(` ${
|
|
3888
|
+
console.error(` ${pc15.bold("Fennec Health Check")}
|
|
3492
3889
|
`);
|
|
3493
3890
|
let adbStatus = "unknown";
|
|
3494
3891
|
try {
|
|
@@ -3498,20 +3895,20 @@ async function healthCommand() {
|
|
|
3498
3895
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3499
3896
|
});
|
|
3500
3897
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
3501
|
-
adbStatus =
|
|
3898
|
+
adbStatus = pc15.green(version);
|
|
3502
3899
|
} catch {
|
|
3503
|
-
adbStatus =
|
|
3900
|
+
adbStatus = pc15.dim("not found") + " (optional)";
|
|
3504
3901
|
}
|
|
3505
3902
|
const tracked = readTracked();
|
|
3506
3903
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
3507
|
-
const logDir =
|
|
3904
|
+
const logDir = resolve8(homedir6(), ".fennec", "logs");
|
|
3508
3905
|
let logSize = "0 B";
|
|
3509
3906
|
try {
|
|
3510
3907
|
if (existsSync6(logDir)) {
|
|
3511
3908
|
const files = readdirSync2(logDir);
|
|
3512
3909
|
let totalBytes = 0;
|
|
3513
3910
|
for (const f of files) {
|
|
3514
|
-
const fStat = statSync2(
|
|
3911
|
+
const fStat = statSync2(resolve8(logDir, f));
|
|
3515
3912
|
totalBytes += fStat.size;
|
|
3516
3913
|
}
|
|
3517
3914
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -3524,7 +3921,7 @@ async function healthCommand() {
|
|
|
3524
3921
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
3525
3922
|
} catch {
|
|
3526
3923
|
}
|
|
3527
|
-
console.error(` ${symbols.fox} ${
|
|
3924
|
+
console.error(` ${symbols.fox} ${pc15.bold("System")}
|
|
3528
3925
|
`);
|
|
3529
3926
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
3530
3927
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -3532,7 +3929,7 @@ async function healthCommand() {
|
|
|
3532
3929
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
3533
3930
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
3534
3931
|
console.error();
|
|
3535
|
-
console.error(` ${symbols.fox} ${
|
|
3932
|
+
console.error(` ${symbols.fox} ${pc15.bold("Processes")}
|
|
3536
3933
|
`);
|
|
3537
3934
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
3538
3935
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -3540,15 +3937,238 @@ async function healthCommand() {
|
|
|
3540
3937
|
console.error();
|
|
3541
3938
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
3542
3939
|
if (allHealthy) {
|
|
3543
|
-
console.error(` ${
|
|
3940
|
+
console.error(` ${pc15.green("\u2713")} ${pc15.bold("All systems healthy")}
|
|
3544
3941
|
`);
|
|
3545
3942
|
} else {
|
|
3546
3943
|
const stopped = tracked.length - runningCount;
|
|
3547
|
-
console.error(` ${
|
|
3944
|
+
console.error(` ${pc15.yellow("\u26A0")} ${pc15.bold(`${stopped} process(es) stopped`)} ${pc15.dim("fennec ps")}
|
|
3548
3945
|
`);
|
|
3549
3946
|
}
|
|
3550
3947
|
}
|
|
3551
3948
|
|
|
3949
|
+
// src/commands/cleanup.ts
|
|
3950
|
+
import pc16 from "picocolors";
|
|
3951
|
+
async function cleanupCommand() {
|
|
3952
|
+
const tracked = readTracked();
|
|
3953
|
+
if (tracked.length === 0) {
|
|
3954
|
+
console.error(`
|
|
3955
|
+
${pc16.dim("No tracked processes to clean up.")}
|
|
3956
|
+
`);
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
3960
|
+
if (toRemove.length === 0) {
|
|
3961
|
+
console.error(`
|
|
3962
|
+
${pc16.green("\u2713")} ${pc16.bold("All clean")} ${pc16.dim("\u2014 no dead entries without saved commands.")}
|
|
3963
|
+
`);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
console.error(`
|
|
3967
|
+
${pc16.yellow("\u26A0")} ${pc16.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
3968
|
+
`);
|
|
3969
|
+
for (const entry of toRemove) {
|
|
3970
|
+
console.error(` ${pc16.dim("\xB7")} ${pc16.bold(entry.name)} ${pc16.dim(`(PID ${entry.pid})`)}`);
|
|
3971
|
+
}
|
|
3972
|
+
console.error();
|
|
3973
|
+
const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
|
|
3974
|
+
if (!confirmed) {
|
|
3975
|
+
console.error(` ${pc16.dim("Cancelled")}
|
|
3976
|
+
`);
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
3980
|
+
saveTracked(remaining);
|
|
3981
|
+
console.error(`
|
|
3982
|
+
${pc16.green("\u2713")} ${pc16.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc16.dim(`(${remaining.length} remaining)`)}
|
|
3983
|
+
`);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// src/commands/info.ts
|
|
3987
|
+
import pc17 from "picocolors";
|
|
3988
|
+
import { resolve as resolve9 } from "path";
|
|
3989
|
+
import { homedir as homedir7 } from "os";
|
|
3990
|
+
async function infoCommand(args2) {
|
|
3991
|
+
const name = args2[0];
|
|
3992
|
+
if (!name) {
|
|
3993
|
+
console.error(renderError("Missing name", "Usage: fennec info <name>"));
|
|
3994
|
+
process.exit(1);
|
|
3995
|
+
}
|
|
3996
|
+
const tracked = readTracked();
|
|
3997
|
+
const match = tracked.find((t) => t.name === name);
|
|
3998
|
+
if (!match) {
|
|
3999
|
+
console.error(renderError("Not found", `No tracked process named "${name}".`));
|
|
4000
|
+
process.exit(1);
|
|
4001
|
+
}
|
|
4002
|
+
const running = isProcessRunning(match.pid);
|
|
4003
|
+
const statusIcon = running ? pc17.green("\u25CF") : pc17.red("\u25CB");
|
|
4004
|
+
const statusText = running ? pc17.green("running") : pc17.red("stopped");
|
|
4005
|
+
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
4006
|
+
const logPath = resolve9(homedir7(), ".fennec", "logs", `${match.name}.log`);
|
|
4007
|
+
console.error(`
|
|
4008
|
+
${symbols.fox} ${pc17.bold(match.name)} ${pc17.dim("\u2014 Process Info")}
|
|
4009
|
+
`);
|
|
4010
|
+
console.error(` ${renderKVColor("Name", match.name, pc17.bold)}`);
|
|
4011
|
+
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
4012
|
+
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
4013
|
+
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
4014
|
+
console.error(` ${renderKVColor("Command", match.command || pc17.dim("(none)"))}`);
|
|
4015
|
+
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
4016
|
+
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
4017
|
+
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
4018
|
+
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
4019
|
+
if (!running && match.command) {
|
|
4020
|
+
console.error(`
|
|
4021
|
+
${pc17.dim("Re-spawn with:")} ${pc17.cyan(`fennec spawn ${match.name}`)}`);
|
|
4022
|
+
}
|
|
4023
|
+
console.error();
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/commands/rename.ts
|
|
4027
|
+
import pc18 from "picocolors";
|
|
4028
|
+
import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
|
|
4029
|
+
import { resolve as resolve10 } from "path";
|
|
4030
|
+
import { homedir as homedir8 } from "os";
|
|
4031
|
+
async function renameCommand(args2) {
|
|
4032
|
+
const [oldName, newName] = args2;
|
|
4033
|
+
if (!oldName || !newName) {
|
|
4034
|
+
console.error(renderError("Missing arguments", "Usage: fennec rename <old-name> <new-name>"));
|
|
4035
|
+
process.exit(1);
|
|
4036
|
+
}
|
|
4037
|
+
if (oldName === newName) {
|
|
4038
|
+
console.error(`
|
|
4039
|
+
${pc18.yellow("\u26A0")} ${pc18.dim("Old and new names are the same.")}
|
|
4040
|
+
`);
|
|
4041
|
+
process.exit(0);
|
|
4042
|
+
}
|
|
4043
|
+
const tracked = readTracked();
|
|
4044
|
+
const match = tracked.find((t) => t.name === oldName);
|
|
4045
|
+
if (!match) {
|
|
4046
|
+
console.error(renderError("Not found", `No tracked process named "${oldName}".`));
|
|
4047
|
+
process.exit(1);
|
|
4048
|
+
}
|
|
4049
|
+
if (tracked.some((t) => t.name === newName)) {
|
|
4050
|
+
console.error(renderError("Name taken", `A process named "${newName}" already exists.`));
|
|
4051
|
+
process.exit(1);
|
|
4052
|
+
}
|
|
4053
|
+
console.error(`
|
|
4054
|
+
${symbols.fox} ${pc18.bold("Rename Process")}
|
|
4055
|
+
`);
|
|
4056
|
+
console.error(` ${renderKVColor("From", oldName)}`);
|
|
4057
|
+
console.error(` ${renderKVColor("To", newName)}`);
|
|
4058
|
+
if (isProcessRunning(match.pid)) {
|
|
4059
|
+
console.error(` ${pc18.yellow("\u26A0")} ${pc18.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
|
|
4060
|
+
}
|
|
4061
|
+
console.error();
|
|
4062
|
+
const confirmed = await confirmPrompt(`Rename ${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}?`, true);
|
|
4063
|
+
if (!confirmed) {
|
|
4064
|
+
console.error(` ${pc18.dim("Cancelled")}
|
|
4065
|
+
`);
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
4069
|
+
const logDir = resolve10(homedir8(), ".fennec", "logs");
|
|
4070
|
+
const oldLog = resolve10(logDir, `${oldName}.log`);
|
|
4071
|
+
const newLog = resolve10(logDir, `${newName}.log`);
|
|
4072
|
+
if (existsSync7(oldLog) && !existsSync7(newLog)) {
|
|
4073
|
+
try {
|
|
4074
|
+
renameSync2(oldLog, newLog);
|
|
4075
|
+
} catch {
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
match.name = newName;
|
|
4079
|
+
saveTracked(tracked);
|
|
4080
|
+
spinner.succeed(`${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}`);
|
|
4081
|
+
console.error();
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// src/commands/export-import.ts
|
|
4085
|
+
import pc19 from "picocolors";
|
|
4086
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
4087
|
+
import { resolve as resolve11 } from "path";
|
|
4088
|
+
async function exportCommand(args2) {
|
|
4089
|
+
const tracked = readTracked();
|
|
4090
|
+
const fileIndex = args2.indexOf("--file");
|
|
4091
|
+
const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
|
|
4092
|
+
if (fileIndex !== -1 && !filePath) {
|
|
4093
|
+
console.error(renderError("Missing file path", "Usage: fennec export --file <path>"));
|
|
4094
|
+
process.exit(1);
|
|
4095
|
+
}
|
|
4096
|
+
if (tracked.length === 0) {
|
|
4097
|
+
console.error(`
|
|
4098
|
+
${pc19.dim("No tracked processes to export.")}
|
|
4099
|
+
`);
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
const json = JSON.stringify(tracked, null, 2);
|
|
4103
|
+
if (filePath) {
|
|
4104
|
+
try {
|
|
4105
|
+
writeFileSync3(resolve11(filePath), json, "utf-8");
|
|
4106
|
+
console.error(`
|
|
4107
|
+
${pc19.green("\u2713")} ${pc19.bold(`Exported ${tracked.length} process(es)`)} ${pc19.dim(`\u2192 ${filePath}`)}
|
|
4108
|
+
`);
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
console.error(renderError("Export failed", String(err)));
|
|
4111
|
+
process.exit(1);
|
|
4112
|
+
}
|
|
4113
|
+
} else {
|
|
4114
|
+
console.log(json);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
async function importCommand(args2) {
|
|
4118
|
+
const filePath = args2[0];
|
|
4119
|
+
if (!filePath) {
|
|
4120
|
+
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
4121
|
+
process.exit(1);
|
|
4122
|
+
}
|
|
4123
|
+
const resolvedPath = resolve11(filePath);
|
|
4124
|
+
if (!existsSync8(resolvedPath)) {
|
|
4125
|
+
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
4126
|
+
process.exit(1);
|
|
4127
|
+
}
|
|
4128
|
+
let imported;
|
|
4129
|
+
try {
|
|
4130
|
+
imported = JSON.parse(readFileSync4(resolvedPath, "utf-8"));
|
|
4131
|
+
if (!Array.isArray(imported) || imported.length === 0) {
|
|
4132
|
+
console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
|
|
4133
|
+
process.exit(1);
|
|
4134
|
+
}
|
|
4135
|
+
} catch (err) {
|
|
4136
|
+
console.error(renderError("Invalid JSON", String(err)));
|
|
4137
|
+
process.exit(1);
|
|
4138
|
+
}
|
|
4139
|
+
const existing = readTracked();
|
|
4140
|
+
console.error(`
|
|
4141
|
+
${symbols.fox} ${pc19.bold("Import Processes")}
|
|
4142
|
+
`);
|
|
4143
|
+
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
4144
|
+
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
4145
|
+
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
4146
|
+
console.error(`
|
|
4147
|
+
${pc19.bold("Processes to import:")}`);
|
|
4148
|
+
for (const p of imported) {
|
|
4149
|
+
console.error(` ${pc19.green("+")} ${pc19.bold(p.name)} ${pc19.dim(`(${p.command})`)}`);
|
|
4150
|
+
}
|
|
4151
|
+
console.error();
|
|
4152
|
+
const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
|
|
4153
|
+
if (!confirmed) {
|
|
4154
|
+
console.error(` ${pc19.dim("Cancelled")}
|
|
4155
|
+
`);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
const merged = [...existing];
|
|
4159
|
+
for (const proc of imported) {
|
|
4160
|
+
const idx = merged.findIndex((t) => t.name === proc.name);
|
|
4161
|
+
if (idx !== -1) {
|
|
4162
|
+
merged[idx] = { ...merged[idx], ...proc };
|
|
4163
|
+
} else {
|
|
4164
|
+
merged.push(proc);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
saveTracked(merged);
|
|
4168
|
+
console.error(` ${pc19.green("\u2713")} ${pc19.bold(`Imported ${imported.length} process(es)`)} ${pc19.dim(`(${merged.length} total)`)}
|
|
4169
|
+
`);
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3552
4172
|
// src/index.ts
|
|
3553
4173
|
var [, , command, ...args] = process.argv;
|
|
3554
4174
|
async function main() {
|
|
@@ -3568,10 +4188,26 @@ async function main() {
|
|
|
3568
4188
|
await statusCommand(args);
|
|
3569
4189
|
} else if (command === "log") {
|
|
3570
4190
|
await logCommand(args);
|
|
4191
|
+
} else if (command === "spawn") {
|
|
4192
|
+
await spawnCommand(args);
|
|
4193
|
+
} else if (command === "stop") {
|
|
4194
|
+
await stopCommand(args);
|
|
3571
4195
|
} else if (command === "kill") {
|
|
3572
4196
|
await killCommand(args);
|
|
3573
4197
|
} else if (command === "restart") {
|
|
3574
4198
|
await restartCommand(args);
|
|
4199
|
+
} else if (command === "info") {
|
|
4200
|
+
printBanner();
|
|
4201
|
+
await infoCommand(args);
|
|
4202
|
+
} else if (command === "cleanup") {
|
|
4203
|
+
printBanner();
|
|
4204
|
+
await cleanupCommand();
|
|
4205
|
+
} else if (command === "rename") {
|
|
4206
|
+
await renameCommand(args);
|
|
4207
|
+
} else if (command === "export") {
|
|
4208
|
+
await exportCommand(args);
|
|
4209
|
+
} else if (command === "import") {
|
|
4210
|
+
await importCommand(args);
|
|
3575
4211
|
} else if (command === "attach") {
|
|
3576
4212
|
await attachCommand(args);
|
|
3577
4213
|
} else if (command === "pipe") {
|