cligr 1.0.7 → 1.0.9

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.
Files changed (35) hide show
  1. package/.claude/worktrees/agent-ac25cfb2/.claude/settings.local.json +30 -0
  2. package/.claude/worktrees/agent-ac25cfb2/README.md +65 -0
  3. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-13-named-params-support.md +391 -0
  4. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-design.md +164 -0
  5. package/.claude/worktrees/agent-ac25cfb2/docs/plans/2026-02-25-named-items-implementation.md +460 -0
  6. package/.claude/worktrees/agent-ac25cfb2/package-lock.json +554 -0
  7. package/.claude/worktrees/agent-ac25cfb2/package.json +27 -0
  8. package/.claude/worktrees/agent-ac25cfb2/scripts/build.js +20 -0
  9. package/.claude/worktrees/agent-ac25cfb2/scripts/test.js +168 -0
  10. package/.claude/worktrees/agent-ac25cfb2/src/commands/config.ts +121 -0
  11. package/.claude/worktrees/agent-ac25cfb2/src/commands/groups.ts +68 -0
  12. package/.claude/worktrees/agent-ac25cfb2/src/commands/ls.ts +25 -0
  13. package/.claude/worktrees/agent-ac25cfb2/src/commands/up.ts +49 -0
  14. package/.claude/worktrees/agent-ac25cfb2/src/config/loader.ts +148 -0
  15. package/.claude/worktrees/agent-ac25cfb2/src/config/types.ts +26 -0
  16. package/.claude/worktrees/agent-ac25cfb2/src/index.ts +97 -0
  17. package/.claude/worktrees/agent-ac25cfb2/src/process/manager.ts +270 -0
  18. package/.claude/worktrees/agent-ac25cfb2/src/process/pid-store.ts +203 -0
  19. package/.claude/worktrees/agent-ac25cfb2/src/process/template.ts +87 -0
  20. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes-fixed.test.ts +255 -0
  21. package/.claude/worktrees/agent-ac25cfb2/tests/integration/blocking-processes.test.ts +497 -0
  22. package/.claude/worktrees/agent-ac25cfb2/tests/integration/commands.test.ts +648 -0
  23. package/.claude/worktrees/agent-ac25cfb2/tests/integration/config-loader.test.ts +426 -0
  24. package/.claude/worktrees/agent-ac25cfb2/tests/integration/process-manager.test.ts +394 -0
  25. package/.claude/worktrees/agent-ac25cfb2/tests/integration/template-expander.test.ts +454 -0
  26. package/.claude/worktrees/agent-ac25cfb2/tsconfig.json +15 -0
  27. package/.claude/worktrees/agent-ac25cfb2/usage.md +9 -0
  28. package/dist/index.js +103 -46
  29. package/docs/superpowers/specs/2026-04-13-improve-web-ui-console-design.md +38 -0
  30. package/package.json +1 -1
  31. package/src/commands/groups.ts +1 -1
  32. package/src/commands/ls.ts +1 -1
  33. package/src/commands/serve.ts +65 -8
  34. package/src/config/loader.ts +6 -2
  35. package/src/config/types.ts +1 -1
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test runner script that compiles TypeScript tests with esbuild
5
+ * and runs them with Node.js built-in test runner
6
+ */
7
+
8
+ import { build } from 'esbuild';
9
+ import { spawnSync } from 'child_process';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
17
+ const TESTS_DIR = path.join(PROJECT_ROOT, 'tests/integration');
18
+ const DIST_DIR = path.join(PROJECT_ROOT, 'tests-dist');
19
+ const SRC_DIR = path.join(PROJECT_ROOT, 'src');
20
+
21
+ async function buildSourceModules() {
22
+ console.log('Building source modules...');
23
+
24
+ // Build source modules directly to src directory (as .js files alongside .ts)
25
+ const dirs = ['config', 'process', 'commands'];
26
+
27
+ for (const dir of dirs) {
28
+ const dirPath = path.join(SRC_DIR, dir);
29
+ if (!fs.existsSync(dirPath)) continue;
30
+
31
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.ts'));
32
+
33
+ for (const file of files) {
34
+ const inFile = path.join(dirPath, file);
35
+ const outFile = path.join(dirPath, file.replace('.ts', '.js'));
36
+
37
+ await build({
38
+ entryPoints: [inFile],
39
+ bundle: false,
40
+ platform: 'node',
41
+ target: 'es2022',
42
+ format: 'esm',
43
+ outfile: outFile,
44
+ absWorkingDir: PROJECT_ROOT,
45
+ logLevel: 'silent',
46
+ });
47
+ }
48
+ }
49
+
50
+ console.log('Built source modules');
51
+ }
52
+
53
+ async function buildTests(skipBlocking = false) {
54
+ console.log('Building TypeScript tests...');
55
+
56
+ // Clean dist directory
57
+ if (fs.existsSync(DIST_DIR)) {
58
+ fs.rmSync(DIST_DIR, { recursive: true, force: true });
59
+ }
60
+ fs.mkdirSync(DIST_DIR, { recursive: true });
61
+
62
+ // Also create tests/integration subdirectory to match original structure
63
+ const outputSubDir = path.join(DIST_DIR, 'integration');
64
+ fs.mkdirSync(outputSubDir, { recursive: true });
65
+
66
+ // Find all test files
67
+ let testFiles = fs.readdirSync(TESTS_DIR)
68
+ .filter(f => f.endsWith('.test.ts'))
69
+ .map(f => path.join(TESTS_DIR, f));
70
+
71
+ // Skip blocking-processes tests by default (they have infinite loops)
72
+ if (skipBlocking) {
73
+ testFiles = testFiles.filter(f => !f.includes('blocking-processes'));
74
+ }
75
+
76
+ // Build each test file
77
+ for (const testFile of testFiles) {
78
+ const outputFile = path.join(
79
+ outputSubDir,
80
+ path.basename(testFile).replace('.ts', '.js')
81
+ );
82
+
83
+ await build({
84
+ entryPoints: [testFile],
85
+ bundle: false,
86
+ platform: 'node',
87
+ target: 'es2022',
88
+ format: 'esm',
89
+ outfile: outputFile,
90
+ absWorkingDir: PROJECT_ROOT,
91
+ logLevel: 'error',
92
+ });
93
+ }
94
+
95
+ console.log(`Built ${testFiles.length} test files`);
96
+ return testFiles.length;
97
+ }
98
+
99
+ function runTests(verbose = false) {
100
+ const testFiles = fs.readdirSync(path.join(DIST_DIR, 'integration'))
101
+ .filter(f => f.endsWith('.test.js'))
102
+ .map(f => path.join(DIST_DIR, 'integration', f));
103
+
104
+ const args = ['--test'];
105
+ if (verbose) {
106
+ args.push('--verbose');
107
+ }
108
+ args.push(...testFiles);
109
+
110
+ console.log('\nRunning tests...\n');
111
+ const result = spawnSync('node', args, {
112
+ stdio: 'inherit',
113
+ cwd: PROJECT_ROOT,
114
+ shell: true
115
+ });
116
+
117
+ if (result.status !== 0) {
118
+ throw new Error(`Tests failed with exit code ${result.status}`);
119
+ }
120
+
121
+ return { status: result.status };
122
+ }
123
+
124
+ async function cleanupSourceModules() {
125
+ // Remove generated .js files from src directory
126
+ const dirs = ['config', 'process', 'commands'];
127
+
128
+ for (const dir of dirs) {
129
+ const dirPath = path.join(SRC_DIR, dir);
130
+ if (!fs.existsSync(dirPath)) continue;
131
+
132
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.js'));
133
+
134
+ for (const file of files) {
135
+ const filePath = path.join(dirPath, file);
136
+ fs.unlinkSync(filePath);
137
+ }
138
+ }
139
+ }
140
+
141
+ async function main() {
142
+ const args = process.argv.slice(2);
143
+ const verbose = args.includes('--verbose') || args.includes('-v');
144
+ const includeBlocking = args.includes('--include-blocking') || args.includes('-b');
145
+
146
+ try {
147
+ await buildSourceModules();
148
+ await buildTests(!includeBlocking);
149
+ runTests(verbose);
150
+
151
+ // Clean up
152
+ cleanupSourceModules();
153
+ if (fs.existsSync(DIST_DIR)) {
154
+ fs.rmSync(DIST_DIR, { recursive: true, force: true });
155
+ }
156
+ } catch (error) {
157
+ console.error('Test failed:', error.message);
158
+ // Still try to clean up on error
159
+ try {
160
+ cleanupSourceModules();
161
+ } catch (e) {
162
+ // Ignore cleanup errors
163
+ }
164
+ process.exit(1);
165
+ }
166
+ }
167
+
168
+ main();
@@ -0,0 +1,121 @@
1
+ import { spawn, spawnSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ const CONFIG_FILENAME = '.cligr.yml';
7
+ const TEMPLATE = `# Cligr Configuration
8
+
9
+ groups:
10
+ web:
11
+ tool: docker
12
+ restart: false
13
+ items:
14
+ - "nginx,8080" # $1=nginx (name), $2=8080 (port)
15
+ - "nginx,3000"
16
+
17
+ simple:
18
+ tool: node
19
+ items:
20
+ - "server" # $1=server (name only)
21
+
22
+ tools:
23
+ docker:
24
+ cmd: "docker run -p $2:$2 nginx" # $1=name, $2=port
25
+ node:
26
+ cmd: "node $1.js" # $1=file name
27
+
28
+ # Syntax:
29
+ # - Items are comma-separated: "name,arg2,arg3"
30
+ # - $1 = name (first value)
31
+ # - $2, $3... = additional arguments
32
+ # - If no tool specified, executes directly
33
+ `;
34
+
35
+ function detectEditor(): string {
36
+ const platform = process.platform;
37
+
38
+ // Try VS Code first
39
+ const whichCmd = platform === 'win32' ? 'where' : 'which';
40
+ const codeCheck = spawnSync(whichCmd, ['code'], { stdio: 'ignore' });
41
+ if (codeCheck.status === 0) {
42
+ return 'code';
43
+ }
44
+
45
+ // Try EDITOR environment variable
46
+ if (process.env.EDITOR) {
47
+ return process.env.EDITOR;
48
+ }
49
+
50
+ // Platform defaults
51
+ if (platform === 'win32') {
52
+ return 'notepad.exe';
53
+ }
54
+ return 'vim';
55
+ }
56
+
57
+ function spawnEditor(filePath: string, editorCmd: string): void {
58
+ // Check if editor exists before spawning
59
+ const platform = process.platform;
60
+ const whichCmd = platform === 'win32' ? 'where' : 'which';
61
+ const editorCheck = spawnSync(whichCmd, [editorCmd], { stdio: 'ignore' });
62
+
63
+ if (editorCheck.status !== 0 && editorCmd !== process.env.EDITOR) {
64
+ throw new Error(
65
+ `Editor '${editorCmd}' not found.\n` +
66
+ `Install VS Code or set EDITOR environment variable.\n\n` +
67
+ `Example:\n` +
68
+ ` export EDITOR=vim\n` +
69
+ ` cligr config`
70
+ );
71
+ }
72
+
73
+ // Spawn detached so terminal is not blocked
74
+ const child = spawn(editorCmd, [filePath], {
75
+ detached: true,
76
+ stdio: 'ignore',
77
+ shell: platform === 'win32',
78
+ });
79
+
80
+ child.unref();
81
+ }
82
+
83
+ function createTemplate(filePath: string): void {
84
+ const dir = path.dirname(filePath);
85
+ if (!fs.existsSync(dir)) {
86
+ fs.mkdirSync(dir, { recursive: true });
87
+ }
88
+ fs.writeFileSync(filePath, TEMPLATE, 'utf-8');
89
+ }
90
+
91
+ export async function configCommand(): Promise<number> {
92
+ try {
93
+ // Determine config path (same logic as ConfigLoader)
94
+ const homeDirConfig = path.join(os.homedir(), CONFIG_FILENAME);
95
+ const currentDirConfig = path.resolve(CONFIG_FILENAME);
96
+
97
+ let configPath: string;
98
+ if (fs.existsSync(homeDirConfig)) {
99
+ configPath = homeDirConfig;
100
+ } else if (fs.existsSync(currentDirConfig)) {
101
+ configPath = currentDirConfig;
102
+ } else {
103
+ configPath = homeDirConfig;
104
+ }
105
+
106
+ // Create template if doesn't exist
107
+ if (!fs.existsSync(configPath)) {
108
+ createTemplate(configPath);
109
+ }
110
+
111
+ // Detect and open editor
112
+ const editor = detectEditor();
113
+ spawnEditor(configPath, editor);
114
+
115
+ console.log(`Opening ${configPath} in ${editor}...`);
116
+ return 0;
117
+ } catch (err: any) {
118
+ console.error(`Error: ${err.message}`);
119
+ return 1;
120
+ }
121
+ }
@@ -0,0 +1,68 @@
1
+ import { ConfigLoader } from '../config/loader.js';
2
+
3
+ interface GroupDetails {
4
+ name: string;
5
+ tool: string;
6
+ restart: string;
7
+ itemCount: number;
8
+ }
9
+
10
+ export async function groupsCommand(verbose: boolean): Promise<number> {
11
+ const loader = new ConfigLoader();
12
+
13
+ try {
14
+ const groupNames = loader.listGroups();
15
+
16
+ if (groupNames.length === 0) {
17
+ // No groups defined - empty output
18
+ return 0;
19
+ }
20
+
21
+ if (verbose) {
22
+ // Verbose mode: gather details and print table
23
+ const config = loader.load();
24
+ const details: GroupDetails[] = [];
25
+
26
+ for (const name of groupNames) {
27
+ const group = config.groups[name];
28
+ details.push({
29
+ name,
30
+ tool: group.tool || '(none)',
31
+ restart: group.restart || '(none)',
32
+ itemCount: Object.keys(group.items).length,
33
+ });
34
+ }
35
+
36
+ // Calculate column widths
37
+ const maxNameLen = Math.max('GROUP'.length, ...details.map(d => d.name.length));
38
+ const maxToolLen = Math.max('TOOL'.length, ...details.map(d => d.tool.length));
39
+ const maxRestartLen = Math.max('RESTART'.length, ...details.map(d => d.restart.length));
40
+
41
+ // Print header
42
+ const header = 'GROUP'.padEnd(maxNameLen) + ' ' +
43
+ 'TOOL'.padEnd(maxToolLen) + ' ' +
44
+ 'RESTART'.padEnd(maxRestartLen) + ' ' +
45
+ 'ITEMS';
46
+ console.log(header);
47
+
48
+ // Print rows
49
+ for (const d of details) {
50
+ const row = d.name.padEnd(maxNameLen) + ' ' +
51
+ d.tool.padEnd(maxToolLen) + ' ' +
52
+ d.restart.padEnd(maxRestartLen) + ' ' +
53
+ String(d.itemCount);
54
+ console.log(row);
55
+ }
56
+ } else {
57
+ // Simple mode: just list names
58
+ for (const name of groupNames) {
59
+ console.log(name);
60
+ }
61
+ }
62
+
63
+ return 0;
64
+ } catch (err) {
65
+ console.error((err as Error).message);
66
+ return 1;
67
+ }
68
+ }
@@ -0,0 +1,25 @@
1
+ import { ConfigLoader } from '../config/loader.js';
2
+
3
+ export async function lsCommand(groupName: string): Promise<number> {
4
+ const loader = new ConfigLoader();
5
+
6
+ try {
7
+ const { config, items } = loader.getGroup(groupName);
8
+
9
+ console.log(`\nGroup: ${groupName}`);
10
+ console.log(`Tool: ${config.tool}`);
11
+ console.log(`Restart: ${config.restart}`);
12
+ console.log('\nItems:');
13
+
14
+ for (const item of items) {
15
+ console.log(` ${item.name}: ${item.value}`);
16
+ }
17
+
18
+ console.log('');
19
+
20
+ return 0;
21
+ } catch (err) {
22
+ console.error((err as Error).message);
23
+ return 1;
24
+ }
25
+ }
@@ -0,0 +1,49 @@
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
+
6
+ export async function upCommand(groupName: string): Promise<number> {
7
+ const loader = new ConfigLoader();
8
+ const manager = new ProcessManager();
9
+ const pidStore = new PidStore();
10
+
11
+ try {
12
+ // Clean up any stale PID files for this group on startup
13
+ await pidStore.cleanupStalePids();
14
+
15
+ // Load group config
16
+ const { config, items, tool, toolTemplate, params } = loader.getGroup(groupName);
17
+
18
+ // Build process items
19
+ const processItems = items.map((item, index) =>
20
+ TemplateExpander.parseItem(tool, toolTemplate, item, index, params)
21
+ );
22
+
23
+ // Spawn all processes
24
+ manager.spawnGroup(groupName, processItems, config.restart);
25
+
26
+ console.log(`Started group ${groupName} with ${processItems.length} process(es)`);
27
+ console.log('Press Ctrl+C to stop...');
28
+
29
+ // Wait for signals
30
+ return new Promise((resolve) => {
31
+ const cleanup = async () => {
32
+ console.log('\nShutting down...');
33
+ process.removeListener('SIGINT', cleanup);
34
+ process.removeListener('SIGTERM', cleanup);
35
+ await manager.killAll();
36
+ resolve(0);
37
+ };
38
+
39
+ process.on('SIGINT', cleanup);
40
+ process.on('SIGTERM', cleanup);
41
+ });
42
+ } catch (error) {
43
+ if (error instanceof Error && error.name === 'ConfigError') {
44
+ console.error(error.message);
45
+ return 1;
46
+ }
47
+ throw error;
48
+ }
49
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import yaml from 'js-yaml';
5
+ import type { CliGrConfig, GroupConfig, ToolConfig, ItemEntry } from './types.js';
6
+
7
+ const CONFIG_FILENAME = '.cligr.yml';
8
+
9
+ export class ConfigError extends Error {
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = 'ConfigError';
13
+ }
14
+ }
15
+
16
+ export class ConfigLoader {
17
+ private configPath: string;
18
+
19
+ constructor(configPath?: string) {
20
+ if (configPath) {
21
+ // User provided explicit path
22
+ this.configPath = path.resolve(configPath);
23
+ } else {
24
+ // Auto-detect: home dir first, then current dir
25
+ const homeDirConfig = path.join(os.homedir(), CONFIG_FILENAME);
26
+ const currentDirConfig = path.resolve(CONFIG_FILENAME);
27
+
28
+ if (fs.existsSync(homeDirConfig)) {
29
+ this.configPath = homeDirConfig;
30
+ } else if (fs.existsSync(currentDirConfig)) {
31
+ this.configPath = currentDirConfig;
32
+ } else {
33
+ // Store home dir as default, will error in load()
34
+ this.configPath = homeDirConfig;
35
+ }
36
+ }
37
+ }
38
+
39
+ load(): CliGrConfig {
40
+ if (!fs.existsSync(this.configPath)) {
41
+ throw new ConfigError(
42
+ `Config file not found. Looking for:\n` +
43
+ ` - ${path.join(os.homedir(), CONFIG_FILENAME)}\n` +
44
+ ` - ${path.resolve(CONFIG_FILENAME)}`
45
+ );
46
+ }
47
+
48
+ const content = fs.readFileSync(this.configPath, 'utf-8');
49
+ let config: unknown;
50
+
51
+ try {
52
+ config = yaml.load(content);
53
+ } catch (err) {
54
+ throw new ConfigError(`Invalid YAML: ${(err as Error).message}`);
55
+ }
56
+
57
+ return this.validate(config);
58
+ }
59
+
60
+ private validate(config: unknown): CliGrConfig {
61
+ if (!config || typeof config !== 'object') {
62
+ throw new ConfigError('Config must be an object');
63
+ }
64
+
65
+ const cfg = config as Record<string, unknown>;
66
+
67
+ if (!cfg.groups || typeof cfg.groups !== 'object') {
68
+ throw new ConfigError('Config must have a "groups" object');
69
+ }
70
+
71
+ // Validate each group's items
72
+ for (const [groupName, group] of Object.entries(cfg.groups as Record<string, unknown>)) {
73
+ if (group && typeof group === 'object') {
74
+ const groupObj = group as Record<string, unknown>;
75
+ this.validateItems(groupObj.items, groupName);
76
+ }
77
+ }
78
+
79
+ return cfg as unknown as CliGrConfig;
80
+ }
81
+
82
+ private validateItems(items: unknown, groupName: string): void {
83
+ if (!items || typeof items !== 'object' || Array.isArray(items)) {
84
+ throw new ConfigError(
85
+ `Group "${groupName}": items must be an object with named entries, e.g.:\n` +
86
+ ' items:\n' +
87
+ ' serviceName: "value1,value2"'
88
+ );
89
+ }
90
+
91
+ const seenNames = new Set<string>();
92
+
93
+ for (const [name, value] of Object.entries(items as Record<string, unknown>)) {
94
+ if (typeof value !== 'string') {
95
+ throw new ConfigError(`Group "${groupName}": item "${name}" must have a string value`);
96
+ }
97
+
98
+ if (seenNames.has(name)) {
99
+ throw new ConfigError(
100
+ `Group "${groupName}": duplicate item name "${name}". ` +
101
+ `Item names must be unique within a group.`
102
+ );
103
+ }
104
+ seenNames.add(name);
105
+ }
106
+ }
107
+
108
+ private normalizeItems(items: Record<string, string>): ItemEntry[] {
109
+ return Object.entries(items).map(([name, value]) => ({
110
+ name,
111
+ value
112
+ }));
113
+ }
114
+
115
+ getGroup(name: string): { config: GroupConfig; items: ItemEntry[]; tool: string | null; toolTemplate: string | null; params: Record<string, string> } {
116
+ const config = this.load();
117
+ const group = config.groups[name];
118
+
119
+ if (!group) {
120
+ const available = Object.keys(config.groups).join(', ');
121
+ throw new ConfigError(`Unknown group: ${name}. Available: ${available}`);
122
+ }
123
+
124
+ // Normalize items to ItemEntry[]
125
+ const items = this.normalizeItems(group.items);
126
+
127
+ // Resolve tool
128
+ let toolTemplate: string | null = null;
129
+ let tool: string | null = null;
130
+
131
+ if (config.tools && config.tools[group.tool]) {
132
+ toolTemplate = config.tools[group.tool].cmd;
133
+ tool = group.tool;
134
+ } else {
135
+ tool = null;
136
+ toolTemplate = null;
137
+ }
138
+
139
+ const params = group.params || {};
140
+
141
+ return { config: group, items, tool, toolTemplate, params };
142
+ }
143
+
144
+ listGroups(): string[] {
145
+ const config = this.load();
146
+ return Object.keys(config.groups);
147
+ }
148
+ }
@@ -0,0 +1,26 @@
1
+ export interface ToolConfig {
2
+ cmd: string;
3
+ }
4
+
5
+ export interface ItemEntry {
6
+ name: string; // the key from config (e.g., "nginxService1")
7
+ value: string; // the value string (e.g., "nginx,8080")
8
+ }
9
+
10
+ export interface GroupConfig {
11
+ tool: string;
12
+ restart?: 'yes' | 'no' | 'unless-stopped';
13
+ params?: Record<string, string>;
14
+ items: Record<string, string>;
15
+ }
16
+
17
+ export interface CliGrConfig {
18
+ tools?: Record<string, ToolConfig>;
19
+ groups: Record<string, GroupConfig>;
20
+ }
21
+
22
+ export interface ProcessItem {
23
+ name: string;
24
+ args: string[];
25
+ fullCmd: string;
26
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { upCommand } from './commands/up.js';
4
+ import { lsCommand } from './commands/ls.js';
5
+ import { configCommand } from './commands/config.js';
6
+ import { groupsCommand } from './commands/groups.js';
7
+
8
+ async function main(): Promise<void> {
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args.length === 0) {
12
+ printUsage();
13
+ process.exit(1);
14
+ return;
15
+ }
16
+
17
+ const [firstArg, ...rest] = args;
18
+ let verbose = false;
19
+
20
+ // Check if this is a known command
21
+ const knownCommands = ['config', 'up', 'ls', 'groups'];
22
+
23
+ if (knownCommands.includes(firstArg)) {
24
+ // It's a command
25
+ const command = firstArg;
26
+ let groupName: string | undefined;
27
+
28
+ // Parse flags for commands that support them
29
+ if (command === 'groups') {
30
+ // groups command supports -v/--verbose flags
31
+ const flagIndex = rest.findIndex(arg => arg === '-v' || arg === '--verbose');
32
+ if (flagIndex !== -1) {
33
+ verbose = true;
34
+ // Remove the flag from rest so it's not treated as a group name
35
+ rest.splice(flagIndex, 1);
36
+ }
37
+ }
38
+ groupName = rest[0];
39
+
40
+ // config and groups commands don't require group name
41
+ if (command !== 'config' && command !== 'groups' && !groupName) {
42
+ console.error('Error: group name required');
43
+ printUsage();
44
+ process.exit(1);
45
+ return;
46
+ }
47
+
48
+ let exitCode = 0;
49
+
50
+ switch (command) {
51
+ case 'config':
52
+ exitCode = await configCommand();
53
+ break;
54
+ case 'up':
55
+ exitCode = await upCommand(groupName!);
56
+ break;
57
+ case 'ls':
58
+ exitCode = await lsCommand(groupName!);
59
+ break;
60
+ case 'groups':
61
+ exitCode = await groupsCommand(verbose);
62
+ break;
63
+ }
64
+
65
+ process.exit(exitCode);
66
+ } else {
67
+ // Treat as a group name - run up command
68
+ const exitCode = await upCommand(firstArg);
69
+ process.exit(exitCode);
70
+ }
71
+ }
72
+
73
+ function printUsage(): void {
74
+ console.log(`
75
+ Usage: cligr <group> | <command> [options]
76
+
77
+ Commands:
78
+ config Open config file in editor
79
+ ls <group> List all items in the group
80
+ groups [-v|--verbose] List all groups
81
+
82
+ Options:
83
+ -v, --verbose Show detailed group information
84
+
85
+ Examples:
86
+ cligr test1 Start all processes in test1 group
87
+ cligr config
88
+ cligr ls test1
89
+ cligr groups
90
+ cligr groups -v
91
+ `);
92
+ }
93
+
94
+ main().catch((err) => {
95
+ console.error(err);
96
+ process.exit(2);
97
+ });