@proletariat/cli 0.3.45 → 0.3.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +9 -1
- package/dist/commands/orchestrator/attach.js +67 -13
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +345 -275
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +2867 -2380
- package/package.json +2 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
2
|
+
import { colors } from '../../lib/colors.js';
|
|
3
|
+
import { shouldOutputJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
4
|
+
import { LinearClient, isLinearConfigured, loadLinearConfig, } from '../../lib/linear/index.js';
|
|
5
|
+
import { LinearMapper } from '../../lib/linear/mapper.js';
|
|
6
|
+
export default class LinearStatus extends PMOCommand {
|
|
7
|
+
static description = 'Show Linear integration status and connection info';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
...pmoBaseFlags,
|
|
14
|
+
};
|
|
15
|
+
async execute() {
|
|
16
|
+
const { flags } = await this.parse(LinearStatus);
|
|
17
|
+
const jsonMode = shouldOutputJson(flags);
|
|
18
|
+
const db = this.storage.getDatabase();
|
|
19
|
+
if (!isLinearConfigured(db)) {
|
|
20
|
+
if (jsonMode) {
|
|
21
|
+
outputSuccessAsJson({
|
|
22
|
+
configured: false,
|
|
23
|
+
message: 'Linear is not configured. Run "prlt linear auth" to connect.',
|
|
24
|
+
}, createMetadata('linear status', flags));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.log(colors.warning('Linear is not configured'));
|
|
28
|
+
this.log(colors.textMuted('Run "prlt linear auth" to connect your Linear workspace.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const config = loadLinearConfig(db);
|
|
32
|
+
// Verify connection
|
|
33
|
+
let connectionInfo = null;
|
|
34
|
+
let connectionError = null;
|
|
35
|
+
try {
|
|
36
|
+
const client = new LinearClient(config.apiKey);
|
|
37
|
+
connectionInfo = await client.verify();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
connectionError = error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
// Count mapped issues
|
|
43
|
+
const mapper = new LinearMapper(db);
|
|
44
|
+
const mappings = mapper.listMappings();
|
|
45
|
+
if (jsonMode) {
|
|
46
|
+
outputSuccessAsJson({
|
|
47
|
+
configured: true,
|
|
48
|
+
connected: connectionInfo !== null,
|
|
49
|
+
organization: connectionInfo?.organizationName ?? config.organizationName ?? null,
|
|
50
|
+
user: connectionInfo?.userName ?? null,
|
|
51
|
+
email: connectionInfo?.email ?? null,
|
|
52
|
+
defaultTeam: config.defaultTeamKey ?? null,
|
|
53
|
+
mappedIssues: mappings.length,
|
|
54
|
+
error: connectionError,
|
|
55
|
+
}, createMetadata('linear status', flags));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.log(colors.primary('Linear Integration Status'));
|
|
59
|
+
this.log('');
|
|
60
|
+
if (connectionInfo) {
|
|
61
|
+
this.log(` ${colors.success('Connected')}`);
|
|
62
|
+
this.log(colors.textMuted(` Organization: ${connectionInfo.organizationName}`));
|
|
63
|
+
this.log(colors.textMuted(` User: ${connectionInfo.userName} (${connectionInfo.email})`));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.log(` ${colors.error('Connection failed')}`);
|
|
67
|
+
if (connectionError) {
|
|
68
|
+
this.log(colors.textMuted(` Error: ${connectionError}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (config.defaultTeamKey) {
|
|
72
|
+
this.log(colors.textMuted(` Default team: ${config.defaultTeamKey}`));
|
|
73
|
+
}
|
|
74
|
+
this.log('');
|
|
75
|
+
this.log(colors.textMuted(` Mapped issues: ${mappings.length}`));
|
|
76
|
+
if (mappings.length > 0) {
|
|
77
|
+
const recent = mappings.slice(0, 5);
|
|
78
|
+
this.log('');
|
|
79
|
+
this.log(colors.textMuted(' Recent mappings:'));
|
|
80
|
+
for (const m of recent) {
|
|
81
|
+
this.log(colors.textMuted(` ${m.linearIdentifier} → ${m.pmoTicketId}`));
|
|
82
|
+
}
|
|
83
|
+
if (mappings.length > 5) {
|
|
84
|
+
this.log(colors.textMuted(` ... and ${mappings.length - 5} more`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class LinearSyncCommand extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
ticket: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'pr-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'pr-title': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
|
+
import { colors } from '../../lib/colors.js';
|
|
4
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
5
|
+
import { LinearClient, LinearMapper, LinearSync, isLinearConfigured, loadLinearConfig, } from '../../lib/linear/index.js';
|
|
6
|
+
export default class LinearSyncCommand extends PMOCommand {
|
|
7
|
+
static description = 'Sync PMO ticket status and PR links back to Linear';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> # Sync all mapped tickets',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --ticket TKT-001 # Sync a specific ticket',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --pr-url https://github.com/... --ticket TKT-001 # Attach PR to Linear issue',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --dry-run # Preview what would be synced',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...pmoBaseFlags,
|
|
16
|
+
ticket: Flags.string({
|
|
17
|
+
description: 'PMO ticket ID to sync (syncs all if omitted)',
|
|
18
|
+
}),
|
|
19
|
+
'pr-url': Flags.string({
|
|
20
|
+
description: 'PR URL to attach to the Linear issue',
|
|
21
|
+
}),
|
|
22
|
+
'pr-title': Flags.string({
|
|
23
|
+
description: 'PR title for the attachment (defaults to ticket title)',
|
|
24
|
+
}),
|
|
25
|
+
'dry-run': Flags.boolean({
|
|
26
|
+
description: 'Preview what would be synced without making changes',
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async execute() {
|
|
31
|
+
const { flags } = await this.parse(LinearSyncCommand);
|
|
32
|
+
const jsonMode = shouldOutputJson(flags);
|
|
33
|
+
const db = this.storage.getDatabase();
|
|
34
|
+
if (!isLinearConfigured(db)) {
|
|
35
|
+
if (jsonMode) {
|
|
36
|
+
outputErrorAsJson('LINEAR_NOT_CONFIGURED', 'Linear is not configured. Run "prlt linear auth" first.', createMetadata('linear sync', flags));
|
|
37
|
+
this.exit(1);
|
|
38
|
+
}
|
|
39
|
+
this.error('Linear is not configured. Run "prlt linear auth" first.');
|
|
40
|
+
}
|
|
41
|
+
const config = loadLinearConfig(db);
|
|
42
|
+
const client = new LinearClient(config.apiKey);
|
|
43
|
+
const mapper = new LinearMapper(db);
|
|
44
|
+
const sync = new LinearSync(client, mapper);
|
|
45
|
+
// Handle PR link attachment
|
|
46
|
+
if (flags['pr-url'] && flags.ticket) {
|
|
47
|
+
const prTitle = flags['pr-title'] ?? (await this.storage.getTicket(flags.ticket))?.title ?? 'Pull Request';
|
|
48
|
+
if (flags['dry-run']) {
|
|
49
|
+
if (jsonMode) {
|
|
50
|
+
outputSuccessAsJson({
|
|
51
|
+
dryRun: true,
|
|
52
|
+
action: 'attach-pr',
|
|
53
|
+
ticketId: flags.ticket,
|
|
54
|
+
prUrl: flags['pr-url'],
|
|
55
|
+
prTitle,
|
|
56
|
+
}, createMetadata('linear sync', flags));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.log(colors.textMuted(`Would attach PR to ${flags.ticket}: ${flags['pr-url']}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const attached = await sync.syncPRLink(flags.ticket, flags['pr-url'], prTitle);
|
|
63
|
+
if (jsonMode) {
|
|
64
|
+
outputSuccessAsJson({
|
|
65
|
+
action: 'attach-pr',
|
|
66
|
+
ticketId: flags.ticket,
|
|
67
|
+
prUrl: flags['pr-url'],
|
|
68
|
+
synced: attached,
|
|
69
|
+
}, createMetadata('linear sync', flags));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (attached) {
|
|
73
|
+
this.log(colors.success(`PR linked to Linear issue for ${flags.ticket}`));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.log(colors.warning(`No Linear mapping found for ${flags.ticket}`));
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Sync ticket status(es) back to Linear
|
|
81
|
+
if (flags.ticket) {
|
|
82
|
+
// Single ticket sync
|
|
83
|
+
const ticket = await this.storage.getTicket(flags.ticket);
|
|
84
|
+
if (!ticket) {
|
|
85
|
+
if (jsonMode) {
|
|
86
|
+
outputErrorAsJson('TICKET_NOT_FOUND', `Ticket ${flags.ticket} not found.`, createMetadata('linear sync', flags));
|
|
87
|
+
this.exit(1);
|
|
88
|
+
}
|
|
89
|
+
this.error(`Ticket ${flags.ticket} not found.`);
|
|
90
|
+
}
|
|
91
|
+
const mapping = mapper.getByTicketId(flags.ticket);
|
|
92
|
+
if (!mapping) {
|
|
93
|
+
if (jsonMode) {
|
|
94
|
+
outputSuccessAsJson({
|
|
95
|
+
synced: false,
|
|
96
|
+
message: `Ticket ${flags.ticket} has no Linear mapping.`,
|
|
97
|
+
}, createMetadata('linear sync', flags));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.log(colors.warning(`Ticket ${flags.ticket} has no Linear mapping.`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Get Linear states for the team
|
|
104
|
+
const team = await client.getTeamByKey(mapping.linearTeamKey);
|
|
105
|
+
if (!team) {
|
|
106
|
+
if (jsonMode) {
|
|
107
|
+
outputErrorAsJson('TEAM_NOT_FOUND', `Linear team ${mapping.linearTeamKey} not found.`, createMetadata('linear sync', flags));
|
|
108
|
+
this.exit(1);
|
|
109
|
+
}
|
|
110
|
+
this.error(`Linear team ${mapping.linearTeamKey} not found.`);
|
|
111
|
+
}
|
|
112
|
+
const linearStates = await client.listStates(team.id);
|
|
113
|
+
if (flags['dry-run']) {
|
|
114
|
+
if (jsonMode) {
|
|
115
|
+
outputSuccessAsJson({
|
|
116
|
+
dryRun: true,
|
|
117
|
+
action: 'sync-status',
|
|
118
|
+
ticketId: flags.ticket,
|
|
119
|
+
linearIdentifier: mapping.linearIdentifier,
|
|
120
|
+
currentStatus: ticket.statusName,
|
|
121
|
+
statusCategory: ticket.statusCategory,
|
|
122
|
+
}, createMetadata('linear sync', flags));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.log(colors.textMuted(`Would sync ${flags.ticket} (${mapping.linearIdentifier}): status "${ticket.statusName}" (${ticket.statusCategory})`));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const synced = await sync.syncTicketStatus(ticket, linearStates);
|
|
129
|
+
if (jsonMode) {
|
|
130
|
+
outputSuccessAsJson({
|
|
131
|
+
action: 'sync-status',
|
|
132
|
+
ticketId: flags.ticket,
|
|
133
|
+
linearIdentifier: mapping.linearIdentifier,
|
|
134
|
+
synced,
|
|
135
|
+
}, createMetadata('linear sync', flags));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (synced) {
|
|
139
|
+
this.log(colors.success(`Synced ${mapping.linearIdentifier} status to Linear`));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.log(colors.warning(`Could not find matching Linear state for ${ticket.statusCategory}`));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Bulk sync: sync all mapped tickets
|
|
147
|
+
const mappings = mapper.listMappings();
|
|
148
|
+
if (mappings.length === 0) {
|
|
149
|
+
if (jsonMode) {
|
|
150
|
+
outputSuccessAsJson({
|
|
151
|
+
synced: 0,
|
|
152
|
+
message: 'No mapped tickets to sync.',
|
|
153
|
+
}, createMetadata('linear sync', flags));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.log(colors.textMuted('No mapped tickets to sync.'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!jsonMode) {
|
|
160
|
+
this.log(colors.textMuted(`Syncing ${mappings.length} mapped ticket(s) to Linear...`));
|
|
161
|
+
}
|
|
162
|
+
// Group mappings by team to fetch states efficiently
|
|
163
|
+
const teamKeys = [...new Set(mappings.map((m) => m.linearTeamKey))];
|
|
164
|
+
const statesByTeam = {};
|
|
165
|
+
for (const teamKey of teamKeys) {
|
|
166
|
+
// eslint-disable-next-line no-await-in-loop
|
|
167
|
+
const team = await client.getTeamByKey(teamKey);
|
|
168
|
+
if (team) {
|
|
169
|
+
// eslint-disable-next-line no-await-in-loop
|
|
170
|
+
statesByTeam[teamKey] = await client.listStates(team.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (flags['dry-run']) {
|
|
174
|
+
const preview = [];
|
|
175
|
+
for (const mapping of mappings) {
|
|
176
|
+
// eslint-disable-next-line no-await-in-loop
|
|
177
|
+
const ticket = await this.storage.getTicket(mapping.pmoTicketId);
|
|
178
|
+
preview.push({
|
|
179
|
+
ticketId: mapping.pmoTicketId,
|
|
180
|
+
linearIdentifier: mapping.linearIdentifier,
|
|
181
|
+
currentStatus: ticket?.statusName,
|
|
182
|
+
statusCategory: ticket?.statusCategory,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (jsonMode) {
|
|
186
|
+
outputSuccessAsJson({
|
|
187
|
+
dryRun: true,
|
|
188
|
+
action: 'sync-all',
|
|
189
|
+
tickets: preview,
|
|
190
|
+
}, createMetadata('linear sync', flags));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.log('');
|
|
194
|
+
for (const item of preview) {
|
|
195
|
+
this.log(colors.textMuted(` ${item.linearIdentifier} → ${item.currentStatus} (${item.statusCategory})`));
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Perform the sync for each team
|
|
200
|
+
let totalSynced = 0;
|
|
201
|
+
let totalSkipped = 0;
|
|
202
|
+
let totalErrors = 0;
|
|
203
|
+
for (const teamKey of teamKeys) {
|
|
204
|
+
const teamStates = statesByTeam[teamKey];
|
|
205
|
+
if (!teamStates) {
|
|
206
|
+
totalErrors += mappings.filter((m) => m.linearTeamKey === teamKey).length;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const teamMappings = mappings.filter((m) => m.linearTeamKey === teamKey);
|
|
210
|
+
// eslint-disable-next-line no-await-in-loop
|
|
211
|
+
const result = await sync.syncAllStatuses(this.storage, teamMappings, teamStates);
|
|
212
|
+
totalSynced += result.synced;
|
|
213
|
+
totalSkipped += result.skipped;
|
|
214
|
+
totalErrors += result.errors;
|
|
215
|
+
}
|
|
216
|
+
if (jsonMode) {
|
|
217
|
+
outputSuccessAsJson({
|
|
218
|
+
action: 'sync-all',
|
|
219
|
+
synced: totalSynced,
|
|
220
|
+
skipped: totalSkipped,
|
|
221
|
+
errors: totalErrors,
|
|
222
|
+
}, createMetadata('linear sync', flags));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.log('');
|
|
226
|
+
if (totalSynced > 0)
|
|
227
|
+
this.log(colors.success(`Synced: ${totalSynced}`));
|
|
228
|
+
if (totalSkipped > 0)
|
|
229
|
+
this.log(colors.textMuted(`Skipped: ${totalSkipped}`));
|
|
230
|
+
if (totalErrors > 0)
|
|
231
|
+
this.log(colors.error(`Errors: ${totalErrors}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect the terminal emulator from environment variables.
|
|
4
|
+
* Returns a terminal app name suitable for AppleScript tab creation,
|
|
5
|
+
* or null if detection fails or we're in a remote/headless environment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function detectTerminalApp(): string | null;
|
|
2
8
|
export default class OrchestratorAttach extends PromptCommand {
|
|
3
9
|
static description: string;
|
|
4
10
|
static examples: string[];
|
|
5
11
|
static flags: {
|
|
12
|
+
'new-tab': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
terminal: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
14
|
'current-terminal': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
terminal: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
15
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
16
|
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
17
|
};
|
|
11
18
|
run(): Promise<void>;
|
|
19
|
+
private attachInCurrentTerminal;
|
|
12
20
|
private openInNewTab;
|
|
13
21
|
}
|
|
@@ -9,23 +9,58 @@ import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadat
|
|
|
9
9
|
import { styles } from '../../lib/styles.js';
|
|
10
10
|
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
11
11
|
import { ORCHESTRATOR_SESSION_NAME } from './start.js';
|
|
12
|
+
/**
|
|
13
|
+
* Detect the terminal emulator from environment variables.
|
|
14
|
+
* Returns a terminal app name suitable for AppleScript tab creation,
|
|
15
|
+
* or null if detection fails or we're in a remote/headless environment.
|
|
16
|
+
*/
|
|
17
|
+
export function detectTerminalApp() {
|
|
18
|
+
// Remote sessions should never attempt AppleScript/GUI operations
|
|
19
|
+
if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// Headless / no display — skip GUI attempts
|
|
23
|
+
if (process.platform !== 'darwin' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const termProgram = process.env.TERM_PROGRAM;
|
|
27
|
+
if (!termProgram)
|
|
28
|
+
return null;
|
|
29
|
+
switch (termProgram) {
|
|
30
|
+
case 'iTerm.app':
|
|
31
|
+
return 'iTerm';
|
|
32
|
+
case 'ghostty':
|
|
33
|
+
return 'Ghostty';
|
|
34
|
+
case 'Apple_Terminal':
|
|
35
|
+
return 'Terminal';
|
|
36
|
+
case 'WezTerm':
|
|
37
|
+
return 'WezTerm';
|
|
38
|
+
default:
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
12
42
|
export default class OrchestratorAttach extends PromptCommand {
|
|
13
43
|
static description = 'Attach to the running orchestrator tmux session';
|
|
14
44
|
static examples = [
|
|
15
45
|
'<%= config.bin %> <%= command.id %>',
|
|
16
|
-
'<%= config.bin %> <%= command.id %> --
|
|
46
|
+
'<%= config.bin %> <%= command.id %> --new-tab',
|
|
47
|
+
'<%= config.bin %> <%= command.id %> --new-tab --terminal Ghostty',
|
|
17
48
|
];
|
|
18
49
|
static flags = {
|
|
19
50
|
...machineOutputFlags,
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
description: 'Attach in current terminal instead of new tab',
|
|
51
|
+
'new-tab': Flags.boolean({
|
|
52
|
+
description: 'Open in a new terminal tab instead of attaching in the current terminal',
|
|
23
53
|
default: false,
|
|
24
54
|
}),
|
|
25
55
|
terminal: Flags.string({
|
|
26
56
|
char: 't',
|
|
27
|
-
description: 'Terminal app to use (iTerm, Terminal, Ghostty)',
|
|
28
|
-
|
|
57
|
+
description: 'Terminal app to use for new tab (iTerm, Terminal, Ghostty). Auto-detected if not specified.',
|
|
58
|
+
}),
|
|
59
|
+
'current-terminal': Flags.boolean({
|
|
60
|
+
char: 'c',
|
|
61
|
+
description: '[deprecated] Attach in current terminal (this is now the default behavior)',
|
|
62
|
+
hidden: true,
|
|
63
|
+
default: false,
|
|
29
64
|
}),
|
|
30
65
|
};
|
|
31
66
|
async run() {
|
|
@@ -51,18 +86,37 @@ export default class OrchestratorAttach extends PromptCommand {
|
|
|
51
86
|
}, createMetadata('orchestrator attach', flags));
|
|
52
87
|
return;
|
|
53
88
|
}
|
|
89
|
+
if (flags['current-terminal']) {
|
|
90
|
+
this.log(styles.warning('--current-terminal is deprecated. Direct tmux attach is now the default behavior.'));
|
|
91
|
+
}
|
|
92
|
+
if (flags.terminal && !flags['new-tab']) {
|
|
93
|
+
this.log(styles.warning('--terminal has no effect without --new-tab. Ignoring.'));
|
|
94
|
+
}
|
|
54
95
|
this.log('');
|
|
55
96
|
this.log(styles.info(`Attaching to orchestrator session: ${ORCHESTRATOR_SESSION_NAME}`));
|
|
56
|
-
if (flags['
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
97
|
+
if (flags['new-tab']) {
|
|
98
|
+
// Determine terminal app: explicit flag > auto-detect > error
|
|
99
|
+
const terminalApp = flags.terminal ?? detectTerminalApp();
|
|
100
|
+
if (!terminalApp) {
|
|
101
|
+
this.log(styles.warning('Could not detect terminal emulator for new tab.'));
|
|
102
|
+
this.log(styles.muted('Falling back to direct tmux attach in current terminal.'));
|
|
103
|
+
this.log(styles.muted('Tip: Use --terminal <app> to specify your terminal (iTerm, Terminal, Ghostty).'));
|
|
104
|
+
this.log('');
|
|
105
|
+
this.attachInCurrentTerminal();
|
|
106
|
+
return;
|
|
62
107
|
}
|
|
108
|
+
await this.openInNewTab(terminalApp);
|
|
63
109
|
}
|
|
64
110
|
else {
|
|
65
|
-
|
|
111
|
+
this.attachInCurrentTerminal();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
attachInCurrentTerminal() {
|
|
115
|
+
try {
|
|
116
|
+
execSync(`tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`, { stdio: 'inherit' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
this.error(`Failed to attach to orchestrator session "${ORCHESTRATOR_SESSION_NAME}"`);
|
|
66
120
|
}
|
|
67
121
|
}
|
|
68
122
|
async openInNewTab(terminalApp) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
2
2
|
import { shouldOutputJson } from '../../lib/prompt-json.js';
|
|
3
|
+
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
|
|
4
|
+
import { ORCHESTRATOR_SESSION_NAME } from './start.js';
|
|
3
5
|
export default class Orchestrator extends PMOCommand {
|
|
4
6
|
static description = 'Manage the orchestrator agent (start, attach, status, stop)';
|
|
5
7
|
static examples = [
|
|
@@ -18,17 +20,30 @@ export default class Orchestrator extends PMOCommand {
|
|
|
18
20
|
async execute() {
|
|
19
21
|
const { flags } = await this.parse(Orchestrator);
|
|
20
22
|
const jsonModeConfig = shouldOutputJson(flags) ? { flags, commandName: 'orchestrator' } : null;
|
|
23
|
+
// Check if orchestrator is currently running to offer contextual options
|
|
24
|
+
const hostSessions = getHostTmuxSessionNames();
|
|
25
|
+
const isRunning = hostSessions.includes(ORCHESTRATOR_SESSION_NAME);
|
|
26
|
+
// When running, show "Attach to running session" first since that's the likely intent
|
|
27
|
+
const choices = isRunning
|
|
28
|
+
? [
|
|
29
|
+
{ name: 'Attach to running session', value: 'attach', command: 'prlt orchestrator attach --json' },
|
|
30
|
+
{ name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
|
|
31
|
+
{ name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
|
|
32
|
+
{ name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
|
|
33
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
34
|
+
]
|
|
35
|
+
: [
|
|
36
|
+
{ name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
|
|
37
|
+
{ name: 'Attach to orchestrator', value: 'attach', command: 'prlt orchestrator attach --json' },
|
|
38
|
+
{ name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
|
|
39
|
+
{ name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
|
|
40
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
41
|
+
];
|
|
21
42
|
const { action } = await this.prompt([{
|
|
22
43
|
type: 'list',
|
|
23
44
|
name: 'action',
|
|
24
45
|
message: 'Orchestrator - What would you like to do?',
|
|
25
|
-
choices
|
|
26
|
-
{ name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
|
|
27
|
-
{ name: 'Attach to orchestrator', value: 'attach', command: 'prlt orchestrator attach --json' },
|
|
28
|
-
{ name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
|
|
29
|
-
{ name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
|
|
30
|
-
{ name: 'Cancel', value: 'cancel' },
|
|
31
|
-
],
|
|
46
|
+
choices,
|
|
32
47
|
}], jsonModeConfig);
|
|
33
48
|
if (action === 'cancel') {
|
|
34
49
|
return;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PMOCommand } from '../../../lib/pmo/index.js';
|
|
2
|
+
export default class TicketLinkDuplicates extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
ticket: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
original: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
|
|
3
|
+
import { styles } from '../../../lib/styles.js';
|
|
4
|
+
import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../../lib/prompt-json.js';
|
|
5
|
+
export default class TicketLinkDuplicates extends PMOCommand {
|
|
6
|
+
static description = 'Mark a ticket as a duplicate of another';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %> TKT-001 TKT-002',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> TKT-001',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --json',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
ticket: Args.string({
|
|
14
|
+
description: 'Ticket that is a duplicate',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
original: Args.string({
|
|
18
|
+
description: 'Original ticket (that this duplicates)',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
...pmoBaseFlags,
|
|
24
|
+
};
|
|
25
|
+
async execute() {
|
|
26
|
+
const { args, flags } = await this.parse(TicketLinkDuplicates);
|
|
27
|
+
const jsonMode = shouldOutputJson(flags);
|
|
28
|
+
const projectId = await this.requireProject();
|
|
29
|
+
const handleError = (code, message) => {
|
|
30
|
+
if (jsonMode) {
|
|
31
|
+
outputErrorAsJson(code, message, createMetadata('ticket link duplicates', flags));
|
|
32
|
+
this.exit(1);
|
|
33
|
+
}
|
|
34
|
+
this.error(message);
|
|
35
|
+
};
|
|
36
|
+
// Verify the source ticket exists
|
|
37
|
+
const ticket = await this.storage.getTicket(args.ticket);
|
|
38
|
+
if (!ticket) {
|
|
39
|
+
return handleError('TICKET_NOT_FOUND', `Ticket not found: ${args.ticket}`);
|
|
40
|
+
}
|
|
41
|
+
// If original ticket not provided, prompt for selection
|
|
42
|
+
if (!args.original) {
|
|
43
|
+
const tickets = await this.storage.listTickets(projectId);
|
|
44
|
+
const otherTickets = tickets.filter(t => t.id !== args.ticket);
|
|
45
|
+
if (otherTickets.length === 0) {
|
|
46
|
+
return handleError('NO_TICKETS', 'No other tickets to select as original.');
|
|
47
|
+
}
|
|
48
|
+
const projectFlag = flags.project ? ` -P ${flags.project}` : '';
|
|
49
|
+
const choices = otherTickets.map(t => ({
|
|
50
|
+
name: `${t.id} - ${t.title}`,
|
|
51
|
+
value: t.id,
|
|
52
|
+
command: `prlt ticket link duplicates ${args.ticket} ${t.id}${projectFlag} --json`,
|
|
53
|
+
}));
|
|
54
|
+
const message = `Select the original ticket that ${args.ticket} duplicates:`;
|
|
55
|
+
if (jsonMode) {
|
|
56
|
+
outputPromptAsJson(buildPromptConfig('list', 'original', message, choices), createMetadata('ticket link duplicates', flags));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const { selected } = await this.prompt([{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'selected',
|
|
62
|
+
message,
|
|
63
|
+
choices,
|
|
64
|
+
}], null);
|
|
65
|
+
args.original = selected;
|
|
66
|
+
}
|
|
67
|
+
// Verify original ticket exists
|
|
68
|
+
const originalTicket = await this.storage.getTicket(args.original);
|
|
69
|
+
if (!originalTicket) {
|
|
70
|
+
return handleError('ORIGINAL_NOT_FOUND', `Original ticket not found: ${args.original}`);
|
|
71
|
+
}
|
|
72
|
+
// Create the duplicates dependency
|
|
73
|
+
try {
|
|
74
|
+
await this.storage.createTicketDependency(args.ticket, args.original, 'duplicates');
|
|
75
|
+
await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
|
|
76
|
+
if (jsonMode) {
|
|
77
|
+
outputSuccessAsJson({
|
|
78
|
+
ticketId: args.ticket,
|
|
79
|
+
originalTicketId: args.original,
|
|
80
|
+
type: 'duplicates',
|
|
81
|
+
}, createMetadata('ticket link duplicates', flags));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.log(styles.success(`\n${args.ticket} marked as duplicate of ${args.original}`));
|
|
85
|
+
this.log(styles.muted(` ${ticket.title}`));
|
|
86
|
+
this.log(styles.muted(` duplicates: ${originalTicket.title}`));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
90
|
+
return handleError('ALREADY_EXISTS', 'Duplicates dependency already exists.');
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -55,6 +55,8 @@ export default class TicketLink extends PMOCommand {
|
|
|
55
55
|
const projectFlag = flags.project ? ` -P ${flags.project}` : '';
|
|
56
56
|
const menuChoices = [
|
|
57
57
|
{ name: 'Add blocker', value: 'block', command: `prlt ticket link block ${args.ticket}${projectFlag} --json` },
|
|
58
|
+
{ name: 'Add related ticket', value: 'relates', command: `prlt ticket link relates ${args.ticket}${projectFlag} --json` },
|
|
59
|
+
{ name: 'Mark as duplicate', value: 'duplicates', command: `prlt ticket link duplicates ${args.ticket}${projectFlag} --json` },
|
|
58
60
|
{ name: 'View links', value: 'view', command: `prlt link list ${args.ticket}${projectFlag} --json` },
|
|
59
61
|
{ name: 'Remove link', value: 'remove', command: `prlt link remove ${args.ticket}${projectFlag} --json` },
|
|
60
62
|
{ name: 'Cancel', value: 'cancel', command: '' },
|
|
@@ -81,6 +83,18 @@ export default class TicketLink extends PMOCommand {
|
|
|
81
83
|
await cmd.run();
|
|
82
84
|
break;
|
|
83
85
|
}
|
|
86
|
+
case 'relates': {
|
|
87
|
+
const { default: RelatesCommand } = await import('./relates.js');
|
|
88
|
+
const cmd = new RelatesCommand([args.ticket, ...(flags.project ? ['-P', flags.project] : [])], this.config);
|
|
89
|
+
await cmd.run();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'duplicates': {
|
|
93
|
+
const { default: DuplicatesCommand } = await import('./duplicates.js');
|
|
94
|
+
const cmd = new DuplicatesCommand([args.ticket, ...(flags.project ? ['-P', flags.project] : [])], this.config);
|
|
95
|
+
await cmd.run();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
84
98
|
case 'view': {
|
|
85
99
|
const { default: LinkListCommand } = await import('../../link/list.js');
|
|
86
100
|
const cmd = new LinkListCommand([args.ticket, ...(flags.project ? ['-P', flags.project] : [])], this.config);
|