@proletariat/cli 0.3.46 → 0.3.48

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 (58) hide show
  1. package/bin/validate-better-sqlite3.cjs +55 -0
  2. package/dist/commands/caffeinate/index.d.ts +10 -0
  3. package/dist/commands/caffeinate/index.js +64 -0
  4. package/dist/commands/caffeinate/start.d.ts +14 -0
  5. package/dist/commands/caffeinate/start.js +86 -0
  6. package/dist/commands/caffeinate/status.d.ts +10 -0
  7. package/dist/commands/caffeinate/status.js +55 -0
  8. package/dist/commands/caffeinate/stop.d.ts +10 -0
  9. package/dist/commands/caffeinate/stop.js +47 -0
  10. package/dist/commands/commit.js +10 -8
  11. package/dist/commands/config/index.js +2 -3
  12. package/dist/commands/init.js +9 -1
  13. package/dist/commands/orchestrator/attach.d.ts +1 -0
  14. package/dist/commands/orchestrator/attach.js +104 -24
  15. package/dist/commands/orchestrator/index.js +2 -2
  16. package/dist/commands/orchestrator/start.d.ts +13 -1
  17. package/dist/commands/orchestrator/start.js +115 -34
  18. package/dist/commands/orchestrator/status.d.ts +1 -0
  19. package/dist/commands/orchestrator/status.js +68 -22
  20. package/dist/commands/orchestrator/stop.d.ts +1 -0
  21. package/dist/commands/orchestrator/stop.js +50 -13
  22. package/dist/commands/session/attach.js +55 -9
  23. package/dist/commands/session/poke.js +1 -1
  24. package/dist/commands/work/index.js +8 -0
  25. package/dist/commands/work/linear.d.ts +24 -0
  26. package/dist/commands/work/linear.js +195 -0
  27. package/dist/commands/work/review.d.ts +45 -0
  28. package/dist/commands/work/review.js +401 -0
  29. package/dist/commands/work/spawn.js +28 -19
  30. package/dist/commands/work/start.js +12 -2
  31. package/dist/hooks/init.js +26 -5
  32. package/dist/lib/caffeinate.d.ts +64 -0
  33. package/dist/lib/caffeinate.js +146 -0
  34. package/dist/lib/database/native-validation.d.ts +21 -0
  35. package/dist/lib/database/native-validation.js +49 -0
  36. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  37. package/dist/lib/execution/codex-adapter.js +148 -0
  38. package/dist/lib/execution/index.d.ts +1 -0
  39. package/dist/lib/execution/index.js +1 -0
  40. package/dist/lib/execution/runners.js +56 -6
  41. package/dist/lib/external-issues/index.d.ts +1 -1
  42. package/dist/lib/external-issues/index.js +1 -1
  43. package/dist/lib/external-issues/linear.d.ts +37 -0
  44. package/dist/lib/external-issues/linear.js +198 -0
  45. package/dist/lib/external-issues/types.d.ts +67 -0
  46. package/dist/lib/external-issues/types.js +41 -0
  47. package/dist/lib/init/index.d.ts +4 -0
  48. package/dist/lib/init/index.js +11 -1
  49. package/dist/lib/machine-config.d.ts +1 -0
  50. package/dist/lib/machine-config.js +6 -3
  51. package/dist/lib/mcp/tools/work.js +36 -0
  52. package/dist/lib/pmo/storage/actions.js +3 -3
  53. package/dist/lib/pmo/storage/base.js +85 -6
  54. package/dist/lib/pmo/storage/epics.js +1 -1
  55. package/dist/lib/pmo/storage/tickets.js +2 -2
  56. package/dist/lib/pmo/storage/types.d.ts +2 -1
  57. package/oclif.manifest.json +4158 -3651
  58. 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
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Caffeinate extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,64 @@
1
+ import { Command } from '@oclif/core';
2
+ import { styles } from '../../lib/styles.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
+ import { FlagResolver } from '../../lib/flags/index.js';
6
+ import { isMacOS } from '../../lib/caffeinate.js';
7
+ export default class Caffeinate extends Command {
8
+ static description = 'Manage caffeinate to keep macOS awake';
9
+ static examples = [
10
+ '<%= config.bin %> caffeinate',
11
+ '<%= config.bin %> caffeinate start',
12
+ '<%= config.bin %> caffeinate status',
13
+ '<%= config.bin %> caffeinate stop',
14
+ '<%= config.bin %> caffeinate start --duration 3600',
15
+ '<%= config.bin %> caffeinate start --display',
16
+ ];
17
+ static flags = {
18
+ ...machineOutputFlags,
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(Caffeinate);
22
+ const jsonMode = shouldOutputJson(flags);
23
+ if (!isMacOS()) {
24
+ if (jsonMode) {
25
+ this.log(JSON.stringify({
26
+ type: 'error',
27
+ error: { code: 'UNSUPPORTED_PLATFORM', message: `caffeinate is only supported on macOS (current platform: ${process.platform})` },
28
+ }, null, 2));
29
+ this.exit(1);
30
+ }
31
+ this.error(`caffeinate is only supported on macOS (current platform: ${process.platform})`);
32
+ }
33
+ const menuChoices = [
34
+ { name: 'Check caffeinate status', value: 'status', command: 'prlt caffeinate status --json' },
35
+ { name: 'Start caffeinate', value: 'start', command: 'prlt caffeinate start --json' },
36
+ { name: 'Stop caffeinate', value: 'stop', command: 'prlt caffeinate stop --json' },
37
+ { name: 'Exit', value: 'exit', command: 'prlt caffeinate --exit' },
38
+ ];
39
+ const resolver = new FlagResolver({
40
+ commandName: 'caffeinate',
41
+ baseCommand: 'prlt caffeinate',
42
+ jsonMode,
43
+ flags,
44
+ });
45
+ resolver.addPrompt({
46
+ flagName: 'action',
47
+ type: 'list',
48
+ message: 'What would you like to do?',
49
+ choices: () => menuChoices,
50
+ skipAutoCommand: true,
51
+ });
52
+ if (!jsonMode) {
53
+ this.log('');
54
+ this.log(styles.header('Caffeinate'));
55
+ this.log('');
56
+ }
57
+ const resolved = await resolver.resolve();
58
+ const action = resolved.action;
59
+ if (!action || action === 'exit') {
60
+ return;
61
+ }
62
+ await this.config.runCommand(`caffeinate:${action}`, []);
63
+ }
64
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CaffeinateStart extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ duration: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ display: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ idle: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ system: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,86 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { styles } from '../../lib/styles.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
+ import { isMacOS, startCaffeinate, getStatus } from '../../lib/caffeinate.js';
6
+ export default class CaffeinateStart extends Command {
7
+ static description = 'Start caffeinate to prevent macOS from sleeping';
8
+ static examples = [
9
+ '<%= config.bin %> caffeinate start',
10
+ '<%= config.bin %> caffeinate start --duration 3600',
11
+ '<%= config.bin %> caffeinate start --display',
12
+ ];
13
+ static flags = {
14
+ ...machineOutputFlags,
15
+ duration: Flags.integer({
16
+ char: 't',
17
+ description: 'Duration in seconds (default: indefinite)',
18
+ }),
19
+ display: Flags.boolean({
20
+ char: 'd',
21
+ description: 'Prevent display from sleeping (adds -d flag)',
22
+ default: false,
23
+ }),
24
+ idle: Flags.boolean({
25
+ char: 'i',
26
+ description: 'Prevent idle sleep (adds -i flag)',
27
+ default: false,
28
+ }),
29
+ system: Flags.boolean({
30
+ char: 's',
31
+ description: 'Prevent system sleep (adds -s flag)',
32
+ default: false,
33
+ }),
34
+ };
35
+ async run() {
36
+ const { flags } = await this.parse(CaffeinateStart);
37
+ const jsonMode = shouldOutputJson(flags);
38
+ if (!isMacOS()) {
39
+ if (jsonMode) {
40
+ this.log(JSON.stringify({
41
+ type: 'error',
42
+ error: { code: 'UNSUPPORTED_PLATFORM', message: `caffeinate is only supported on macOS (current platform: ${process.platform})` },
43
+ }, null, 2));
44
+ this.exit(1);
45
+ }
46
+ this.error(`caffeinate is only supported on macOS (current platform: ${process.platform})`);
47
+ }
48
+ // Build caffeinate flags
49
+ const caffeinateFlags = [];
50
+ if (flags.display)
51
+ caffeinateFlags.push('-d');
52
+ if (flags.idle)
53
+ caffeinateFlags.push('-i');
54
+ if (flags.system)
55
+ caffeinateFlags.push('-s');
56
+ // Check if already running (idempotent)
57
+ const { running, state: existingState } = getStatus();
58
+ if (running && existingState) {
59
+ if (jsonMode) {
60
+ this.log(JSON.stringify({
61
+ type: 'success',
62
+ result: { ...existingState, status: 'already_running' },
63
+ }, null, 2));
64
+ return;
65
+ }
66
+ this.log(`\n${styles.warning('caffeinate is already running')} ${styles.muted(`(PID: ${existingState.pid})`)}\n`);
67
+ return;
68
+ }
69
+ const state = startCaffeinate(caffeinateFlags, flags.duration);
70
+ if (jsonMode) {
71
+ this.log(JSON.stringify({
72
+ type: 'success',
73
+ result: { ...state, status: 'started' },
74
+ }, null, 2));
75
+ return;
76
+ }
77
+ this.log(`\n${styles.success('caffeinate started')} ${styles.muted(`(PID: ${state.pid})`)}`);
78
+ if (state.duration) {
79
+ this.log(styles.muted(` Duration: ${state.duration}s`));
80
+ }
81
+ if (state.flags.length > 0) {
82
+ this.log(styles.muted(` Flags: ${state.flags.join(' ')}`));
83
+ }
84
+ this.log('');
85
+ }
86
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CaffeinateStatus extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,55 @@
1
+ import { Command } from '@oclif/core';
2
+ import { styles } from '../../lib/styles.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
+ import { isMacOS, getStatus } from '../../lib/caffeinate.js';
6
+ export default class CaffeinateStatus extends Command {
7
+ static description = 'Check if caffeinate is running';
8
+ static examples = [
9
+ '<%= config.bin %> caffeinate status',
10
+ ];
11
+ static flags = {
12
+ ...machineOutputFlags,
13
+ };
14
+ async run() {
15
+ const { flags } = await this.parse(CaffeinateStatus);
16
+ const jsonMode = shouldOutputJson(flags);
17
+ if (!isMacOS()) {
18
+ if (jsonMode) {
19
+ this.log(JSON.stringify({
20
+ type: 'error',
21
+ error: { code: 'UNSUPPORTED_PLATFORM', message: `caffeinate is only supported on macOS (current platform: ${process.platform})` },
22
+ }, null, 2));
23
+ this.exit(1);
24
+ }
25
+ this.error(`caffeinate is only supported on macOS (current platform: ${process.platform})`);
26
+ }
27
+ const { running, state } = getStatus();
28
+ if (jsonMode) {
29
+ this.log(JSON.stringify({
30
+ type: 'success',
31
+ result: {
32
+ running,
33
+ ...(state ? { pid: state.pid, startedAt: state.startedAt, flags: state.flags, duration: state.duration ?? null } : {}),
34
+ },
35
+ }, null, 2));
36
+ return;
37
+ }
38
+ this.log(`\n${styles.header('Caffeinate Status')}`);
39
+ this.log('─'.repeat(40));
40
+ if (running && state) {
41
+ this.log(`${styles.success('Running')} ${styles.muted(`(PID: ${state.pid})`)}`);
42
+ this.log(styles.muted(` Started: ${state.startedAt}`));
43
+ if (state.flags.length > 0) {
44
+ this.log(styles.muted(` Flags: ${state.flags.join(' ')}`));
45
+ }
46
+ if (state.duration) {
47
+ this.log(styles.muted(` Duration: ${state.duration}s`));
48
+ }
49
+ }
50
+ else {
51
+ this.log(styles.muted('Not running'));
52
+ }
53
+ this.log('');
54
+ }
55
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CaffeinateStop extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,47 @@
1
+ import { Command } from '@oclif/core';
2
+ import { styles } from '../../lib/styles.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
5
+ import { isMacOS, stopCaffeinate, getStatus } from '../../lib/caffeinate.js';
6
+ export default class CaffeinateStop extends Command {
7
+ static description = 'Stop the managed caffeinate process';
8
+ static examples = [
9
+ '<%= config.bin %> caffeinate stop',
10
+ ];
11
+ static flags = {
12
+ ...machineOutputFlags,
13
+ };
14
+ async run() {
15
+ const { flags } = await this.parse(CaffeinateStop);
16
+ const jsonMode = shouldOutputJson(flags);
17
+ if (!isMacOS()) {
18
+ if (jsonMode) {
19
+ this.log(JSON.stringify({
20
+ type: 'error',
21
+ error: { code: 'UNSUPPORTED_PLATFORM', message: `caffeinate is only supported on macOS (current platform: ${process.platform})` },
22
+ }, null, 2));
23
+ this.exit(1);
24
+ }
25
+ this.error(`caffeinate is only supported on macOS (current platform: ${process.platform})`);
26
+ }
27
+ // Get current state for reporting
28
+ const { state } = getStatus();
29
+ const stopped = stopCaffeinate();
30
+ if (jsonMode) {
31
+ this.log(JSON.stringify({
32
+ type: 'success',
33
+ result: {
34
+ stopped,
35
+ pid: state?.pid ?? null,
36
+ },
37
+ }, null, 2));
38
+ return;
39
+ }
40
+ if (stopped) {
41
+ this.log(`\n${styles.success('caffeinate stopped')} ${styles.muted(`(PID: ${state?.pid})`)}\n`);
42
+ }
43
+ else {
44
+ this.log(`\n${styles.muted('caffeinate is not running')}\n`);
45
+ }
46
+ }
47
+ }
@@ -438,23 +438,25 @@ export default class Commit extends PromptCommand {
438
438
  }
439
439
  }
440
440
  // Check if there are staged changes
441
+ let hasStagedChanges = true;
441
442
  try {
442
443
  const staged = execSync('git diff --cached --name-only', {
443
444
  encoding: 'utf-8',
444
445
  stdio: ['pipe', 'pipe', 'pipe'],
445
446
  }).trim();
446
- if (!staged) {
447
- this.error('No staged changes to commit.\n\n' +
448
- 'Stage your changes first:\n' +
449
- ' git add <files>\n' +
450
- ' git add -A\n\n' +
451
- 'Or use --all flag:\n' +
452
- ' prlt commit --all "your message"');
453
- }
447
+ hasStagedChanges = staged.length > 0;
454
448
  }
455
449
  catch {
456
450
  // Ignore errors checking staged changes
457
451
  }
452
+ if (!hasStagedChanges) {
453
+ this.error('No staged changes to commit.\n\n' +
454
+ 'Stage your changes first:\n' +
455
+ ' git add <files>\n' +
456
+ ' git add -A\n\n' +
457
+ 'Or use --all flag:\n' +
458
+ ' prlt commit --all "your message"');
459
+ }
458
460
  // Create commit
459
461
  try {
460
462
  execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
@@ -77,10 +77,9 @@ export default class Config extends PromptCommand {
77
77
  }
78
78
  // Handle --list or --json flag without --setting (just show config)
79
79
  // Also handle non-TTY mode without explicit flags - output config as readable list
80
- const isExplicitJsonMode = (flags.json === true || flags.machine === true);
81
- const shouldShowConfigList = flags.list || (isExplicitJsonMode && !flags.setting) || (isNonTTY() && !flags.setting && !flags.set?.length);
80
+ const shouldShowConfigList = flags.list || (jsonMode && !flags.setting) || (isNonTTY() && !flags.setting && !flags.set?.length);
82
81
  if (shouldShowConfigList) {
83
- if (isExplicitJsonMode) {
82
+ if (jsonMode) {
84
83
  outputSuccessAsJson({
85
84
  terminal: {
86
85
  app: config.terminal.app,
@@ -2,7 +2,7 @@ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import * as path from 'node:path';
4
4
  import * as fs from 'node:fs';
5
- import { promptForHQName, promptForHQLocation, initializeHQ, showNextSteps, validateHQLocation } from '../lib/init/index.js';
5
+ import { promptForHQName, promptForHQLocation, initializeHQ, showNextSteps, validateHQLocation, isHQNameTaken, } from '../lib/init/index.js';
6
6
  import { promptForAgentsWithTheme } from '../lib/agents/index.js';
7
7
  import { promptForRepositories } from '../lib/repos/index.js';
8
8
  import { promptForPMOSetup, machineOutputFlags } from '../lib/pmo/index.js';
@@ -90,6 +90,14 @@ export default class Init extends Command {
90
90
  outputPromptAsJson(buildPromptConfig('input', 'name', 'Enter a name for your headquarters:', undefined, undefined), createMetadata('init', flags));
91
91
  }
92
92
  const hqName = flags.name;
93
+ // Check if HQ name is already in use
94
+ if (hqName && isHQNameTaken(hqName)) {
95
+ this.outputJson({
96
+ success: false,
97
+ error: `HQ name "${hqName}" is already in use on this machine. Pick another name.`,
98
+ });
99
+ this.exit(1);
100
+ }
93
101
  const hqPath = flags.path || path.resolve(`./${hqName}-hq`);
94
102
  // Validate HQ path is not inside a git repo
95
103
  if (!validateHQLocation(hqPath)) {
@@ -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,17 @@ 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 { ORCHESTRATOR_SESSION_NAME } from './start.js';
12
+ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
13
+ import { findHQRoot } from '../../lib/workspace.js';
14
+ import { getHeadquartersNameFromPath } from '../../lib/machine-config.js';
15
+ import { loadExecutionConfig, shouldUseControlMode, buildTmuxAttachCommand } from '../../lib/execution/index.js';
16
+ import { buildOrchestratorSessionName, findRunningOrchestratorSessions } from './start.js';
12
17
  /**
13
18
  * Detect the terminal emulator from environment variables.
14
19
  * Returns a terminal app name suitable for AppleScript tab creation,
@@ -24,8 +29,6 @@ export function detectTerminalApp() {
24
29
  return null;
25
30
  }
26
31
  const termProgram = process.env.TERM_PROGRAM;
27
- if (!termProgram)
28
- return null;
29
32
  switch (termProgram) {
30
33
  case 'iTerm.app':
31
34
  return 'iTerm';
@@ -35,9 +38,13 @@ export function detectTerminalApp() {
35
38
  return 'Terminal';
36
39
  case 'WezTerm':
37
40
  return 'WezTerm';
38
- default:
39
- return null;
40
41
  }
42
+ // TERM_PROGRAM is overwritten to 'tmux' inside tmux sessions.
43
+ // Fall back to vars that persist through tmux to detect the outer terminal.
44
+ if (process.env.LC_TERMINAL === 'iTerm2' || process.env.ITERM_SESSION_ID) {
45
+ return 'iTerm';
46
+ }
47
+ return null;
41
48
  }
42
49
  export default class OrchestratorAttach extends PromptCommand {
43
50
  static description = 'Attach to the running orchestrator tmux session';
@@ -48,6 +55,10 @@ export default class OrchestratorAttach extends PromptCommand {
48
55
  ];
49
56
  static flags = {
50
57
  ...machineOutputFlags,
58
+ name: Flags.string({
59
+ char: 'n',
60
+ description: 'Name of the orchestrator session to attach to (default: main)',
61
+ }),
51
62
  'new-tab': Flags.boolean({
52
63
  description: 'Open in a new terminal tab instead of attaching in the current terminal',
53
64
  default: false,
@@ -66,22 +77,55 @@ export default class OrchestratorAttach extends PromptCommand {
66
77
  async run() {
67
78
  const { flags } = await this.parse(OrchestratorAttach);
68
79
  const jsonMode = shouldOutputJson(flags);
69
- // Check if orchestrator session exists
70
80
  const hostSessions = getHostTmuxSessionNames();
71
- if (!hostSessions.includes(ORCHESTRATOR_SESSION_NAME)) {
72
- if (jsonMode) {
73
- outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running. Start it with: prlt orchestrator start', createMetadata('orchestrator attach', flags));
81
+ // Resolve session name: try HQ-scoped first, fall back to discovery
82
+ let sessionName;
83
+ const hqPath = findHQRoot(process.cwd());
84
+ if (hqPath) {
85
+ const hqName = getHeadquartersNameFromPath(hqPath);
86
+ sessionName = buildOrchestratorSessionName(hqName, flags.name || 'main');
87
+ if (!hostSessions.includes(sessionName)) {
88
+ sessionName = undefined; // Not running for this HQ
89
+ }
90
+ }
91
+ // If not in HQ or session not found, discover running orchestrator sessions
92
+ if (!sessionName) {
93
+ const runningSessions = findRunningOrchestratorSessions(hostSessions);
94
+ if (runningSessions.length === 0) {
95
+ if (jsonMode) {
96
+ outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running. Start it with: prlt orchestrator start', createMetadata('orchestrator attach', flags));
97
+ return;
98
+ }
99
+ this.log('');
100
+ this.log(styles.warning('Orchestrator is not running.'));
101
+ this.log(styles.muted('Start it with: prlt orchestrator start'));
102
+ this.log('');
74
103
  return;
75
104
  }
76
- this.log('');
77
- this.log(styles.warning('Orchestrator is not running.'));
78
- this.log(styles.muted('Start it with: prlt orchestrator start'));
79
- this.log('');
105
+ else if (runningSessions.length === 1) {
106
+ sessionName = runningSessions[0];
107
+ }
108
+ else {
109
+ // Multiple sessions — let user pick
110
+ const { session } = await this.prompt([{
111
+ type: 'list',
112
+ name: 'session',
113
+ message: 'Multiple orchestrator sessions found. Select one to attach:',
114
+ choices: runningSessions.map(s => ({
115
+ name: s,
116
+ value: s,
117
+ command: `prlt orchestrator attach --name "${s}" --json`,
118
+ })),
119
+ }], jsonMode ? { flags, commandName: 'orchestrator attach' } : null);
120
+ sessionName = session;
121
+ }
122
+ }
123
+ if (!sessionName) {
80
124
  return;
81
125
  }
82
126
  if (jsonMode) {
83
127
  outputSuccessAsJson({
84
- sessionId: ORCHESTRATOR_SESSION_NAME,
128
+ sessionId: sessionName,
85
129
  status: 'attaching',
86
130
  }, createMetadata('orchestrator attach', flags));
87
131
  return;
@@ -93,7 +137,27 @@ export default class OrchestratorAttach extends PromptCommand {
93
137
  this.log(styles.warning('--terminal has no effect without --new-tab. Ignoring.'));
94
138
  }
95
139
  this.log('');
96
- this.log(styles.info(`Attaching to orchestrator session: ${ORCHESTRATOR_SESSION_NAME}`));
140
+ this.log(styles.info(`Attaching to orchestrator session: ${sessionName}`));
141
+ // Determine if we should use tmux control mode (-u -CC) for iTerm
142
+ let useControlMode = false;
143
+ try {
144
+ const workspaceInfo = getWorkspaceInfo();
145
+ const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
146
+ const db = new Database(dbPath);
147
+ try {
148
+ const config = loadExecutionConfig(db);
149
+ const termApp = detectTerminalApp();
150
+ if (termApp === 'iTerm') {
151
+ useControlMode = shouldUseControlMode('iTerm', config.tmux.controlMode);
152
+ }
153
+ }
154
+ finally {
155
+ db.close();
156
+ }
157
+ }
158
+ catch {
159
+ // Not in a workspace or DB not available - fall back to no control mode
160
+ }
97
161
  if (flags['new-tab']) {
98
162
  // Determine terminal app: explicit flag > auto-detect > error
99
163
  const terminalApp = flags.terminal ?? detectTerminalApp();
@@ -102,26 +166,39 @@ export default class OrchestratorAttach extends PromptCommand {
102
166
  this.log(styles.muted('Falling back to direct tmux attach in current terminal.'));
103
167
  this.log(styles.muted('Tip: Use --terminal <app> to specify your terminal (iTerm, Terminal, Ghostty).'));
104
168
  this.log('');
105
- this.attachInCurrentTerminal();
169
+ this.attachInCurrentTerminal(useControlMode, sessionName);
106
170
  return;
107
171
  }
108
- await this.openInNewTab(terminalApp);
172
+ await this.openInNewTab(terminalApp, useControlMode, sessionName);
109
173
  }
110
174
  else {
111
- this.attachInCurrentTerminal();
175
+ this.attachInCurrentTerminal(useControlMode, sessionName);
112
176
  }
113
177
  }
114
- attachInCurrentTerminal() {
178
+ attachInCurrentTerminal(useControlMode, sessionName) {
115
179
  try {
116
- execSync(`tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`, { stdio: 'inherit' });
180
+ // Set mouse mode based on attach type:
181
+ // - Plain terminal: mouse on (enables scroll in tmux; hold Shift/Option to bypass)
182
+ // - iTerm -CC: mouse off (iTerm handles scrolling natively)
183
+ const mouseMode = useControlMode ? 'off' : 'on';
184
+ try {
185
+ execSync(`tmux set-option -t "${sessionName}" mouse ${mouseMode}`, { stdio: 'pipe' });
186
+ }
187
+ catch {
188
+ // Non-fatal: mouse mode is a convenience, don't block attach
189
+ }
190
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
191
+ execSync(`${tmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
117
192
  }
118
193
  catch {
119
- this.error(`Failed to attach to orchestrator session "${ORCHESTRATOR_SESSION_NAME}"`);
194
+ this.error(`Failed to attach to orchestrator session "${sessionName}"`);
120
195
  }
121
196
  }
122
- async openInNewTab(terminalApp) {
197
+ async openInNewTab(terminalApp, useControlMode, sessionName) {
123
198
  const title = 'Orchestrator';
124
- const attachCmd = `tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`;
199
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
200
+ const attachCmd = `${tmuxAttach} -t "${sessionName}"`;
201
+ const mouseMode = useControlMode ? 'off' : 'on';
125
202
  const baseDir = path.join(os.homedir(), '.proletariat', 'scripts');
126
203
  fs.mkdirSync(baseDir, { recursive: true });
127
204
  const scriptPath = path.join(baseDir, `attach-orch-${Date.now()}.sh`);
@@ -130,7 +207,10 @@ export default class OrchestratorAttach extends PromptCommand {
130
207
  echo -ne "\\033]0;${title}\\007"
131
208
  echo -ne "\\033]1;${title}\\007"
132
209
 
133
- echo "Attaching to: ${ORCHESTRATOR_SESSION_NAME}"
210
+ # Set mouse mode before attaching
211
+ tmux set-option -t "${sessionName}" mouse ${mouseMode} 2>/dev/null || true
212
+
213
+ echo "Attaching to: ${sessionName}"
134
214
  ${attachCmd}
135
215
 
136
216
  # Clean up