@proletariat/cli 0.3.44 → 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/agent/list.js +2 -3
- package/dist/commands/agent/login.js +2 -2
- package/dist/commands/agent/rebuild.js +2 -3
- package/dist/commands/agent/shell.js +2 -2
- package/dist/commands/agent/status.js +3 -3
- package/dist/commands/agent/visit.js +2 -2
- 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/staff/list.js +2 -3
- 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 +7 -6
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +79 -23
- package/dist/commands/work/watch.js +2 -2
- package/dist/lib/agents/commands.d.ts +11 -0
- package/dist/lib/agents/commands.js +40 -10
- 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 -271
- package/dist/lib/execution/spawner.js +65 -7
- 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/dist/lib/themes.js +32 -16
- package/oclif.manifest.json +2823 -2336
- package/package.json +2 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
4
|
+
import { colors } from '../../lib/colors.js';
|
|
5
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, outputPromptAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
6
|
+
import { LinearClient, isLinearConfigured, loadLinearConfig, LinearMapper, } from '../../lib/linear/index.js';
|
|
7
|
+
export default class LinearImport extends PMOCommand {
|
|
8
|
+
static description = 'Import Linear issues into PMO as tickets';
|
|
9
|
+
static strict = false; // Allow variadic issue identifier args
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> <%= command.id %> # Interactive: select team and issues',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> ENG-123 ENG-124 # Import specific issues by identifier',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --team ENG # Import from a specific team',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --team ENG --state "In Progress" # Filter by state',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --team ENG --label bug # Filter by label',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --limit 20 --json # Import up to 20 issues, JSON output',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...pmoBaseFlags,
|
|
20
|
+
team: Flags.string({
|
|
21
|
+
char: 't',
|
|
22
|
+
description: 'Linear team key (e.g., ENG)',
|
|
23
|
+
}),
|
|
24
|
+
state: Flags.string({
|
|
25
|
+
description: 'Filter by Linear state name (e.g., "In Progress", "Backlog")',
|
|
26
|
+
}),
|
|
27
|
+
'state-type': Flags.string({
|
|
28
|
+
description: 'Filter by Linear state type',
|
|
29
|
+
options: ['triage', 'backlog', 'unstarted', 'started', 'completed', 'canceled'],
|
|
30
|
+
}),
|
|
31
|
+
label: Flags.string({
|
|
32
|
+
description: 'Filter by Linear label name',
|
|
33
|
+
}),
|
|
34
|
+
assignee: Flags.string({
|
|
35
|
+
description: 'Filter by assignee name or "me"',
|
|
36
|
+
}),
|
|
37
|
+
cycle: Flags.string({
|
|
38
|
+
description: 'Filter by cycle ID',
|
|
39
|
+
}),
|
|
40
|
+
limit: Flags.integer({
|
|
41
|
+
char: 'n',
|
|
42
|
+
description: 'Maximum number of issues to import',
|
|
43
|
+
default: 50,
|
|
44
|
+
}),
|
|
45
|
+
all: Flags.boolean({
|
|
46
|
+
char: 'a',
|
|
47
|
+
description: 'Import all matching issues without interactive selection',
|
|
48
|
+
default: false,
|
|
49
|
+
}),
|
|
50
|
+
'dry-run': Flags.boolean({
|
|
51
|
+
description: 'Preview issues that would be imported without creating tickets',
|
|
52
|
+
default: false,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
async execute() {
|
|
56
|
+
const { flags, argv } = await this.parse(LinearImport);
|
|
57
|
+
const jsonMode = shouldOutputJson(flags);
|
|
58
|
+
const db = this.storage.getDatabase();
|
|
59
|
+
// Check Linear is configured
|
|
60
|
+
if (!isLinearConfigured(db)) {
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
outputErrorAsJson('LINEAR_NOT_CONFIGURED', 'Linear is not configured. Run "prlt linear auth" first.', createMetadata('linear import', flags));
|
|
63
|
+
this.exit(1);
|
|
64
|
+
}
|
|
65
|
+
this.error('Linear is not configured. Run "prlt linear auth" first.');
|
|
66
|
+
}
|
|
67
|
+
const config = loadLinearConfig(db);
|
|
68
|
+
const client = new LinearClient(config.apiKey);
|
|
69
|
+
const mapper = new LinearMapper(db);
|
|
70
|
+
// Parse explicit issue identifiers from args
|
|
71
|
+
const issueIdentifiers = argv;
|
|
72
|
+
// Get project for import
|
|
73
|
+
const projectId = await this.requireProject({
|
|
74
|
+
jsonMode: jsonMode ? {
|
|
75
|
+
flags,
|
|
76
|
+
commandName: 'linear import',
|
|
77
|
+
baseCommand: `${this.config.bin} linear import`,
|
|
78
|
+
} : undefined,
|
|
79
|
+
});
|
|
80
|
+
// Get the project's workflow statuses for mapping
|
|
81
|
+
const workflow = await this.storage.getProjectWorkflow(projectId);
|
|
82
|
+
if (!workflow) {
|
|
83
|
+
if (jsonMode) {
|
|
84
|
+
outputErrorAsJson('NO_WORKFLOW', 'Project has no workflow configured.', createMetadata('linear import', flags));
|
|
85
|
+
this.exit(1);
|
|
86
|
+
}
|
|
87
|
+
this.error('Project has no workflow configured.');
|
|
88
|
+
}
|
|
89
|
+
const statuses = await this.storage.listStatuses(workflow.id);
|
|
90
|
+
let issues;
|
|
91
|
+
if (issueIdentifiers.length > 0) {
|
|
92
|
+
// Fetch specific issues by identifier
|
|
93
|
+
this.log(colors.textMuted(`Fetching ${issueIdentifiers.length} issue(s) from Linear...`));
|
|
94
|
+
issues = [];
|
|
95
|
+
for (const id of issueIdentifiers) {
|
|
96
|
+
// eslint-disable-next-line no-await-in-loop
|
|
97
|
+
const issue = await client.getIssueByIdentifier(id);
|
|
98
|
+
if (issue) {
|
|
99
|
+
issues.push(issue);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.log(colors.warning(`Issue not found: ${id}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Build filter from flags
|
|
108
|
+
const filter = {
|
|
109
|
+
teamKey: flags.team ?? config.defaultTeamKey,
|
|
110
|
+
stateName: flags.state,
|
|
111
|
+
stateType: flags['state-type'],
|
|
112
|
+
labelName: flags.label,
|
|
113
|
+
assigneeMe: flags.assignee?.toLowerCase() === 'me' ? true : undefined,
|
|
114
|
+
assigneeId: flags.assignee && flags.assignee.toLowerCase() !== 'me' ? flags.assignee : undefined,
|
|
115
|
+
cycleId: flags.cycle,
|
|
116
|
+
limit: flags.limit,
|
|
117
|
+
};
|
|
118
|
+
// If no team specified and no default, prompt
|
|
119
|
+
if (!filter.teamKey && !filter.teamId) {
|
|
120
|
+
const teams = await client.listTeams();
|
|
121
|
+
if (teams.length === 0) {
|
|
122
|
+
if (jsonMode) {
|
|
123
|
+
outputErrorAsJson('NO_TEAMS', 'No teams found in your Linear workspace.', createMetadata('linear import', flags));
|
|
124
|
+
this.exit(1);
|
|
125
|
+
}
|
|
126
|
+
this.error('No teams found in your Linear workspace.');
|
|
127
|
+
}
|
|
128
|
+
if (teams.length === 1) {
|
|
129
|
+
filter.teamKey = teams[0].key;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (jsonMode) {
|
|
133
|
+
const teamChoices = teams.map((t) => ({
|
|
134
|
+
name: `${t.name} (${t.key})`,
|
|
135
|
+
value: t.key,
|
|
136
|
+
}));
|
|
137
|
+
outputPromptAsJson(buildPromptConfig('list', 'teamKey', 'Select a team to import from:', teamChoices), createMetadata('linear import', flags));
|
|
138
|
+
}
|
|
139
|
+
const teamChoices = teams.map((t) => ({
|
|
140
|
+
name: `${t.name} (${t.key})`,
|
|
141
|
+
value: t.key,
|
|
142
|
+
}));
|
|
143
|
+
const { teamKey } = await inquirer.prompt([{
|
|
144
|
+
type: 'list',
|
|
145
|
+
name: 'teamKey',
|
|
146
|
+
message: 'Select a team to import from:',
|
|
147
|
+
choices: teamChoices,
|
|
148
|
+
}]);
|
|
149
|
+
filter.teamKey = teamKey;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.log(colors.textMuted(`Fetching issues from Linear (team: ${filter.teamKey})...`));
|
|
153
|
+
issues = await client.listIssues(filter);
|
|
154
|
+
}
|
|
155
|
+
if (issues.length === 0) {
|
|
156
|
+
if (jsonMode) {
|
|
157
|
+
outputSuccessAsJson({
|
|
158
|
+
imported: 0,
|
|
159
|
+
message: 'No matching issues found.',
|
|
160
|
+
}, createMetadata('linear import', flags));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.log(colors.warning('No matching issues found.'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Check which are already imported
|
|
167
|
+
const newIssues = [];
|
|
168
|
+
const alreadyImported = [];
|
|
169
|
+
for (const issue of issues) {
|
|
170
|
+
const existing = mapper.getByLinearId(issue.id);
|
|
171
|
+
if (existing) {
|
|
172
|
+
alreadyImported.push(issue);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
newIssues.push(issue);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (newIssues.length === 0) {
|
|
179
|
+
if (jsonMode) {
|
|
180
|
+
outputSuccessAsJson({
|
|
181
|
+
imported: 0,
|
|
182
|
+
skipped: alreadyImported.length,
|
|
183
|
+
message: 'All matching issues are already imported.',
|
|
184
|
+
}, createMetadata('linear import', flags));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.log(colors.textMuted(`All ${alreadyImported.length} matching issue(s) already imported.`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Interactive selection (unless --all or explicit IDs)
|
|
191
|
+
let selectedIssues = newIssues;
|
|
192
|
+
if (!flags.all && issueIdentifiers.length === 0 && !jsonMode) {
|
|
193
|
+
const issueChoices = newIssues.map((issue) => ({
|
|
194
|
+
name: `${issue.identifier} ${issue.title} [${issue.state.name}] ${issue.priority > 0 ? `P${issue.priority - 1}` : ''}`,
|
|
195
|
+
value: issue.id,
|
|
196
|
+
checked: true,
|
|
197
|
+
}));
|
|
198
|
+
const { selectedIds } = await inquirer.prompt([{
|
|
199
|
+
type: 'checkbox',
|
|
200
|
+
name: 'selectedIds',
|
|
201
|
+
message: `Select issues to import (${newIssues.length} new, ${alreadyImported.length} already imported):`,
|
|
202
|
+
choices: issueChoices,
|
|
203
|
+
}]);
|
|
204
|
+
selectedIssues = newIssues.filter((i) => selectedIds.includes(i.id));
|
|
205
|
+
}
|
|
206
|
+
if (selectedIssues.length === 0) {
|
|
207
|
+
this.log(colors.textMuted('No issues selected.'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Dry run mode
|
|
211
|
+
if (flags['dry-run']) {
|
|
212
|
+
if (jsonMode) {
|
|
213
|
+
outputSuccessAsJson({
|
|
214
|
+
dryRun: true,
|
|
215
|
+
wouldImport: selectedIssues.map((i) => ({
|
|
216
|
+
identifier: i.identifier,
|
|
217
|
+
title: i.title,
|
|
218
|
+
state: i.state.name,
|
|
219
|
+
priority: i.priority,
|
|
220
|
+
})),
|
|
221
|
+
}, createMetadata('linear import', flags));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.log('');
|
|
225
|
+
this.log(colors.primary('Dry run - would import:'));
|
|
226
|
+
for (const issue of selectedIssues) {
|
|
227
|
+
this.log(` ${colors.textSecondary(issue.identifier)} ${issue.title}`);
|
|
228
|
+
}
|
|
229
|
+
this.log('');
|
|
230
|
+
this.log(colors.textMuted(`${selectedIssues.length} issue(s) would be imported.`));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Import issues
|
|
234
|
+
this.log('');
|
|
235
|
+
this.log(colors.textMuted(`Importing ${selectedIssues.length} issue(s)...`));
|
|
236
|
+
const result = await mapper.importIssues(selectedIssues, projectId, this.storage, statuses);
|
|
237
|
+
if (jsonMode) {
|
|
238
|
+
outputSuccessAsJson({
|
|
239
|
+
imported: result.imported,
|
|
240
|
+
updated: result.updated,
|
|
241
|
+
skipped: result.skipped,
|
|
242
|
+
errors: result.errors,
|
|
243
|
+
}, createMetadata('linear import', flags));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.log('');
|
|
247
|
+
if (result.imported > 0) {
|
|
248
|
+
this.log(colors.success(`Imported ${result.imported} issue(s) into PMO`));
|
|
249
|
+
}
|
|
250
|
+
if (result.skipped > 0) {
|
|
251
|
+
this.log(colors.textMuted(` Skipped ${result.skipped} (already imported)`));
|
|
252
|
+
}
|
|
253
|
+
if (result.errors.length > 0) {
|
|
254
|
+
this.log(colors.error(` ${result.errors.length} error(s):`));
|
|
255
|
+
for (const err of result.errors) {
|
|
256
|
+
this.log(colors.textMuted(` ${err.identifier}: ${err.error}`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class LinearStatus extends PMOCommand {
|
|
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
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
execute(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -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
|
}
|