cligr 1.0.0 → 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.
@@ -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