@proletariat/cli 0.3.46 → 0.3.47
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/bin/validate-better-sqlite3.cjs +55 -0
- package/dist/commands/orchestrator/attach.d.ts +1 -0
- package/dist/commands/orchestrator/attach.js +43 -13
- package/dist/commands/orchestrator/index.js +2 -2
- package/dist/commands/orchestrator/start.d.ts +13 -1
- package/dist/commands/orchestrator/start.js +96 -25
- package/dist/commands/orchestrator/status.d.ts +1 -0
- package/dist/commands/orchestrator/status.js +10 -5
- package/dist/commands/orchestrator/stop.d.ts +1 -0
- package/dist/commands/orchestrator/stop.js +9 -4
- package/dist/commands/session/attach.js +32 -9
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/review.d.ts +45 -0
- package/dist/commands/work/review.js +401 -0
- package/dist/hooks/init.js +18 -5
- package/dist/lib/database/native-validation.d.ts +21 -0
- package/dist/lib/database/native-validation.js +49 -0
- package/dist/lib/execution/runners.js +10 -4
- package/dist/lib/mcp/tools/work.js +36 -0
- package/oclif.manifest.json +2834 -2653
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_NODE_MAJORS = [20, 22, 23, 24, 25]
|
|
4
|
+
|
|
5
|
+
function parseNodeMajor(version) {
|
|
6
|
+
const match = /^v?(\d+)/.exec(version)
|
|
7
|
+
return match ? Number.parseInt(match[1], 10) : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function runtimeInfo() {
|
|
11
|
+
return {
|
|
12
|
+
nodeVersion: process.version,
|
|
13
|
+
nodeMajor: parseNodeMajor(process.version),
|
|
14
|
+
abi: process.versions.modules,
|
|
15
|
+
platform: process.platform,
|
|
16
|
+
arch: process.arch,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildMessage(error, context) {
|
|
21
|
+
const info = runtimeInfo()
|
|
22
|
+
const nodeMajorHint = info.nodeMajor === null || SUPPORTED_NODE_MAJORS.includes(info.nodeMajor)
|
|
23
|
+
? ''
|
|
24
|
+
: `\n- Unsupported Node major for this CLI: ${info.nodeMajor} (supported: ${SUPPORTED_NODE_MAJORS.join(', ')})`
|
|
25
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
`better-sqlite3 native module validation failed (${context}).`,
|
|
29
|
+
`Runtime: node ${info.nodeVersion} (ABI ${info.abi}) on ${info.platform}-${info.arch}.${nodeMajorHint}`,
|
|
30
|
+
`Load error: ${reason}`,
|
|
31
|
+
'',
|
|
32
|
+
'Fix steps:',
|
|
33
|
+
'1. Rebuild native bindings for this runtime: npm rebuild better-sqlite3',
|
|
34
|
+
'2. Verify runtime architecture: node -p "process.platform + \'-\' + process.arch + \' abi=\' + process.versions.modules"',
|
|
35
|
+
'3. Reinstall with your active Node version if needed: npm uninstall -g @proletariat/cli && npm install -g @proletariat/cli',
|
|
36
|
+
].join('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validate(context) {
|
|
40
|
+
try {
|
|
41
|
+
const Database = require('better-sqlite3')
|
|
42
|
+
const db = new Database(':memory:')
|
|
43
|
+
db.pragma('foreign_keys = ON')
|
|
44
|
+
db.close()
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(buildMessage(error, context))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
validate('postinstall')
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
@@ -9,6 +9,7 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
9
9
|
static description: string;
|
|
10
10
|
static examples: string[];
|
|
11
11
|
static flags: {
|
|
12
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
'new-tab': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
terminal: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
'current-terminal': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -3,12 +3,15 @@ import { execSync } from 'node:child_process';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
6
7
|
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
7
8
|
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
8
9
|
import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
9
10
|
import { styles } from '../../lib/styles.js';
|
|
10
11
|
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
11
|
-
import {
|
|
12
|
+
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
13
|
+
import { loadExecutionConfig, shouldUseControlMode, buildTmuxAttachCommand } from '../../lib/execution/index.js';
|
|
14
|
+
import { buildOrchestratorSessionName } from './start.js';
|
|
12
15
|
/**
|
|
13
16
|
* Detect the terminal emulator from environment variables.
|
|
14
17
|
* Returns a terminal app name suitable for AppleScript tab creation,
|
|
@@ -48,6 +51,10 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
48
51
|
];
|
|
49
52
|
static flags = {
|
|
50
53
|
...machineOutputFlags,
|
|
54
|
+
name: Flags.string({
|
|
55
|
+
char: 'n',
|
|
56
|
+
description: 'Name of the orchestrator session to attach to (default: main)',
|
|
57
|
+
}),
|
|
51
58
|
'new-tab': Flags.boolean({
|
|
52
59
|
description: 'Open in a new terminal tab instead of attaching in the current terminal',
|
|
53
60
|
default: false,
|
|
@@ -66,9 +73,10 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
66
73
|
async run() {
|
|
67
74
|
const { flags } = await this.parse(OrchestratorAttach);
|
|
68
75
|
const jsonMode = shouldOutputJson(flags);
|
|
76
|
+
const sessionName = buildOrchestratorSessionName(flags.name || 'main');
|
|
69
77
|
// Check if orchestrator session exists
|
|
70
78
|
const hostSessions = getHostTmuxSessionNames();
|
|
71
|
-
if (!hostSessions.includes(
|
|
79
|
+
if (!hostSessions.includes(sessionName)) {
|
|
72
80
|
if (jsonMode) {
|
|
73
81
|
outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running. Start it with: prlt orchestrator start', createMetadata('orchestrator attach', flags));
|
|
74
82
|
return;
|
|
@@ -81,7 +89,7 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
81
89
|
}
|
|
82
90
|
if (jsonMode) {
|
|
83
91
|
outputSuccessAsJson({
|
|
84
|
-
sessionId:
|
|
92
|
+
sessionId: sessionName,
|
|
85
93
|
status: 'attaching',
|
|
86
94
|
}, createMetadata('orchestrator attach', flags));
|
|
87
95
|
return;
|
|
@@ -93,7 +101,27 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
93
101
|
this.log(styles.warning('--terminal has no effect without --new-tab. Ignoring.'));
|
|
94
102
|
}
|
|
95
103
|
this.log('');
|
|
96
|
-
this.log(styles.info(`Attaching to orchestrator session: ${
|
|
104
|
+
this.log(styles.info(`Attaching to orchestrator session: ${sessionName}`));
|
|
105
|
+
// Determine if we should use tmux control mode (-u -CC) for iTerm
|
|
106
|
+
let useControlMode = false;
|
|
107
|
+
try {
|
|
108
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
109
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
110
|
+
const db = new Database(dbPath);
|
|
111
|
+
try {
|
|
112
|
+
const config = loadExecutionConfig(db);
|
|
113
|
+
const termApp = detectTerminalApp();
|
|
114
|
+
if (termApp === 'iTerm') {
|
|
115
|
+
useControlMode = shouldUseControlMode('iTerm', config.tmux.controlMode);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
db.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Not in a workspace or DB not available - fall back to no control mode
|
|
124
|
+
}
|
|
97
125
|
if (flags['new-tab']) {
|
|
98
126
|
// Determine terminal app: explicit flag > auto-detect > error
|
|
99
127
|
const terminalApp = flags.terminal ?? detectTerminalApp();
|
|
@@ -102,26 +130,28 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
102
130
|
this.log(styles.muted('Falling back to direct tmux attach in current terminal.'));
|
|
103
131
|
this.log(styles.muted('Tip: Use --terminal <app> to specify your terminal (iTerm, Terminal, Ghostty).'));
|
|
104
132
|
this.log('');
|
|
105
|
-
this.attachInCurrentTerminal();
|
|
133
|
+
this.attachInCurrentTerminal(useControlMode, sessionName);
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
108
|
-
await this.openInNewTab(terminalApp);
|
|
136
|
+
await this.openInNewTab(terminalApp, useControlMode, sessionName);
|
|
109
137
|
}
|
|
110
138
|
else {
|
|
111
|
-
this.attachInCurrentTerminal();
|
|
139
|
+
this.attachInCurrentTerminal(useControlMode, sessionName);
|
|
112
140
|
}
|
|
113
141
|
}
|
|
114
|
-
attachInCurrentTerminal() {
|
|
142
|
+
attachInCurrentTerminal(useControlMode, sessionName) {
|
|
115
143
|
try {
|
|
116
|
-
|
|
144
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode);
|
|
145
|
+
execSync(`${tmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
|
|
117
146
|
}
|
|
118
147
|
catch {
|
|
119
|
-
this.error(`Failed to attach to orchestrator session "${
|
|
148
|
+
this.error(`Failed to attach to orchestrator session "${sessionName}"`);
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
|
-
async openInNewTab(terminalApp) {
|
|
151
|
+
async openInNewTab(terminalApp, useControlMode, sessionName) {
|
|
123
152
|
const title = 'Orchestrator';
|
|
124
|
-
const
|
|
153
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode);
|
|
154
|
+
const attachCmd = `${tmuxAttach} -t "${sessionName}"`;
|
|
125
155
|
const baseDir = path.join(os.homedir(), '.proletariat', 'scripts');
|
|
126
156
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
127
157
|
const scriptPath = path.join(baseDir, `attach-orch-${Date.now()}.sh`);
|
|
@@ -130,7 +160,7 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
130
160
|
echo -ne "\\033]0;${title}\\007"
|
|
131
161
|
echo -ne "\\033]1;${title}\\007"
|
|
132
162
|
|
|
133
|
-
echo "Attaching to: ${
|
|
163
|
+
echo "Attaching to: ${sessionName}"
|
|
134
164
|
${attachCmd}
|
|
135
165
|
|
|
136
166
|
# Clean up
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
2
2
|
import { shouldOutputJson } from '../../lib/prompt-json.js';
|
|
3
3
|
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findRunningOrchestratorSessions } from './start.js';
|
|
5
5
|
export default class Orchestrator extends PMOCommand {
|
|
6
6
|
static description = 'Manage the orchestrator agent (start, attach, status, stop)';
|
|
7
7
|
static examples = [
|
|
@@ -22,7 +22,7 @@ export default class Orchestrator extends PMOCommand {
|
|
|
22
22
|
const jsonModeConfig = shouldOutputJson(flags) ? { flags, commandName: 'orchestrator' } : null;
|
|
23
23
|
// Check if orchestrator is currently running to offer contextual options
|
|
24
24
|
const hostSessions = getHostTmuxSessionNames();
|
|
25
|
-
const isRunning = hostSessions.
|
|
25
|
+
const isRunning = findRunningOrchestratorSessions(hostSessions).length > 0;
|
|
26
26
|
// When running, show "Attach to running session" first since that's the likely intent
|
|
27
27
|
const choices = isRunning
|
|
28
28
|
? [
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Build orchestrator tmux session name.
|
|
4
|
+
* Default: 'prlt-orchestrator-main'
|
|
5
|
+
* With --name: 'prlt-orchestrator-{name}'
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildOrchestratorSessionName(name?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Find running orchestrator session(s) by prefix match.
|
|
10
|
+
* Returns all tmux session names that start with 'prlt-orchestrator-'.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findRunningOrchestratorSessions(hostSessions: string[]): string[];
|
|
3
13
|
export default class OrchestratorStart extends PromptCommand {
|
|
4
14
|
static description: string;
|
|
5
15
|
static examples: string[];
|
|
@@ -9,7 +19,9 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
9
19
|
executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
20
|
'skip-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
21
|
sandboxed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
23
|
background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
foreground: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
25
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
26
|
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
27
|
};
|
|
@@ -11,8 +11,26 @@ import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
|
|
|
11
11
|
import { runExecution } from '../../lib/execution/runners.js';
|
|
12
12
|
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
13
13
|
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
14
|
-
import { loadExecutionConfig, getTerminalApp,
|
|
15
|
-
|
|
14
|
+
import { loadExecutionConfig, getTerminalApp, getShell, detectShell, detectTerminalApp, } from '../../lib/execution/config.js';
|
|
15
|
+
/**
|
|
16
|
+
* Build orchestrator tmux session name.
|
|
17
|
+
* Default: 'prlt-orchestrator-main'
|
|
18
|
+
* With --name: 'prlt-orchestrator-{name}'
|
|
19
|
+
*/
|
|
20
|
+
export function buildOrchestratorSessionName(name = 'main') {
|
|
21
|
+
const safeName = name
|
|
22
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
23
|
+
.replace(/-+/g, '-')
|
|
24
|
+
.replace(/^-|-$/g, '');
|
|
25
|
+
return `prlt-orchestrator-${safeName || 'main'}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find running orchestrator session(s) by prefix match.
|
|
29
|
+
* Returns all tmux session names that start with 'prlt-orchestrator-'.
|
|
30
|
+
*/
|
|
31
|
+
export function findRunningOrchestratorSessions(hostSessions) {
|
|
32
|
+
return hostSessions.filter(s => s.startsWith('prlt-orchestrator-'));
|
|
33
|
+
}
|
|
16
34
|
export default class OrchestratorStart extends PromptCommand {
|
|
17
35
|
static description = 'Start the orchestrator agent in a tmux session';
|
|
18
36
|
static examples = [
|
|
@@ -47,36 +65,50 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
47
65
|
default: false,
|
|
48
66
|
exclusive: ['skip-permissions'],
|
|
49
67
|
}),
|
|
68
|
+
name: Flags.string({
|
|
69
|
+
char: 'n',
|
|
70
|
+
description: 'Name for the orchestrator session (default: main)',
|
|
71
|
+
}),
|
|
50
72
|
background: Flags.boolean({
|
|
51
73
|
char: 'b',
|
|
52
74
|
description: 'Start detached (don\'t open terminal tab)',
|
|
53
75
|
default: false,
|
|
76
|
+
exclusive: ['foreground'],
|
|
77
|
+
}),
|
|
78
|
+
foreground: Flags.boolean({
|
|
79
|
+
char: 'f',
|
|
80
|
+
description: 'Attach to the tmux session in the current terminal (blocking)',
|
|
81
|
+
default: false,
|
|
82
|
+
exclusive: ['background'],
|
|
54
83
|
}),
|
|
55
84
|
};
|
|
56
85
|
async run() {
|
|
57
86
|
const { flags } = await this.parse(OrchestratorStart);
|
|
58
87
|
const jsonMode = shouldOutputJson(flags);
|
|
88
|
+
const orchestratorName = flags.name || 'main';
|
|
89
|
+
const sessionName = buildOrchestratorSessionName(orchestratorName);
|
|
59
90
|
// Check if orchestrator is already running
|
|
60
91
|
const hostSessions = getHostTmuxSessionNames();
|
|
61
|
-
if (hostSessions.includes(
|
|
92
|
+
if (hostSessions.includes(sessionName)) {
|
|
62
93
|
if (jsonMode) {
|
|
63
|
-
outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${
|
|
94
|
+
outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${sessionName}). Use "prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}" to reattach.`, createMetadata('orchestrator start', flags));
|
|
64
95
|
return;
|
|
65
96
|
}
|
|
66
97
|
this.log('');
|
|
67
|
-
this.log(styles.warning(`Orchestrator is already running (session: ${
|
|
98
|
+
this.log(styles.warning(`Orchestrator is already running (session: ${sessionName})`));
|
|
68
99
|
this.log('');
|
|
100
|
+
const attachArgs = flags.name ? ['--name', flags.name] : [];
|
|
69
101
|
const { choice } = await this.prompt([{
|
|
70
102
|
type: 'list',
|
|
71
103
|
name: 'choice',
|
|
72
104
|
message: 'What would you like to do?',
|
|
73
105
|
choices: [
|
|
74
|
-
{ name: 'Attach to running orchestrator', value: 'attach', command:
|
|
106
|
+
{ name: 'Attach to running orchestrator', value: 'attach', command: `prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''} --json` },
|
|
75
107
|
{ name: 'Cancel', value: 'cancel' },
|
|
76
108
|
],
|
|
77
109
|
}], jsonMode ? { flags, commandName: 'orchestrator start' } : null);
|
|
78
110
|
if (choice === 'attach') {
|
|
79
|
-
await this.config.runCommand('orchestrator:attach',
|
|
111
|
+
await this.config.runCommand('orchestrator:attach', attachArgs);
|
|
80
112
|
}
|
|
81
113
|
return;
|
|
82
114
|
}
|
|
@@ -169,12 +201,12 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
169
201
|
}
|
|
170
202
|
}
|
|
171
203
|
// Build execution context
|
|
172
|
-
// Use ticketId='prlt', actionName='orchestrator', agentName=
|
|
173
|
-
// so buildSessionName produces 'prlt-orchestrator-
|
|
204
|
+
// Use ticketId='prlt', actionName='orchestrator', agentName=orchestratorName
|
|
205
|
+
// so buildSessionName produces 'prlt-orchestrator-{name}'
|
|
174
206
|
const context = {
|
|
175
207
|
ticketId: 'prlt',
|
|
176
208
|
ticketTitle: 'Orchestrator',
|
|
177
|
-
agentName:
|
|
209
|
+
agentName: orchestratorName,
|
|
178
210
|
agentDir: hqPath,
|
|
179
211
|
worktreePath: hqPath,
|
|
180
212
|
branch: 'main',
|
|
@@ -202,19 +234,54 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
202
234
|
catch {
|
|
203
235
|
// Ignore config loading errors, use defaults
|
|
204
236
|
}
|
|
205
|
-
//
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
// Auto-detect shell (never prompt for orchestrator)
|
|
238
|
+
if (db) {
|
|
239
|
+
executionConfig.shell = await getShell(db);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
executionConfig.shell = detectShell() || 'zsh';
|
|
243
|
+
}
|
|
244
|
+
// Determine display mode
|
|
245
|
+
let displayMode;
|
|
246
|
+
if (flags.background) {
|
|
247
|
+
displayMode = 'background';
|
|
248
|
+
}
|
|
249
|
+
else if (flags.foreground) {
|
|
250
|
+
displayMode = 'foreground';
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const displayChoices = [
|
|
254
|
+
{ name: 'New terminal tab — opens attached to the tmux session', value: 'terminal', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --json` },
|
|
255
|
+
{ name: 'Current session — attach to tmux here (foreground, blocking)', value: 'foreground', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --foreground --json` },
|
|
256
|
+
{ name: 'Background — start detached, attach later', value: 'background', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --background --json` },
|
|
257
|
+
];
|
|
258
|
+
const displayMessage = 'How do you want to view the orchestrator?';
|
|
259
|
+
if (jsonMode) {
|
|
260
|
+
outputPromptAsJson(buildPromptConfig('list', 'displayMode', displayMessage, displayChoices), createMetadata('orchestrator start', flags));
|
|
261
|
+
return;
|
|
209
262
|
}
|
|
210
|
-
|
|
263
|
+
const { displayMode: selectedMode } = await this.prompt([{
|
|
264
|
+
type: 'list',
|
|
265
|
+
name: 'displayMode',
|
|
266
|
+
message: displayMessage,
|
|
267
|
+
choices: displayChoices,
|
|
268
|
+
}], jsonMode ? { flags, commandName: 'orchestrator start' } : null);
|
|
269
|
+
displayMode = selectedMode;
|
|
270
|
+
}
|
|
271
|
+
// For 'terminal' display mode, auto-detect terminal app
|
|
272
|
+
if (displayMode === 'terminal') {
|
|
273
|
+
if (db) {
|
|
211
274
|
executionConfig.terminal.app = await getTerminalApp(db);
|
|
212
275
|
}
|
|
213
|
-
if (!hasShellPreference(db)) {
|
|
214
|
-
executionConfig.shell = await promptShellPreference(db);
|
|
215
|
-
}
|
|
216
276
|
else {
|
|
217
|
-
|
|
277
|
+
const detected = detectTerminalApp();
|
|
278
|
+
if (detected) {
|
|
279
|
+
executionConfig.terminal.app = detected;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Can't detect terminal and no db to prompt — fall back to foreground
|
|
283
|
+
displayMode = 'foreground';
|
|
284
|
+
}
|
|
218
285
|
}
|
|
219
286
|
}
|
|
220
287
|
// Show what we're doing
|
|
@@ -223,14 +290,17 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
223
290
|
this.log(styles.muted(` Starting orchestrator...`));
|
|
224
291
|
this.log(styles.muted(` Executor: ${selectedExecutor}`));
|
|
225
292
|
this.log(styles.muted(` Permission mode: ${sandboxed ? 'sandboxed' : 'skip-permissions'}`));
|
|
293
|
+
this.log(styles.muted(` Display mode: ${displayMode}`));
|
|
226
294
|
this.log(styles.muted(` Directory: ${hqPath}`));
|
|
295
|
+
if (orchestratorName !== 'main') {
|
|
296
|
+
this.log(styles.muted(` Name: ${orchestratorName}`));
|
|
297
|
+
}
|
|
227
298
|
if (actionPrompt) {
|
|
228
299
|
this.log(styles.muted(` Prompt: "${actionPrompt.substring(0, 60)}${actionPrompt.length > 60 ? '...' : ''}"`));
|
|
229
300
|
}
|
|
230
301
|
this.log('');
|
|
231
302
|
}
|
|
232
303
|
// Launch orchestrator
|
|
233
|
-
const displayMode = flags.background ? 'background' : 'terminal';
|
|
234
304
|
const result = await runExecution('host', context, selectedExecutor, executionConfig, {
|
|
235
305
|
displayMode,
|
|
236
306
|
});
|
|
@@ -246,7 +316,7 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
246
316
|
environment: 'host',
|
|
247
317
|
displayMode,
|
|
248
318
|
sandboxed,
|
|
249
|
-
sessionId: result.sessionId ||
|
|
319
|
+
sessionId: result.sessionId || sessionName,
|
|
250
320
|
});
|
|
251
321
|
}
|
|
252
322
|
catch {
|
|
@@ -255,17 +325,18 @@ export default class OrchestratorStart extends PromptCommand {
|
|
|
255
325
|
}
|
|
256
326
|
if (jsonMode) {
|
|
257
327
|
outputSuccessAsJson({
|
|
258
|
-
sessionId: result.sessionId ||
|
|
328
|
+
sessionId: result.sessionId || sessionName,
|
|
259
329
|
executor: selectedExecutor,
|
|
260
330
|
sandboxed,
|
|
261
331
|
displayMode,
|
|
262
332
|
directory: hqPath,
|
|
333
|
+
name: orchestratorName,
|
|
263
334
|
}, createMetadata('orchestrator start', flags));
|
|
264
335
|
}
|
|
265
|
-
if (
|
|
336
|
+
if (displayMode === 'background') {
|
|
266
337
|
this.log(styles.success(`Orchestrator started in background`));
|
|
267
|
-
this.log(styles.muted(` Session: ${result.sessionId ||
|
|
268
|
-
this.log(styles.muted(` Attach with: prlt orchestrator attach`));
|
|
338
|
+
this.log(styles.muted(` Session: ${result.sessionId || sessionName}`));
|
|
339
|
+
this.log(styles.muted(` Attach with: prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}`));
|
|
269
340
|
}
|
|
270
341
|
else {
|
|
271
342
|
this.log(styles.success(`Orchestrator started`));
|
|
@@ -3,6 +3,7 @@ export default class OrchestratorStatus extends PromptCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
peek: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
lines: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -4,7 +4,7 @@ import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
|
4
4
|
import { shouldOutputJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
5
5
|
import { styles } from '../../lib/styles.js';
|
|
6
6
|
import { getHostTmuxSessionNames, captureTmuxPane } from '../../lib/execution/session-utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { buildOrchestratorSessionName } from './start.js';
|
|
8
8
|
export default class OrchestratorStatus extends PromptCommand {
|
|
9
9
|
static description = 'Check if the orchestrator is running';
|
|
10
10
|
static examples = [
|
|
@@ -14,6 +14,10 @@ export default class OrchestratorStatus extends PromptCommand {
|
|
|
14
14
|
];
|
|
15
15
|
static flags = {
|
|
16
16
|
...machineOutputFlags,
|
|
17
|
+
name: Flags.string({
|
|
18
|
+
char: 'n',
|
|
19
|
+
description: 'Name of the orchestrator session to check (default: main)',
|
|
20
|
+
}),
|
|
17
21
|
peek: Flags.boolean({
|
|
18
22
|
description: 'Show recent output from the orchestrator',
|
|
19
23
|
default: false,
|
|
@@ -26,16 +30,17 @@ export default class OrchestratorStatus extends PromptCommand {
|
|
|
26
30
|
async run() {
|
|
27
31
|
const { flags } = await this.parse(OrchestratorStatus);
|
|
28
32
|
const jsonMode = shouldOutputJson(flags);
|
|
33
|
+
const sessionName = buildOrchestratorSessionName(flags.name || 'main');
|
|
29
34
|
const hostSessions = getHostTmuxSessionNames();
|
|
30
|
-
const isRunning = hostSessions.includes(
|
|
35
|
+
const isRunning = hostSessions.includes(sessionName);
|
|
31
36
|
let recentOutput = null;
|
|
32
37
|
if (isRunning && flags.peek) {
|
|
33
|
-
recentOutput = captureTmuxPane(
|
|
38
|
+
recentOutput = captureTmuxPane(sessionName, flags.lines);
|
|
34
39
|
}
|
|
35
40
|
if (jsonMode) {
|
|
36
41
|
outputSuccessAsJson({
|
|
37
42
|
running: isRunning,
|
|
38
|
-
sessionId: isRunning ?
|
|
43
|
+
sessionId: isRunning ? sessionName : null,
|
|
39
44
|
...(recentOutput !== null && { recentOutput }),
|
|
40
45
|
}, createMetadata('orchestrator status', flags));
|
|
41
46
|
return;
|
|
@@ -43,7 +48,7 @@ export default class OrchestratorStatus extends PromptCommand {
|
|
|
43
48
|
this.log('');
|
|
44
49
|
if (isRunning) {
|
|
45
50
|
this.log(styles.success(`Orchestrator is running`));
|
|
46
|
-
this.log(styles.muted(` Session: ${
|
|
51
|
+
this.log(styles.muted(` Session: ${sessionName}`));
|
|
47
52
|
this.log(styles.muted(` Attach: prlt orchestrator attach`));
|
|
48
53
|
this.log(styles.muted(` Poke: prlt session poke orchestrator "message"`));
|
|
49
54
|
if (recentOutput) {
|
|
@@ -3,6 +3,7 @@ export default class OrchestratorStop extends PromptCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -10,7 +10,7 @@ import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadat
|
|
|
10
10
|
import { styles } from '../../lib/styles.js';
|
|
11
11
|
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
12
12
|
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
13
|
-
import {
|
|
13
|
+
import { buildOrchestratorSessionName } from './start.js';
|
|
14
14
|
export default class OrchestratorStop extends PromptCommand {
|
|
15
15
|
static description = 'Stop the running orchestrator';
|
|
16
16
|
static examples = [
|
|
@@ -19,6 +19,10 @@ export default class OrchestratorStop extends PromptCommand {
|
|
|
19
19
|
];
|
|
20
20
|
static flags = {
|
|
21
21
|
...machineOutputFlags,
|
|
22
|
+
name: Flags.string({
|
|
23
|
+
char: 'n',
|
|
24
|
+
description: 'Name of the orchestrator session to stop (default: main)',
|
|
25
|
+
}),
|
|
22
26
|
force: Flags.boolean({
|
|
23
27
|
char: 'f',
|
|
24
28
|
description: 'Skip confirmation',
|
|
@@ -28,9 +32,10 @@ export default class OrchestratorStop extends PromptCommand {
|
|
|
28
32
|
async run() {
|
|
29
33
|
const { flags } = await this.parse(OrchestratorStop);
|
|
30
34
|
const jsonMode = shouldOutputJson(flags);
|
|
35
|
+
const sessionName = buildOrchestratorSessionName(flags.name || 'main');
|
|
31
36
|
// Check if orchestrator session exists
|
|
32
37
|
const hostSessions = getHostTmuxSessionNames();
|
|
33
|
-
if (!hostSessions.includes(
|
|
38
|
+
if (!hostSessions.includes(sessionName)) {
|
|
34
39
|
if (jsonMode) {
|
|
35
40
|
outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running.', createMetadata('orchestrator stop', flags));
|
|
36
41
|
return;
|
|
@@ -58,7 +63,7 @@ export default class OrchestratorStop extends PromptCommand {
|
|
|
58
63
|
}
|
|
59
64
|
// Kill the tmux session
|
|
60
65
|
try {
|
|
61
|
-
execSync(`tmux kill-session -t "${
|
|
66
|
+
execSync(`tmux kill-session -t "${sessionName}"`, { stdio: 'pipe' });
|
|
62
67
|
}
|
|
63
68
|
catch (error) {
|
|
64
69
|
if (jsonMode) {
|
|
@@ -92,7 +97,7 @@ export default class OrchestratorStop extends PromptCommand {
|
|
|
92
97
|
}
|
|
93
98
|
if (jsonMode) {
|
|
94
99
|
outputSuccessAsJson({
|
|
95
|
-
sessionId:
|
|
100
|
+
sessionId: sessionName,
|
|
96
101
|
status: 'stopped',
|
|
97
102
|
}, createMetadata('orchestrator stop', flags));
|
|
98
103
|
return;
|
|
@@ -6,7 +6,8 @@ import * as os from 'node:os';
|
|
|
6
6
|
import Database from 'better-sqlite3';
|
|
7
7
|
import { styles } from '../../lib/styles.js';
|
|
8
8
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
9
|
-
import { ExecutionStorage } from '../../lib/execution/index.js';
|
|
9
|
+
import { ExecutionStorage, loadExecutionConfig, shouldUseControlMode, buildTmuxAttachCommand } from '../../lib/execution/index.js';
|
|
10
|
+
import { detectTerminalApp } from '../orchestrator/attach.js';
|
|
10
11
|
import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
|
|
11
12
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
12
13
|
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
@@ -106,12 +107,32 @@ export default class SessionAttach extends PMOCommand {
|
|
|
106
107
|
// Attach to the session
|
|
107
108
|
this.log('');
|
|
108
109
|
this.log(styles.info(`Attaching to session: ${selectedSession.sessionId}`));
|
|
110
|
+
// Determine if we should use tmux control mode (-u -CC) for iTerm
|
|
111
|
+
let useControlMode = false;
|
|
112
|
+
try {
|
|
113
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
114
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
115
|
+
const db = new Database(dbPath);
|
|
116
|
+
try {
|
|
117
|
+
const config = loadExecutionConfig(db);
|
|
118
|
+
const termApp = detectTerminalApp();
|
|
119
|
+
if (termApp === 'iTerm') {
|
|
120
|
+
useControlMode = shouldUseControlMode('iTerm', config.tmux.controlMode);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
db.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not in a workspace or DB not available - fall back to no control mode
|
|
129
|
+
}
|
|
109
130
|
// Default to new tab unless --current-terminal is specified
|
|
110
131
|
if (flags['current-terminal']) {
|
|
111
|
-
await this.attachInCurrentTerminal(selectedSession);
|
|
132
|
+
await this.attachInCurrentTerminal(selectedSession, useControlMode);
|
|
112
133
|
}
|
|
113
134
|
else {
|
|
114
|
-
await this.attachInNewTab(selectedSession, flags.terminal);
|
|
135
|
+
await this.attachInNewTab(selectedSession, flags.terminal, useControlMode);
|
|
115
136
|
}
|
|
116
137
|
}
|
|
117
138
|
/**
|
|
@@ -247,13 +268,14 @@ export default class SessionAttach extends PMOCommand {
|
|
|
247
268
|
/**
|
|
248
269
|
* Attach to session in current terminal
|
|
249
270
|
*/
|
|
250
|
-
async attachInCurrentTerminal(session) {
|
|
271
|
+
async attachInCurrentTerminal(session, useControlMode) {
|
|
251
272
|
try {
|
|
273
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode, session.type === 'container');
|
|
252
274
|
if (session.type === 'container' && session.containerId) {
|
|
253
|
-
execSync(`docker exec -it ${session.containerId}
|
|
275
|
+
execSync(`docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`, { stdio: 'inherit' });
|
|
254
276
|
}
|
|
255
277
|
else {
|
|
256
|
-
execSync(
|
|
278
|
+
execSync(`${tmuxAttach} -t "${session.sessionId}"`, { stdio: 'inherit' });
|
|
257
279
|
}
|
|
258
280
|
}
|
|
259
281
|
catch {
|
|
@@ -263,7 +285,7 @@ export default class SessionAttach extends PMOCommand {
|
|
|
263
285
|
/**
|
|
264
286
|
* Attach to session in a new terminal tab
|
|
265
287
|
*/
|
|
266
|
-
async attachInNewTab(session, terminalApp) {
|
|
288
|
+
async attachInNewTab(session, terminalApp, useControlMode) {
|
|
267
289
|
// Build a readable title for the tab
|
|
268
290
|
const title = `${session.ticketId} (${session.agentName})`;
|
|
269
291
|
// Create a script that sets tab title and attaches to tmux
|
|
@@ -271,9 +293,10 @@ export default class SessionAttach extends PMOCommand {
|
|
|
271
293
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
272
294
|
const scriptPath = path.join(baseDir, `attach-${Date.now()}.sh`);
|
|
273
295
|
// Different attach command for container vs host sessions
|
|
296
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode, session.type === 'container');
|
|
274
297
|
const attachCmd = session.type === 'container' && session.containerId
|
|
275
|
-
? `docker exec -it ${session.containerId}
|
|
276
|
-
:
|
|
298
|
+
? `docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`
|
|
299
|
+
: `${tmuxAttach} -t "${session.sessionId}"`;
|
|
277
300
|
const script = `#!/bin/bash
|
|
278
301
|
# Set terminal tab title
|
|
279
302
|
echo -ne "\\033]0;${title}\\007"
|