@proletariat/cli 0.3.45 → 0.3.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/bin/validate-better-sqlite3.cjs +55 -0
  2. package/dist/commands/config/index.js +39 -1
  3. package/dist/commands/linear/auth.d.ts +14 -0
  4. package/dist/commands/linear/auth.js +211 -0
  5. package/dist/commands/linear/import.d.ts +21 -0
  6. package/dist/commands/linear/import.js +260 -0
  7. package/dist/commands/linear/status.d.ts +11 -0
  8. package/dist/commands/linear/status.js +88 -0
  9. package/dist/commands/linear/sync.d.ts +15 -0
  10. package/dist/commands/linear/sync.js +233 -0
  11. package/dist/commands/orchestrator/attach.d.ts +10 -1
  12. package/dist/commands/orchestrator/attach.js +102 -18
  13. package/dist/commands/orchestrator/index.js +22 -7
  14. package/dist/commands/orchestrator/start.d.ts +13 -1
  15. package/dist/commands/orchestrator/start.js +96 -25
  16. package/dist/commands/orchestrator/status.d.ts +1 -0
  17. package/dist/commands/orchestrator/status.js +10 -5
  18. package/dist/commands/orchestrator/stop.d.ts +1 -0
  19. package/dist/commands/orchestrator/stop.js +9 -4
  20. package/dist/commands/session/attach.js +32 -9
  21. package/dist/commands/ticket/link/duplicates.d.ts +15 -0
  22. package/dist/commands/ticket/link/duplicates.js +95 -0
  23. package/dist/commands/ticket/link/index.js +14 -0
  24. package/dist/commands/ticket/link/relates.d.ts +15 -0
  25. package/dist/commands/ticket/link/relates.js +95 -0
  26. package/dist/commands/work/index.js +4 -0
  27. package/dist/commands/work/review.d.ts +45 -0
  28. package/dist/commands/work/review.js +401 -0
  29. package/dist/commands/work/revise.js +4 -3
  30. package/dist/commands/work/spawn.d.ts +5 -0
  31. package/dist/commands/work/spawn.js +195 -14
  32. package/dist/commands/work/start.js +75 -19
  33. package/dist/hooks/init.js +18 -5
  34. package/dist/lib/database/native-validation.d.ts +21 -0
  35. package/dist/lib/database/native-validation.js +49 -0
  36. package/dist/lib/execution/config.d.ts +15 -0
  37. package/dist/lib/execution/config.js +54 -0
  38. package/dist/lib/execution/devcontainer.d.ts +6 -3
  39. package/dist/lib/execution/devcontainer.js +39 -12
  40. package/dist/lib/execution/runners.d.ts +28 -32
  41. package/dist/lib/execution/runners.js +353 -277
  42. package/dist/lib/execution/spawner.js +62 -5
  43. package/dist/lib/execution/types.d.ts +4 -0
  44. package/dist/lib/execution/types.js +3 -0
  45. package/dist/lib/external-issues/adapters.d.ts +26 -0
  46. package/dist/lib/external-issues/adapters.js +251 -0
  47. package/dist/lib/external-issues/index.d.ts +10 -0
  48. package/dist/lib/external-issues/index.js +14 -0
  49. package/dist/lib/external-issues/mapper.d.ts +21 -0
  50. package/dist/lib/external-issues/mapper.js +86 -0
  51. package/dist/lib/external-issues/types.d.ts +144 -0
  52. package/dist/lib/external-issues/types.js +26 -0
  53. package/dist/lib/external-issues/validation.d.ts +34 -0
  54. package/dist/lib/external-issues/validation.js +219 -0
  55. package/dist/lib/linear/client.d.ts +55 -0
  56. package/dist/lib/linear/client.js +254 -0
  57. package/dist/lib/linear/config.d.ts +37 -0
  58. package/dist/lib/linear/config.js +100 -0
  59. package/dist/lib/linear/index.d.ts +11 -0
  60. package/dist/lib/linear/index.js +10 -0
  61. package/dist/lib/linear/mapper.d.ts +67 -0
  62. package/dist/lib/linear/mapper.js +219 -0
  63. package/dist/lib/linear/sync.d.ts +37 -0
  64. package/dist/lib/linear/sync.js +89 -0
  65. package/dist/lib/linear/types.d.ts +139 -0
  66. package/dist/lib/linear/types.js +34 -0
  67. package/dist/lib/mcp/helpers.d.ts +8 -0
  68. package/dist/lib/mcp/helpers.js +10 -0
  69. package/dist/lib/mcp/tools/board.js +63 -11
  70. package/dist/lib/mcp/tools/work.js +36 -0
  71. package/dist/lib/pmo/schema.d.ts +2 -0
  72. package/dist/lib/pmo/schema.js +20 -0
  73. package/dist/lib/pmo/storage/base.js +92 -13
  74. package/dist/lib/pmo/storage/dependencies.js +15 -0
  75. package/dist/lib/prompt-json.d.ts +4 -0
  76. package/oclif.manifest.json +3205 -2537
  77. package/package.json +3 -2
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ const SUPPORTED_NODE_MAJORS = [20, 22, 23, 24, 25]
4
+
5
+ function parseNodeMajor(version) {
6
+ const match = /^v?(\d+)/.exec(version)
7
+ return match ? Number.parseInt(match[1], 10) : null
8
+ }
9
+
10
+ function runtimeInfo() {
11
+ return {
12
+ nodeVersion: process.version,
13
+ nodeMajor: parseNodeMajor(process.version),
14
+ abi: process.versions.modules,
15
+ platform: process.platform,
16
+ arch: process.arch,
17
+ }
18
+ }
19
+
20
+ function buildMessage(error, context) {
21
+ const info = runtimeInfo()
22
+ const nodeMajorHint = info.nodeMajor === null || SUPPORTED_NODE_MAJORS.includes(info.nodeMajor)
23
+ ? ''
24
+ : `\n- Unsupported Node major for this CLI: ${info.nodeMajor} (supported: ${SUPPORTED_NODE_MAJORS.join(', ')})`
25
+ const reason = error instanceof Error ? error.message : String(error)
26
+
27
+ return [
28
+ `better-sqlite3 native module validation failed (${context}).`,
29
+ `Runtime: node ${info.nodeVersion} (ABI ${info.abi}) on ${info.platform}-${info.arch}.${nodeMajorHint}`,
30
+ `Load error: ${reason}`,
31
+ '',
32
+ 'Fix steps:',
33
+ '1. Rebuild native bindings for this runtime: npm rebuild better-sqlite3',
34
+ '2. Verify runtime architecture: node -p "process.platform + \'-\' + process.arch + \' abi=\' + process.versions.modules"',
35
+ '3. Reinstall with your active Node version if needed: npm uninstall -g @proletariat/cli && npm install -g @proletariat/cli',
36
+ ].join('\n')
37
+ }
38
+
39
+ function validate(context) {
40
+ try {
41
+ const Database = require('better-sqlite3')
42
+ const db = new Database(':memory:')
43
+ db.pragma('foreign_keys = ON')
44
+ db.close()
45
+ } catch (error) {
46
+ throw new Error(buildMessage(error, context))
47
+ }
48
+ }
49
+
50
+ try {
51
+ validate('postinstall')
52
+ } catch (error) {
53
+ console.error(error instanceof Error ? error.message : String(error))
54
+ process.exit(1)
55
+ }
@@ -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
+ }