cligr 1.0.1 → 1.0.2
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/commands/config.js +102 -0
- package/dist/commands/down.js +26 -0
- package/dist/commands/groups.js +43 -0
- package/dist/commands/ls.js +23 -0
- package/dist/commands/up.js +39 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.js +0 -0
- package/dist/index.js +35 -35
- package/dist/process/manager.js +226 -0
- package/dist/process/pid-store.js +141 -0
- package/dist/process/template.js +53 -0
- package/package.json +2 -1
- package/src/commands/down.ts +28 -3
- package/src/commands/up.ts +5 -0
- package/src/process/manager.ts +102 -2
- package/src/process/pid-store.ts +192 -0
- package/tests/integration/commands.test.ts +3 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
const CONFIG_FILENAME = ".cligr.yml";
|
|
6
|
+
const TEMPLATE = `# Cligr Configuration
|
|
7
|
+
|
|
8
|
+
groups:
|
|
9
|
+
web:
|
|
10
|
+
tool: docker
|
|
11
|
+
restart: false
|
|
12
|
+
items:
|
|
13
|
+
- "nginx,8080" # $1=nginx (name), $2=8080 (port)
|
|
14
|
+
- "nginx,3000"
|
|
15
|
+
|
|
16
|
+
simple:
|
|
17
|
+
tool: node
|
|
18
|
+
items:
|
|
19
|
+
- "server" # $1=server (name only)
|
|
20
|
+
|
|
21
|
+
tools:
|
|
22
|
+
docker:
|
|
23
|
+
cmd: "docker run -p $2:$2 nginx" # $1=name, $2=port
|
|
24
|
+
node:
|
|
25
|
+
cmd: "node $1.js" # $1=file name
|
|
26
|
+
|
|
27
|
+
# Syntax:
|
|
28
|
+
# - Items are comma-separated: "name,arg2,arg3"
|
|
29
|
+
# - $1 = name (first value)
|
|
30
|
+
# - $2, $3... = additional arguments
|
|
31
|
+
# - If no tool specified, executes directly
|
|
32
|
+
`;
|
|
33
|
+
function detectEditor() {
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
const whichCmd = platform === "win32" ? "where" : "which";
|
|
36
|
+
const codeCheck = spawnSync(whichCmd, ["code"], { stdio: "ignore" });
|
|
37
|
+
if (codeCheck.status === 0) {
|
|
38
|
+
return "code";
|
|
39
|
+
}
|
|
40
|
+
if (process.env.EDITOR) {
|
|
41
|
+
return process.env.EDITOR;
|
|
42
|
+
}
|
|
43
|
+
if (platform === "win32") {
|
|
44
|
+
return "notepad.exe";
|
|
45
|
+
}
|
|
46
|
+
return "vim";
|
|
47
|
+
}
|
|
48
|
+
function spawnEditor(filePath, editorCmd) {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
const whichCmd = platform === "win32" ? "where" : "which";
|
|
51
|
+
const editorCheck = spawnSync(whichCmd, [editorCmd], { stdio: "ignore" });
|
|
52
|
+
if (editorCheck.status !== 0 && editorCmd !== process.env.EDITOR) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Editor '${editorCmd}' not found.
|
|
55
|
+
Install VS Code or set EDITOR environment variable.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
export EDITOR=vim
|
|
59
|
+
cligr config`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const child = spawn(editorCmd, [filePath], {
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: "ignore",
|
|
65
|
+
shell: platform === "win32"
|
|
66
|
+
});
|
|
67
|
+
child.unref();
|
|
68
|
+
}
|
|
69
|
+
function createTemplate(filePath) {
|
|
70
|
+
const dir = path.dirname(filePath);
|
|
71
|
+
if (!fs.existsSync(dir)) {
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
fs.writeFileSync(filePath, TEMPLATE, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
async function configCommand() {
|
|
77
|
+
try {
|
|
78
|
+
const homeDirConfig = path.join(os.homedir(), CONFIG_FILENAME);
|
|
79
|
+
const currentDirConfig = path.resolve(CONFIG_FILENAME);
|
|
80
|
+
let configPath;
|
|
81
|
+
if (fs.existsSync(homeDirConfig)) {
|
|
82
|
+
configPath = homeDirConfig;
|
|
83
|
+
} else if (fs.existsSync(currentDirConfig)) {
|
|
84
|
+
configPath = currentDirConfig;
|
|
85
|
+
} else {
|
|
86
|
+
configPath = homeDirConfig;
|
|
87
|
+
}
|
|
88
|
+
if (!fs.existsSync(configPath)) {
|
|
89
|
+
createTemplate(configPath);
|
|
90
|
+
}
|
|
91
|
+
const editor = detectEditor();
|
|
92
|
+
spawnEditor(configPath, editor);
|
|
93
|
+
console.log(`Opening ${configPath} in ${editor}...`);
|
|
94
|
+
return 0;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`Error: ${err.message}`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
configCommand
|
|
102
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProcessManager } from "../process/manager.js";
|
|
2
|
+
async function downCommand(groupName) {
|
|
3
|
+
const manager = new ProcessManager();
|
|
4
|
+
const result = await manager.killGroupByPid(groupName);
|
|
5
|
+
if (result.killed === 0 && result.notRunning === 0) {
|
|
6
|
+
console.log(`Group '${groupName}' is not running`);
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
if (result.killed > 0) {
|
|
10
|
+
console.log(`Stopped ${result.killed} process(es) for group '${groupName}'`);
|
|
11
|
+
}
|
|
12
|
+
if (result.notRunning > 0) {
|
|
13
|
+
console.log(`Cleaned up ${result.notRunning} stale PID file(s) for group '${groupName}'`);
|
|
14
|
+
}
|
|
15
|
+
if (result.errors.length > 0) {
|
|
16
|
+
console.error("Errors while stopping processes:");
|
|
17
|
+
for (const err of result.errors) {
|
|
18
|
+
console.error(` ${err}`);
|
|
19
|
+
}
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
downCommand
|
|
26
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
2
|
+
async function groupsCommand(verbose) {
|
|
3
|
+
const loader = new ConfigLoader();
|
|
4
|
+
try {
|
|
5
|
+
const groupNames = loader.listGroups();
|
|
6
|
+
if (groupNames.length === 0) {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
if (verbose) {
|
|
10
|
+
const config = loader.load();
|
|
11
|
+
const details = [];
|
|
12
|
+
for (const name of groupNames) {
|
|
13
|
+
const group = config.groups[name];
|
|
14
|
+
details.push({
|
|
15
|
+
name,
|
|
16
|
+
tool: group.tool || "(none)",
|
|
17
|
+
restart: group.restart || "(none)",
|
|
18
|
+
itemCount: group.items.length
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const maxNameLen = Math.max("GROUP".length, ...details.map((d) => d.name.length));
|
|
22
|
+
const maxToolLen = Math.max("TOOL".length, ...details.map((d) => d.tool.length));
|
|
23
|
+
const maxRestartLen = Math.max("RESTART".length, ...details.map((d) => d.restart.length));
|
|
24
|
+
const header = "GROUP".padEnd(maxNameLen) + " " + "TOOL".padEnd(maxToolLen) + " " + "RESTART".padEnd(maxRestartLen) + " ITEMS";
|
|
25
|
+
console.log(header);
|
|
26
|
+
for (const d of details) {
|
|
27
|
+
const row = d.name.padEnd(maxNameLen) + " " + d.tool.padEnd(maxToolLen) + " " + d.restart.padEnd(maxRestartLen) + " " + String(d.itemCount);
|
|
28
|
+
console.log(row);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
for (const name of groupNames) {
|
|
32
|
+
console.log(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(err.message);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
groupsCommand
|
|
43
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
2
|
+
async function lsCommand(groupName) {
|
|
3
|
+
const loader = new ConfigLoader();
|
|
4
|
+
try {
|
|
5
|
+
const { config } = loader.getGroup(groupName);
|
|
6
|
+
console.log(`
|
|
7
|
+
Group: ${groupName}`);
|
|
8
|
+
console.log(`Tool: ${config.tool}`);
|
|
9
|
+
console.log(`Restart: ${config.restart}`);
|
|
10
|
+
console.log("\nItems:");
|
|
11
|
+
for (const item of config.items) {
|
|
12
|
+
console.log(` - ${item}`);
|
|
13
|
+
}
|
|
14
|
+
console.log("");
|
|
15
|
+
return 0;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err.message);
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
lsCommand
|
|
23
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
2
|
+
import { TemplateExpander } from "../process/template.js";
|
|
3
|
+
import { ProcessManager } from "../process/manager.js";
|
|
4
|
+
import { PidStore } from "../process/pid-store.js";
|
|
5
|
+
async function upCommand(groupName) {
|
|
6
|
+
const loader = new ConfigLoader();
|
|
7
|
+
const manager = new ProcessManager();
|
|
8
|
+
const pidStore = new PidStore();
|
|
9
|
+
try {
|
|
10
|
+
await pidStore.cleanupStalePids();
|
|
11
|
+
const { config, tool, toolTemplate } = loader.getGroup(groupName);
|
|
12
|
+
const items = config.items.map(
|
|
13
|
+
(itemStr, index) => TemplateExpander.parseItem(tool, toolTemplate, itemStr, index)
|
|
14
|
+
);
|
|
15
|
+
manager.spawnGroup(groupName, items, config.restart);
|
|
16
|
+
console.log(`Started group ${groupName} with ${items.length} process(es)`);
|
|
17
|
+
console.log("Press Ctrl+C to stop...");
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const cleanup = async () => {
|
|
20
|
+
console.log("\nShutting down...");
|
|
21
|
+
process.removeListener("SIGINT", cleanup);
|
|
22
|
+
process.removeListener("SIGTERM", cleanup);
|
|
23
|
+
await manager.killAll();
|
|
24
|
+
resolve(0);
|
|
25
|
+
};
|
|
26
|
+
process.on("SIGINT", cleanup);
|
|
27
|
+
process.on("SIGTERM", cleanup);
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof Error && error.name === "ConfigError") {
|
|
31
|
+
console.error(error.message);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
upCommand
|
|
39
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
const CONFIG_FILENAME = ".cligr.yml";
|
|
6
|
+
class ConfigError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ConfigError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
class ConfigLoader {
|
|
13
|
+
configPath;
|
|
14
|
+
constructor(configPath) {
|
|
15
|
+
if (configPath) {
|
|
16
|
+
this.configPath = path.resolve(configPath);
|
|
17
|
+
} else {
|
|
18
|
+
const homeDirConfig = path.join(os.homedir(), CONFIG_FILENAME);
|
|
19
|
+
const currentDirConfig = path.resolve(CONFIG_FILENAME);
|
|
20
|
+
if (fs.existsSync(homeDirConfig)) {
|
|
21
|
+
this.configPath = homeDirConfig;
|
|
22
|
+
} else if (fs.existsSync(currentDirConfig)) {
|
|
23
|
+
this.configPath = currentDirConfig;
|
|
24
|
+
} else {
|
|
25
|
+
this.configPath = homeDirConfig;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
load() {
|
|
30
|
+
if (!fs.existsSync(this.configPath)) {
|
|
31
|
+
throw new ConfigError(
|
|
32
|
+
`Config file not found. Looking for:
|
|
33
|
+
- ${path.join(os.homedir(), CONFIG_FILENAME)}
|
|
34
|
+
- ${path.resolve(CONFIG_FILENAME)}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const content = fs.readFileSync(this.configPath, "utf-8");
|
|
38
|
+
let config;
|
|
39
|
+
try {
|
|
40
|
+
config = yaml.load(content);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new ConfigError(`Invalid YAML: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
return this.validate(config);
|
|
45
|
+
}
|
|
46
|
+
validate(config) {
|
|
47
|
+
if (!config || typeof config !== "object") {
|
|
48
|
+
throw new ConfigError("Config must be an object");
|
|
49
|
+
}
|
|
50
|
+
const cfg = config;
|
|
51
|
+
if (!cfg.groups || typeof cfg.groups !== "object") {
|
|
52
|
+
throw new ConfigError('Config must have a "groups" object');
|
|
53
|
+
}
|
|
54
|
+
return cfg;
|
|
55
|
+
}
|
|
56
|
+
getGroup(name) {
|
|
57
|
+
const config = this.load();
|
|
58
|
+
const group = config.groups[name];
|
|
59
|
+
if (!group) {
|
|
60
|
+
const available = Object.keys(config.groups).join(", ");
|
|
61
|
+
throw new ConfigError(`Unknown group: ${name}. Available: ${available}`);
|
|
62
|
+
}
|
|
63
|
+
let toolTemplate = null;
|
|
64
|
+
let tool = null;
|
|
65
|
+
if (config.tools && config.tools[group.tool]) {
|
|
66
|
+
toolTemplate = config.tools[group.tool].cmd;
|
|
67
|
+
tool = group.tool;
|
|
68
|
+
} else {
|
|
69
|
+
tool = null;
|
|
70
|
+
toolTemplate = null;
|
|
71
|
+
}
|
|
72
|
+
return { config: group, tool, toolTemplate };
|
|
73
|
+
}
|
|
74
|
+
listGroups() {
|
|
75
|
+
const config = this.load();
|
|
76
|
+
return Object.keys(config.groups);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
ConfigError,
|
|
81
|
+
ConfigLoader
|
|
82
|
+
};
|
|
File without changes
|