@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
|
@@ -6,7 +6,7 @@ import { PromptCommand } from '../../lib/prompt-command.js';
|
|
|
6
6
|
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
7
7
|
import { styles } from '../../lib/styles.js';
|
|
8
8
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
9
|
-
import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, } from '../../lib/execution/config.js';
|
|
9
|
+
import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, saveCreatePrDefault, saveFirewallAllowlistDomains, } from '../../lib/execution/config.js';
|
|
10
10
|
import { shouldOutputJson, isNonTTY, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
11
11
|
export default class Config extends PromptCommand {
|
|
12
12
|
static description = 'View and update workspace configuration';
|
|
@@ -15,6 +15,7 @@ export default class Config extends PromptCommand {
|
|
|
15
15
|
'<%= config.bin %> <%= command.id %> --json # Output current config as JSON',
|
|
16
16
|
'<%= config.bin %> <%= command.id %> --set terminal.app iTerm',
|
|
17
17
|
'<%= config.bin %> <%= command.id %> --set terminal.openInBackground true',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --set firewall.allowlistDomains "api.staging.example.com"',
|
|
18
19
|
'<%= config.bin %> <%= command.id %> --setting terminal.app --json # Show terminal app choices',
|
|
19
20
|
];
|
|
20
21
|
static flags = {
|
|
@@ -93,6 +94,10 @@ export default class Config extends PromptCommand {
|
|
|
93
94
|
defaultEnvironment: config.defaultEnvironment,
|
|
94
95
|
outputMode: config.outputMode,
|
|
95
96
|
sandboxed: config.sandboxed,
|
|
97
|
+
createPrDefault: config.createPrDefault ?? null,
|
|
98
|
+
firewall: {
|
|
99
|
+
allowlistDomains: config.firewall.allowlistDomains,
|
|
100
|
+
},
|
|
96
101
|
}, createMetadata('config', flags));
|
|
97
102
|
}
|
|
98
103
|
else {
|
|
@@ -115,6 +120,8 @@ export default class Config extends PromptCommand {
|
|
|
115
120
|
this.log(` defaultEnvironment: ${config.defaultEnvironment}`);
|
|
116
121
|
this.log(` outputMode: ${config.outputMode}`);
|
|
117
122
|
this.log(` sandboxed: ${config.sandboxed}`);
|
|
123
|
+
this.log(` createPrDefault: ${config.createPrDefault ?? 'not set (will prompt)'}`);
|
|
124
|
+
this.log(` firewall.allowlistDomains: ${config.firewall.allowlistDomains.join(', ') || '(none)'}`);
|
|
118
125
|
this.log('');
|
|
119
126
|
}
|
|
120
127
|
db.close();
|
|
@@ -132,6 +139,7 @@ export default class Config extends PromptCommand {
|
|
|
132
139
|
{ name: `Open Tabs in Background: ${config.terminal.openInBackground}`, value: 'terminal.openInBackground', command: 'prlt config --setting terminal.openInBackground --json' },
|
|
133
140
|
{ name: `Shell: ${config.shell}`, value: 'shell', command: 'prlt config --setting shell --json' },
|
|
134
141
|
{ name: `Tmux Control Mode (iTerm -CC): ${config.tmux.controlMode}`, value: 'tmux.controlMode', command: 'prlt config --setting tmux.controlMode --json' },
|
|
142
|
+
{ name: `Firewall allowlist domains: ${config.firewall.allowlistDomains.length || 0}`, value: 'firewall.allowlistDomains', command: 'prlt config --setting firewall.allowlistDomains --json' },
|
|
135
143
|
];
|
|
136
144
|
const { setting } = await this.prompt([
|
|
137
145
|
{
|
|
@@ -145,6 +153,8 @@ export default class Config extends PromptCommand {
|
|
|
145
153
|
settingChoices[2],
|
|
146
154
|
new inquirer.Separator('── Tmux ──'),
|
|
147
155
|
settingChoices[3],
|
|
156
|
+
new inquirer.Separator('── Execution ──'),
|
|
157
|
+
settingChoices[4],
|
|
148
158
|
new inquirer.Separator(),
|
|
149
159
|
{ name: 'Exit', value: '__exit__' },
|
|
150
160
|
],
|
|
@@ -247,6 +257,23 @@ export default class Config extends PromptCommand {
|
|
|
247
257
|
this.log(styles.success(`Tmux control mode set to: ${controlMode}`));
|
|
248
258
|
break;
|
|
249
259
|
}
|
|
260
|
+
case 'firewall.allowlistDomains': {
|
|
261
|
+
const { domainsInput } = await this.prompt([
|
|
262
|
+
{
|
|
263
|
+
type: 'input',
|
|
264
|
+
name: 'domainsInput',
|
|
265
|
+
message: 'Extra firewall allowlist domains (comma-separated, leave empty to clear):',
|
|
266
|
+
default: config.firewall.allowlistDomains.join(', '),
|
|
267
|
+
},
|
|
268
|
+
], jsonModeConfig);
|
|
269
|
+
const domains = domainsInput
|
|
270
|
+
.split(',')
|
|
271
|
+
.map(domain => domain.trim())
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
saveFirewallAllowlistDomains(db, domains);
|
|
274
|
+
this.log(styles.success(`Firewall allowlist domains set (${domains.length})`));
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
250
277
|
default: {
|
|
251
278
|
const jsonMode = shouldOutputJson(jsonModeConfig?.flags ?? {});
|
|
252
279
|
if (jsonMode) {
|
|
@@ -271,6 +298,17 @@ export default class Config extends PromptCommand {
|
|
|
271
298
|
case 'tmux.controlmode':
|
|
272
299
|
saveTmuxControlMode(db, value.toLowerCase() === 'true');
|
|
273
300
|
break;
|
|
301
|
+
case 'execution.create_pr_default':
|
|
302
|
+
saveCreatePrDefault(db, value.toLowerCase() === 'true');
|
|
303
|
+
break;
|
|
304
|
+
case 'firewall.allowlistdomains': {
|
|
305
|
+
const domains = value
|
|
306
|
+
.split(',')
|
|
307
|
+
.map(domain => domain.trim())
|
|
308
|
+
.filter(Boolean);
|
|
309
|
+
saveFirewallAllowlistDomains(db, domains);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
274
312
|
default:
|
|
275
313
|
if (jsonMode) {
|
|
276
314
|
outputErrorAsJson('UNKNOWN_KEY', `Unknown config key: ${key}`, createMetadata('config', {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class LinearAuth extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
disconnect: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
execute(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
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, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
|
+
import { LinearClient, loadLinearConfig, saveLinearApiKey, saveLinearDefaultTeam, saveLinearOrganization, clearLinearConfig, getLinearApiKey, } from '../../lib/linear/index.js';
|
|
7
|
+
export default class LinearAuth extends PMOCommand {
|
|
8
|
+
static description = 'Authenticate with Linear and configure workspace connection';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --check',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --force',
|
|
13
|
+
'LINEAR_API_KEY=lin_api_... <%= config.bin %> <%= command.id %>',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...pmoBaseFlags,
|
|
17
|
+
check: Flags.boolean({
|
|
18
|
+
description: 'Only check if Linear credentials exist (do not prompt)',
|
|
19
|
+
default: false,
|
|
20
|
+
}),
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
description: 'Force re-authentication even if credentials exist',
|
|
23
|
+
default: false,
|
|
24
|
+
}),
|
|
25
|
+
disconnect: Flags.boolean({
|
|
26
|
+
description: 'Remove stored Linear credentials',
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async execute() {
|
|
31
|
+
const { flags } = await this.parse(LinearAuth);
|
|
32
|
+
const jsonMode = shouldOutputJson(flags);
|
|
33
|
+
const db = this.storage.getDatabase();
|
|
34
|
+
// Handle --disconnect
|
|
35
|
+
if (flags.disconnect) {
|
|
36
|
+
clearLinearConfig(db);
|
|
37
|
+
if (jsonMode) {
|
|
38
|
+
outputSuccessAsJson({
|
|
39
|
+
disconnected: true,
|
|
40
|
+
message: 'Linear credentials removed.',
|
|
41
|
+
}, createMetadata('linear auth', flags));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.log(colors.success('Linear credentials removed.'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Handle --check
|
|
48
|
+
if (flags.check) {
|
|
49
|
+
const config = loadLinearConfig(db);
|
|
50
|
+
if (config) {
|
|
51
|
+
// Try to verify the connection
|
|
52
|
+
try {
|
|
53
|
+
const client = new LinearClient(config.apiKey);
|
|
54
|
+
const info = await client.verify();
|
|
55
|
+
if (jsonMode) {
|
|
56
|
+
outputSuccessAsJson({
|
|
57
|
+
authenticated: true,
|
|
58
|
+
organization: info.organizationName,
|
|
59
|
+
user: info.userName,
|
|
60
|
+
email: info.email,
|
|
61
|
+
defaultTeam: config.defaultTeamKey ?? null,
|
|
62
|
+
}, createMetadata('linear auth', flags));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.log(colors.success('Linear connection is active'));
|
|
66
|
+
this.log(colors.textMuted(` Organization: ${info.organizationName}`));
|
|
67
|
+
this.log(colors.textMuted(` User: ${info.userName} (${info.email})`));
|
|
68
|
+
if (config.defaultTeamKey) {
|
|
69
|
+
this.log(colors.textMuted(` Default team: ${config.defaultTeamKey}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (jsonMode) {
|
|
74
|
+
outputErrorAsJson('LINEAR_AUTH_INVALID', 'Stored Linear API key is invalid or expired.', createMetadata('linear auth', flags));
|
|
75
|
+
this.exit(1);
|
|
76
|
+
}
|
|
77
|
+
this.log(colors.error('Stored Linear API key is invalid or expired.'));
|
|
78
|
+
this.log(colors.textMuted('Run "prlt linear auth --force" to re-authenticate.'));
|
|
79
|
+
this.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (jsonMode) {
|
|
84
|
+
outputErrorAsJson('LINEAR_NOT_CONFIGURED', 'Linear is not configured. Run "prlt linear auth" to authenticate.', createMetadata('linear auth', flags));
|
|
85
|
+
this.exit(1);
|
|
86
|
+
}
|
|
87
|
+
this.log(colors.warning('Linear is not configured.'));
|
|
88
|
+
this.log(colors.textMuted('Run "prlt linear auth" to authenticate.'));
|
|
89
|
+
this.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Check for existing config
|
|
94
|
+
const existingConfig = loadLinearConfig(db);
|
|
95
|
+
if (existingConfig && !flags.force) {
|
|
96
|
+
try {
|
|
97
|
+
const client = new LinearClient(existingConfig.apiKey);
|
|
98
|
+
const info = await client.verify();
|
|
99
|
+
if (jsonMode) {
|
|
100
|
+
outputSuccessAsJson({
|
|
101
|
+
authenticated: true,
|
|
102
|
+
organization: info.organizationName,
|
|
103
|
+
user: info.userName,
|
|
104
|
+
message: 'Already authenticated. Use --force to re-authenticate.',
|
|
105
|
+
}, createMetadata('linear auth', flags));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.log(colors.success('Already connected to Linear'));
|
|
109
|
+
this.log(colors.textMuted(` Organization: ${info.organizationName}`));
|
|
110
|
+
this.log(colors.textMuted(` User: ${info.userName}`));
|
|
111
|
+
this.log('');
|
|
112
|
+
this.log(colors.textMuted('Use --force to re-authenticate.'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Stored key is bad, proceed with re-auth
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Try environment variable first
|
|
120
|
+
const envKey = getLinearApiKey(db);
|
|
121
|
+
let apiKey = envKey;
|
|
122
|
+
if (!apiKey) {
|
|
123
|
+
// Prompt for API key
|
|
124
|
+
if (jsonMode) {
|
|
125
|
+
outputErrorAsJson('API_KEY_REQUIRED', 'Linear API key required. Set LINEAR_API_KEY or PRLT_LINEAR_API_KEY environment variable, or run interactively.', createMetadata('linear auth', flags));
|
|
126
|
+
this.exit(1);
|
|
127
|
+
}
|
|
128
|
+
this.log('');
|
|
129
|
+
this.log(colors.primary('Linear Authentication'));
|
|
130
|
+
this.log('');
|
|
131
|
+
this.log('Create a personal API key at:');
|
|
132
|
+
this.log(colors.textSecondary(' https://linear.app/settings/api'));
|
|
133
|
+
this.log('');
|
|
134
|
+
const { inputKey } = await inquirer.prompt([{
|
|
135
|
+
type: 'password',
|
|
136
|
+
name: 'inputKey',
|
|
137
|
+
message: 'Enter your Linear API key:',
|
|
138
|
+
mask: '*',
|
|
139
|
+
validate: (input) => {
|
|
140
|
+
if (!input.trim())
|
|
141
|
+
return 'API key is required';
|
|
142
|
+
if (!input.startsWith('lin_api_'))
|
|
143
|
+
return 'Linear API keys start with "lin_api_"';
|
|
144
|
+
return true;
|
|
145
|
+
},
|
|
146
|
+
}]);
|
|
147
|
+
apiKey = inputKey;
|
|
148
|
+
}
|
|
149
|
+
// Verify the API key
|
|
150
|
+
this.log('');
|
|
151
|
+
this.log(colors.textMuted('Verifying API key...'));
|
|
152
|
+
let client;
|
|
153
|
+
try {
|
|
154
|
+
client = new LinearClient(apiKey);
|
|
155
|
+
const info = await client.verify();
|
|
156
|
+
// Save API key and org name
|
|
157
|
+
saveLinearApiKey(db, apiKey);
|
|
158
|
+
saveLinearOrganization(db, info.organizationName);
|
|
159
|
+
if (jsonMode) {
|
|
160
|
+
// Don't prompt for team in JSON mode, just save the key
|
|
161
|
+
outputSuccessAsJson({
|
|
162
|
+
authenticated: true,
|
|
163
|
+
organization: info.organizationName,
|
|
164
|
+
user: info.userName,
|
|
165
|
+
email: info.email,
|
|
166
|
+
}, createMetadata('linear auth', flags));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.log(colors.success(`Connected to ${info.organizationName}`));
|
|
170
|
+
this.log(colors.textMuted(` Signed in as ${info.userName} (${info.email})`));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (jsonMode) {
|
|
174
|
+
outputErrorAsJson('LINEAR_AUTH_FAILED', `Authentication failed: ${error instanceof Error ? error.message : String(error)}`, createMetadata('linear auth', flags));
|
|
175
|
+
this.exit(1);
|
|
176
|
+
}
|
|
177
|
+
this.error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
// Prompt for default team selection
|
|
180
|
+
this.log('');
|
|
181
|
+
const teams = await client.listTeams();
|
|
182
|
+
if (teams.length === 0) {
|
|
183
|
+
this.log(colors.warning('No teams found in your Linear workspace.'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (teams.length === 1) {
|
|
187
|
+
// Auto-select the only team
|
|
188
|
+
saveLinearDefaultTeam(db, teams[0].id, teams[0].key);
|
|
189
|
+
this.log(colors.textMuted(` Default team set to: ${teams[0].name} (${teams[0].key})`));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const teamChoices = teams.map((t) => ({
|
|
193
|
+
name: `${t.name} (${t.key})`,
|
|
194
|
+
value: t.id,
|
|
195
|
+
}));
|
|
196
|
+
const { selectedTeamId } = await inquirer.prompt([{
|
|
197
|
+
type: 'list',
|
|
198
|
+
name: 'selectedTeamId',
|
|
199
|
+
message: 'Select your default team:',
|
|
200
|
+
choices: teamChoices,
|
|
201
|
+
}]);
|
|
202
|
+
const selectedTeam = teams.find((t) => t.id === selectedTeamId);
|
|
203
|
+
saveLinearDefaultTeam(db, selectedTeam.id, selectedTeam.key);
|
|
204
|
+
this.log(colors.textMuted(` Default team set to: ${selectedTeam.name} (${selectedTeam.key})`));
|
|
205
|
+
}
|
|
206
|
+
this.log('');
|
|
207
|
+
this.log(colors.success('Linear integration configured!'));
|
|
208
|
+
this.log(colors.textMuted(' Run "prlt linear import" to pull issues into PMO'));
|
|
209
|
+
this.log(colors.textMuted(' Run "prlt work spawn --from-linear" to pull and spawn agents'));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class LinearImport extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static strict: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
state: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'state-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
assignee: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
cycle: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
};
|
|
20
|
+
execute(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -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
|
+
}
|