@dabble/linear-cli 1.0.1 → 1.0.3

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/bin/linear.mjs CHANGED
@@ -180,9 +180,11 @@ async function cmdIssues(args) {
180
180
  unblocked: 'boolean', u: 'boolean',
181
181
  all: 'boolean', a: 'boolean',
182
182
  open: 'boolean', o: 'boolean',
183
+ backlog: 'boolean', b: 'boolean',
183
184
  mine: 'boolean', m: 'boolean',
184
185
  'in-progress': 'boolean',
185
186
  project: 'string', p: 'string',
187
+ milestone: 'string',
186
188
  state: 'string', s: 'string',
187
189
  label: 'string', l: 'string',
188
190
  });
@@ -191,8 +193,11 @@ async function cmdIssues(args) {
191
193
  const unblocked = opts.unblocked || opts.u;
192
194
  const allStates = opts.all || opts.a;
193
195
  const openOnly = opts.open || opts.o;
196
+ const backlogOnly = opts.backlog || opts.b;
194
197
  const mineOnly = opts.mine || opts.m;
195
- const stateFilter = inProgress ? 'started' : (opts.state || opts.s || 'backlog');
198
+ const projectFilter = opts.project || opts.p;
199
+ const milestoneFilter = opts.milestone;
200
+ const stateFilter = opts.state || opts.s;
196
201
  const labelFilter = opts.label || opts.l;
197
202
 
198
203
  // Get current user ID for filtering/sorting
@@ -206,8 +211,10 @@ async function cmdIssues(args) {
206
211
  identifier
207
212
  title
208
213
  priority
214
+ sortOrder
209
215
  state { name type }
210
216
  project { name }
217
+ projectMilestone { name }
211
218
  assignee { id name }
212
219
  labels { nodes { name } }
213
220
  relations(first: 20) {
@@ -227,13 +234,16 @@ async function cmdIssues(args) {
227
234
  // Check if any issues have assignees (to decide whether to show column)
228
235
  const hasAssignees = issues.some(i => i.assignee);
229
236
 
230
- // Sort: assigned to you first, then by identifier
237
+ // Sort: assigned to you first, then by priority, then by sortOrder
231
238
  issues.sort((a, b) => {
232
239
  const aIsMine = a.assignee?.id === viewerId;
233
240
  const bIsMine = b.assignee?.id === viewerId;
234
241
  if (aIsMine && !bIsMine) return -1;
235
242
  if (!aIsMine && bIsMine) return 1;
236
- return a.identifier.localeCompare(b.identifier);
243
+ // Then by priority (higher = more urgent)
244
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
245
+ // Then by sortOrder
246
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
237
247
  });
238
248
 
239
249
  // Helper to format issue row
@@ -251,7 +261,7 @@ async function cmdIssues(args) {
251
261
  return row;
252
262
  };
253
263
 
254
- // Helper to apply common filters (mine, label)
264
+ // Helper to apply common filters (mine, label, project, milestone)
255
265
  const applyFilters = (list) => {
256
266
  let filtered = list;
257
267
  if (mineOnly) {
@@ -262,6 +272,16 @@ async function cmdIssues(args) {
262
272
  i.labels?.nodes?.some(l => l.name.toLowerCase() === labelFilter.toLowerCase())
263
273
  );
264
274
  }
275
+ if (projectFilter) {
276
+ filtered = filtered.filter(i =>
277
+ i.project?.name?.toLowerCase().includes(projectFilter.toLowerCase())
278
+ );
279
+ }
280
+ if (milestoneFilter) {
281
+ filtered = filtered.filter(i =>
282
+ i.projectMilestone?.name?.toLowerCase().includes(milestoneFilter.toLowerCase())
283
+ );
284
+ }
265
285
  return filtered;
266
286
  };
267
287
 
@@ -301,14 +321,28 @@ async function cmdIssues(args) {
301
321
 
302
322
  console.log(colors.bold('Open Issues:\n'));
303
323
  console.log(formatTable(filtered.map(formatRow)));
304
- } else {
324
+ } else if (inProgress) {
325
+ let filtered = issues.filter(i => i.state.type === 'started');
326
+ filtered = applyFilters(filtered);
327
+
328
+ console.log(colors.bold('In Progress:\n'));
329
+ console.log(formatTable(filtered.map(formatRow)));
330
+ } else if (backlogOnly || stateFilter) {
331
+ const targetState = stateFilter || 'backlog';
305
332
  let filtered = issues.filter(i =>
306
- i.state.type === stateFilter || i.state.name.toLowerCase() === stateFilter.toLowerCase()
333
+ i.state.type === targetState || i.state.name.toLowerCase() === targetState.toLowerCase()
307
334
  );
308
335
 
309
336
  filtered = applyFilters(filtered);
310
337
 
311
- console.log(colors.bold(`Issues (${stateFilter}):\n`));
338
+ console.log(colors.bold(`Issues (${targetState}):\n`));
339
+ console.log(formatTable(filtered.map(formatRow)));
340
+ } else {
341
+ // Default: show backlog
342
+ let filtered = issues.filter(i => i.state.type === 'backlog');
343
+ filtered = applyFilters(filtered);
344
+
345
+ console.log(colors.bold('Issues (backlog):\n'));
312
346
  console.log(formatTable(filtered.map(formatRow)));
313
347
  }
314
348
  }
@@ -468,6 +502,7 @@ async function cmdIssueCreate(args) {
468
502
  title: 'string', t: 'string',
469
503
  description: 'string', d: 'string',
470
504
  project: 'string', p: 'string',
505
+ milestone: 'string',
471
506
  parent: 'string',
472
507
  state: 'string', s: 'string',
473
508
  assign: 'boolean',
@@ -480,6 +515,7 @@ async function cmdIssueCreate(args) {
480
515
  const title = opts.title || opts.t || opts._[0];
481
516
  const description = opts.description || opts.d || '';
482
517
  const project = opts.project || opts.p;
518
+ const milestone = opts.milestone;
483
519
  const parent = opts.parent;
484
520
  const shouldAssign = opts.assign;
485
521
  const estimate = (opts.estimate || opts.e || '').toLowerCase();
@@ -489,7 +525,7 @@ async function cmdIssueCreate(args) {
489
525
 
490
526
  if (!title) {
491
527
  console.error(colors.red('Error: Title is required'));
492
- console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--parent ISSUE-X] [--estimate M] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
528
+ console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--milestone "..."] [--parent ISSUE-X] [--estimate M] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
493
529
  process.exit(1);
494
530
  }
495
531
 
@@ -508,17 +544,47 @@ async function cmdIssueCreate(args) {
508
544
  process.exit(1);
509
545
  }
510
546
 
511
- // Look up project ID
547
+ // Look up project and milestone IDs
512
548
  let projectId = null;
513
- if (project) {
549
+ let milestoneId = null;
550
+ if (project || milestone) {
514
551
  const projectsResult = await gql(`{
515
552
  team(id: "${TEAM_KEY}") {
516
- projects(first: 50) { nodes { id name } }
553
+ projects(first: 50) {
554
+ nodes {
555
+ id name
556
+ projectMilestones { nodes { id name } }
557
+ }
558
+ }
517
559
  }
518
560
  }`);
519
561
  const projects = projectsResult.data?.team?.projects?.nodes || [];
520
- const match = projects.find(p => p.name.toLowerCase().includes(project.toLowerCase()));
521
- if (match) projectId = match.id;
562
+
563
+ if (project) {
564
+ const projectMatch = projects.find(p => p.name.toLowerCase().includes(project.toLowerCase()));
565
+ if (projectMatch) {
566
+ projectId = projectMatch.id;
567
+ // Look for milestone within this project
568
+ if (milestone) {
569
+ const milestoneMatch = projectMatch.projectMilestones?.nodes?.find(m =>
570
+ m.name.toLowerCase().includes(milestone.toLowerCase())
571
+ );
572
+ if (milestoneMatch) milestoneId = milestoneMatch.id;
573
+ }
574
+ }
575
+ } else if (milestone) {
576
+ // Search all projects for the milestone
577
+ for (const p of projects) {
578
+ const milestoneMatch = p.projectMilestones?.nodes?.find(m =>
579
+ m.name.toLowerCase().includes(milestone.toLowerCase())
580
+ );
581
+ if (milestoneMatch) {
582
+ projectId = p.id; // Auto-set project from milestone
583
+ milestoneId = milestoneMatch.id;
584
+ break;
585
+ }
586
+ }
587
+ }
522
588
  }
523
589
 
524
590
  // Look up label ID
@@ -556,6 +622,7 @@ async function cmdIssueCreate(args) {
556
622
 
557
623
  const input = { teamId, title, description };
558
624
  if (projectId) input.projectId = projectId;
625
+ if (milestoneId) input.projectMilestoneId = milestoneId;
559
626
  if (parent) input.parentId = parent;
560
627
  if (assigneeId) input.assigneeId = assigneeId;
561
628
  if (estimate) input.estimate = ESTIMATE_MAP[estimate];
@@ -611,6 +678,8 @@ async function cmdIssueUpdate(args) {
611
678
  title: 'string', t: 'string',
612
679
  description: 'string', d: 'string',
613
680
  state: 'string', s: 'string',
681
+ project: 'string', p: 'string',
682
+ milestone: 'string',
614
683
  append: 'string', a: 'string',
615
684
  blocks: 'string',
616
685
  'blocked-by': 'string',
@@ -618,6 +687,8 @@ async function cmdIssueUpdate(args) {
618
687
 
619
688
  const blocksIssue = opts.blocks;
620
689
  const blockedByIssue = opts['blocked-by'];
690
+ const projectName = opts.project || opts.p;
691
+ const milestoneName = opts.milestone;
621
692
  const input = {};
622
693
 
623
694
  if (opts.title || opts.t) input.title = opts.title || opts.t;
@@ -644,6 +715,46 @@ async function cmdIssueUpdate(args) {
644
715
  if (match) input.stateId = match.id;
645
716
  }
646
717
 
718
+ // Handle project and milestone
719
+ if (projectName || milestoneName) {
720
+ const projectsResult = await gql(`{
721
+ team(id: "${TEAM_KEY}") {
722
+ projects(first: 50) {
723
+ nodes {
724
+ id name
725
+ projectMilestones { nodes { id name } }
726
+ }
727
+ }
728
+ }
729
+ }`);
730
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
731
+
732
+ if (projectName) {
733
+ const projectMatch = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
734
+ if (projectMatch) {
735
+ input.projectId = projectMatch.id;
736
+ if (milestoneName) {
737
+ const milestoneMatch = projectMatch.projectMilestones?.nodes?.find(m =>
738
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
739
+ );
740
+ if (milestoneMatch) input.projectMilestoneId = milestoneMatch.id;
741
+ }
742
+ }
743
+ } else if (milestoneName) {
744
+ // Search all projects for the milestone
745
+ for (const p of projects) {
746
+ const milestoneMatch = p.projectMilestones?.nodes?.find(m =>
747
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
748
+ );
749
+ if (milestoneMatch) {
750
+ input.projectId = p.id;
751
+ input.projectMilestoneId = milestoneMatch.id;
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ }
757
+
647
758
  // Handle blocking relations (can be set even without other updates)
648
759
  const hasRelationUpdates = blocksIssue || blockedByIssue;
649
760
 
@@ -985,6 +1096,746 @@ async function cmdProjectComplete(args) {
985
1096
  }
986
1097
  }
987
1098
 
1099
+ // ============================================================================
1100
+ // MILESTONES
1101
+ // ============================================================================
1102
+
1103
+ async function cmdMilestones(args) {
1104
+ const opts = parseArgs(args, {
1105
+ project: 'string', p: 'string',
1106
+ all: 'boolean', a: 'boolean',
1107
+ });
1108
+ const projectFilter = opts.project || opts.p;
1109
+ const showAll = opts.all || opts.a;
1110
+
1111
+ const query = `{
1112
+ team(id: "${TEAM_KEY}") {
1113
+ projects(first: 50) {
1114
+ nodes {
1115
+ id name state
1116
+ projectMilestones {
1117
+ nodes { id name targetDate sortOrder status }
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ }`;
1123
+
1124
+ const result = await gql(query);
1125
+ let projects = result.data?.team?.projects?.nodes || [];
1126
+
1127
+ // Filter to active projects unless --all
1128
+ if (!showAll) {
1129
+ projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
1130
+ }
1131
+
1132
+ // Filter by project name if specified
1133
+ if (projectFilter) {
1134
+ projects = projects.filter(p => p.name.toLowerCase().includes(projectFilter.toLowerCase()));
1135
+ }
1136
+
1137
+ console.log(colors.bold('Milestones:\n'));
1138
+ for (const project of projects) {
1139
+ const milestones = project.projectMilestones?.nodes || [];
1140
+ if (milestones.length === 0) continue;
1141
+
1142
+ console.log(colors.bold(project.name));
1143
+ for (const m of milestones) {
1144
+ const date = m.targetDate ? ` (${m.targetDate})` : '';
1145
+ const status = m.status !== 'planned' ? ` [${m.status}]` : '';
1146
+ console.log(` ${m.name}${date}${status}`);
1147
+ }
1148
+ console.log('');
1149
+ }
1150
+ }
1151
+
1152
+ async function cmdMilestoneShow(args) {
1153
+ const milestoneName = args[0];
1154
+ if (!milestoneName) {
1155
+ console.error(colors.red('Error: Milestone name required'));
1156
+ process.exit(1);
1157
+ }
1158
+
1159
+ const query = `{
1160
+ team(id: "${TEAM_KEY}") {
1161
+ projects(first: 50) {
1162
+ nodes {
1163
+ name
1164
+ projectMilestones {
1165
+ nodes {
1166
+ id name description targetDate status sortOrder
1167
+ issues { nodes { identifier title state { name type } } }
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }`;
1174
+
1175
+ const result = await gql(query);
1176
+ const projects = result.data?.team?.projects?.nodes || [];
1177
+
1178
+ let milestone = null;
1179
+ let projectName = '';
1180
+ for (const p of projects) {
1181
+ const m = p.projectMilestones?.nodes?.find(m =>
1182
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
1183
+ );
1184
+ if (m) {
1185
+ milestone = m;
1186
+ projectName = p.name;
1187
+ break;
1188
+ }
1189
+ }
1190
+
1191
+ if (!milestone) {
1192
+ console.error(colors.red(`Milestone not found: ${milestoneName}`));
1193
+ process.exit(1);
1194
+ }
1195
+
1196
+ console.log(`# ${milestone.name}\n`);
1197
+ console.log(`Project: ${projectName}`);
1198
+ console.log(`Status: ${milestone.status}`);
1199
+ if (milestone.targetDate) console.log(`Target: ${milestone.targetDate}`);
1200
+ if (milestone.description) console.log(`\n## Description\n${milestone.description}`);
1201
+
1202
+ const issues = milestone.issues?.nodes || [];
1203
+ if (issues.length > 0) {
1204
+ // Group by state type
1205
+ const done = issues.filter(i => i.state.type === 'completed');
1206
+ const inProgress = issues.filter(i => i.state.type === 'started');
1207
+ const backlog = issues.filter(i => !['completed', 'started', 'canceled'].includes(i.state.type));
1208
+
1209
+ console.log('\n## Issues\n');
1210
+ if (inProgress.length > 0) {
1211
+ console.log('### In Progress');
1212
+ inProgress.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1213
+ console.log('');
1214
+ }
1215
+ if (backlog.length > 0) {
1216
+ console.log('### Backlog');
1217
+ backlog.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1218
+ console.log('');
1219
+ }
1220
+ if (done.length > 0) {
1221
+ console.log('### Done');
1222
+ done.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1223
+ console.log('');
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ async function cmdMilestoneCreate(args) {
1229
+ const opts = parseArgs(args, {
1230
+ name: 'string', n: 'string',
1231
+ project: 'string', p: 'string',
1232
+ description: 'string', d: 'string',
1233
+ 'target-date': 'string',
1234
+ });
1235
+
1236
+ const name = opts.name || opts.n || opts._[0];
1237
+ const projectName = opts.project || opts.p;
1238
+ const description = opts.description || opts.d;
1239
+ const targetDate = opts['target-date'];
1240
+
1241
+ if (!name) {
1242
+ console.error(colors.red('Error: Milestone name required'));
1243
+ console.error('Usage: linear milestone create "Name" --project "Project" [--target-date 2024-03-01]');
1244
+ process.exit(1);
1245
+ }
1246
+
1247
+ if (!projectName) {
1248
+ console.error(colors.red('Error: Project required (--project)'));
1249
+ process.exit(1);
1250
+ }
1251
+
1252
+ // Find project
1253
+ const projectsResult = await gql(`{
1254
+ team(id: "${TEAM_KEY}") {
1255
+ projects(first: 50) { nodes { id name } }
1256
+ }
1257
+ }`);
1258
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1259
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1260
+
1261
+ if (!project) {
1262
+ console.error(colors.red(`Project not found: ${projectName}`));
1263
+ process.exit(1);
1264
+ }
1265
+
1266
+ const mutation = `
1267
+ mutation($input: ProjectMilestoneCreateInput!) {
1268
+ projectMilestoneCreate(input: $input) {
1269
+ success
1270
+ projectMilestone { id name }
1271
+ }
1272
+ }
1273
+ `;
1274
+
1275
+ const input = { projectId: project.id, name };
1276
+ if (description) input.description = description;
1277
+ if (targetDate) input.targetDate = targetDate;
1278
+
1279
+ const result = await gql(mutation, { input });
1280
+
1281
+ if (result.data?.projectMilestoneCreate?.success) {
1282
+ console.log(colors.green(`Created milestone: ${name}`));
1283
+ console.log(`Project: ${project.name}`);
1284
+ } else {
1285
+ console.error(colors.red('Failed to create milestone'));
1286
+ console.error(result.errors?.[0]?.message || JSON.stringify(result));
1287
+ process.exit(1);
1288
+ }
1289
+ }
1290
+
1291
+ // ============================================================================
1292
+ // ROADMAP
1293
+ // ============================================================================
1294
+
1295
+ async function cmdRoadmap(args) {
1296
+ const opts = parseArgs(args, { all: 'boolean', a: 'boolean' });
1297
+ const showAll = opts.all || opts.a;
1298
+
1299
+ // Fetch projects and milestones
1300
+ const projectsQuery = `{
1301
+ team(id: "${TEAM_KEY}") {
1302
+ projects(first: 50) {
1303
+ nodes {
1304
+ id name state priority sortOrder targetDate startDate
1305
+ projectMilestones {
1306
+ nodes {
1307
+ id name targetDate status sortOrder
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ }`;
1314
+
1315
+ // Fetch issues separately to avoid complexity limits
1316
+ const issuesQuery = `{
1317
+ team(id: "${TEAM_KEY}") {
1318
+ issues(first: 200) {
1319
+ nodes {
1320
+ id identifier title state { name type } sortOrder priority
1321
+ project { id }
1322
+ projectMilestone { id }
1323
+ }
1324
+ }
1325
+ }
1326
+ }`;
1327
+
1328
+ const [projectsResult, issuesResult] = await Promise.all([
1329
+ gql(projectsQuery),
1330
+ gql(issuesQuery)
1331
+ ]);
1332
+
1333
+ let projects = projectsResult.data?.team?.projects?.nodes || [];
1334
+ const allIssues = issuesResult.data?.team?.issues?.nodes || [];
1335
+
1336
+ // Sort by sortOrder descending (higher = first)
1337
+ projects.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1338
+
1339
+ if (!showAll) {
1340
+ projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
1341
+ }
1342
+
1343
+ console.log(colors.bold('Roadmap\n'));
1344
+
1345
+ for (const project of projects) {
1346
+ const issues = allIssues.filter(i => i.project?.id === project.id);
1347
+ const milestones = project.projectMilestones?.nodes || [];
1348
+
1349
+ // Count issues by state
1350
+ const done = issues.filter(i => i.state.type === 'completed').length;
1351
+ const inProgress = issues.filter(i => i.state.type === 'started').length;
1352
+ const backlog = issues.filter(i => !['completed', 'started', 'canceled'].includes(i.state.type)).length;
1353
+
1354
+ // Project header
1355
+ const dates = [];
1356
+ if (project.startDate) dates.push(`start: ${project.startDate}`);
1357
+ if (project.targetDate) dates.push(`target: ${project.targetDate}`);
1358
+ const dateStr = dates.length > 0 ? ` (${dates.join(', ')})` : '';
1359
+ const priorityStr = project.priority > 0 ? ` [P${project.priority}]` : '';
1360
+
1361
+ console.log(colors.bold(`${project.name}${priorityStr}${dateStr}`));
1362
+ console.log(` ${colors.green(`✓ ${done}`)} done | ${colors.yellow(`→ ${inProgress}`)} in progress | ${colors.gray(`○ ${backlog}`)} backlog`);
1363
+
1364
+ // Show milestones with their issues
1365
+ if (milestones.length > 0) {
1366
+ // Sort milestones by sortOrder descending
1367
+ milestones.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1368
+
1369
+ for (const m of milestones) {
1370
+ // Count issues in this milestone from project issues
1371
+ const mIssues = issues.filter(i => i.projectMilestone?.id === m.id);
1372
+ const mDone = mIssues.filter(i => i.state.type === 'completed').length;
1373
+ const mTotal = mIssues.length;
1374
+ const statusIcon = m.status === 'completed' ? colors.green('✓') :
1375
+ m.status === 'inProgress' ? colors.yellow('→') : '○';
1376
+ const targetStr = m.targetDate ? ` (${m.targetDate})` : '';
1377
+
1378
+ console.log(` ${statusIcon} ${m.name}${targetStr}: ${mDone}/${mTotal} done`);
1379
+
1380
+ // Show non-completed issues in this milestone
1381
+ const issuesInMilestone = mIssues.filter(i =>
1382
+ !['completed', 'canceled'].includes(i.state.type)
1383
+ );
1384
+
1385
+ // Sort by priority then sortOrder
1386
+ issuesInMilestone.sort((a, b) => {
1387
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
1388
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
1389
+ });
1390
+
1391
+ for (const i of issuesInMilestone.slice(0, 5)) {
1392
+ const stateIcon = i.state.type === 'started' ? colors.yellow('→') : '○';
1393
+ console.log(` ${stateIcon} ${i.identifier}: ${i.title}`);
1394
+ }
1395
+ if (issuesInMilestone.length > 5) {
1396
+ console.log(colors.gray(` ... and ${issuesInMilestone.length - 5} more`));
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ // Show issues not in any milestone
1402
+ const unmilestonedIssues = issues.filter(i =>
1403
+ !i.projectMilestone &&
1404
+ !['completed', 'canceled'].includes(i.state.type)
1405
+ );
1406
+
1407
+ if (unmilestonedIssues.length > 0 && milestones.length > 0) {
1408
+ console.log(colors.gray(` (${unmilestonedIssues.length} issues not in milestones)`));
1409
+ } else if (unmilestonedIssues.length > 0) {
1410
+ // Sort by priority then sortOrder
1411
+ unmilestonedIssues.sort((a, b) => {
1412
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
1413
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
1414
+ });
1415
+
1416
+ for (const i of unmilestonedIssues.slice(0, 5)) {
1417
+ const stateIcon = i.state.type === 'started' ? colors.yellow('→') : '○';
1418
+ console.log(` ${stateIcon} ${i.identifier}: ${i.title}`);
1419
+ }
1420
+ if (unmilestonedIssues.length > 5) {
1421
+ console.log(colors.gray(` ... and ${unmilestonedIssues.length - 5} more`));
1422
+ }
1423
+ }
1424
+
1425
+ console.log('');
1426
+ }
1427
+ }
1428
+
1429
+ // ============================================================================
1430
+ // REORDERING
1431
+ // ============================================================================
1432
+
1433
+ async function cmdProjectsReorder(args) {
1434
+ if (args.length < 2) {
1435
+ console.error(colors.red('Error: At least 2 project names required'));
1436
+ console.error('Usage: linear projects reorder "Project A" "Project B" "Project C"');
1437
+ process.exit(1);
1438
+ }
1439
+
1440
+ // Get all projects
1441
+ const projectsResult = await gql(`{
1442
+ team(id: "${TEAM_KEY}") {
1443
+ projects(first: 50) { nodes { id name sortOrder } }
1444
+ }
1445
+ }`);
1446
+ const allProjects = projectsResult.data?.team?.projects?.nodes || [];
1447
+
1448
+ // Match provided names to projects
1449
+ const orderedProjects = [];
1450
+ for (const name of args) {
1451
+ const match = allProjects.find(p => p.name.toLowerCase().includes(name.toLowerCase()));
1452
+ if (!match) {
1453
+ console.error(colors.red(`Project not found: ${name}`));
1454
+ process.exit(1);
1455
+ }
1456
+ orderedProjects.push(match);
1457
+ }
1458
+
1459
+ // Assign new sortOrders (highest first)
1460
+ const baseSort = Math.max(...allProjects.map(p => p.sortOrder || 0)) + 1000;
1461
+ const mutations = [];
1462
+
1463
+ for (let i = 0; i < orderedProjects.length; i++) {
1464
+ const newSortOrder = baseSort - (i * 1000);
1465
+ mutations.push(gql(`
1466
+ mutation {
1467
+ projectUpdate(id: "${orderedProjects[i].id}", input: { sortOrder: ${newSortOrder} }) {
1468
+ success
1469
+ }
1470
+ }
1471
+ `));
1472
+ }
1473
+
1474
+ await Promise.all(mutations);
1475
+ console.log(colors.green('Reordered projects:'));
1476
+ orderedProjects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`));
1477
+ }
1478
+
1479
+ async function cmdProjectMove(args) {
1480
+ const opts = parseArgs(args, {
1481
+ before: 'string',
1482
+ after: 'string',
1483
+ });
1484
+
1485
+ const projectName = opts._[0];
1486
+ const beforeName = opts.before;
1487
+ const afterName = opts.after;
1488
+
1489
+ if (!projectName) {
1490
+ console.error(colors.red('Error: Project name required'));
1491
+ console.error('Usage: linear project move "Project" --before "Other" or --after "Other"');
1492
+ process.exit(1);
1493
+ }
1494
+
1495
+ if (!beforeName && !afterName) {
1496
+ console.error(colors.red('Error: --before or --after required'));
1497
+ process.exit(1);
1498
+ }
1499
+
1500
+ // Get all projects
1501
+ const projectsResult = await gql(`{
1502
+ team(id: "${TEAM_KEY}") {
1503
+ projects(first: 50) { nodes { id name sortOrder } }
1504
+ }
1505
+ }`);
1506
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1507
+ projects.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1508
+
1509
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1510
+ const target = projects.find(p => p.name.toLowerCase().includes((beforeName || afterName).toLowerCase()));
1511
+
1512
+ if (!project) {
1513
+ console.error(colors.red(`Project not found: ${projectName}`));
1514
+ process.exit(1);
1515
+ }
1516
+ if (!target) {
1517
+ console.error(colors.red(`Target project not found: ${beforeName || afterName}`));
1518
+ process.exit(1);
1519
+ }
1520
+
1521
+ const targetIdx = projects.findIndex(p => p.id === target.id);
1522
+ let newSortOrder;
1523
+
1524
+ if (beforeName) {
1525
+ // Insert before target (higher sortOrder)
1526
+ const prevProject = projects[targetIdx - 1];
1527
+ if (prevProject) {
1528
+ newSortOrder = (target.sortOrder + prevProject.sortOrder) / 2;
1529
+ } else {
1530
+ newSortOrder = target.sortOrder + 1000;
1531
+ }
1532
+ } else {
1533
+ // Insert after target (lower sortOrder)
1534
+ const nextProject = projects[targetIdx + 1];
1535
+ if (nextProject) {
1536
+ newSortOrder = (target.sortOrder + nextProject.sortOrder) / 2;
1537
+ } else {
1538
+ newSortOrder = target.sortOrder - 1000;
1539
+ }
1540
+ }
1541
+
1542
+ await gql(`
1543
+ mutation {
1544
+ projectUpdate(id: "${project.id}", input: { sortOrder: ${newSortOrder} }) {
1545
+ success
1546
+ }
1547
+ }
1548
+ `);
1549
+
1550
+ console.log(colors.green(`Moved "${project.name}" ${beforeName ? 'before' : 'after'} "${target.name}"`));
1551
+ }
1552
+
1553
+ async function cmdMilestonesReorder(args) {
1554
+ const opts = parseArgs(args, { project: 'string', p: 'string' });
1555
+ const projectName = opts.project || opts.p;
1556
+ const milestoneNames = opts._;
1557
+
1558
+ if (!projectName) {
1559
+ console.error(colors.red('Error: --project required'));
1560
+ console.error('Usage: linear milestones reorder "M1" "M2" "M3" --project "Project"');
1561
+ process.exit(1);
1562
+ }
1563
+
1564
+ if (milestoneNames.length < 2) {
1565
+ console.error(colors.red('Error: At least 2 milestone names required'));
1566
+ process.exit(1);
1567
+ }
1568
+
1569
+ // Get project with milestones
1570
+ const projectsResult = await gql(`{
1571
+ team(id: "${TEAM_KEY}") {
1572
+ projects(first: 50) {
1573
+ nodes {
1574
+ id name
1575
+ projectMilestones { nodes { id name sortOrder } }
1576
+ }
1577
+ }
1578
+ }
1579
+ }`);
1580
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1581
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1582
+
1583
+ if (!project) {
1584
+ console.error(colors.red(`Project not found: ${projectName}`));
1585
+ process.exit(1);
1586
+ }
1587
+
1588
+ const allMilestones = project.projectMilestones?.nodes || [];
1589
+
1590
+ // Match provided names to milestones
1591
+ const orderedMilestones = [];
1592
+ for (const name of milestoneNames) {
1593
+ const match = allMilestones.find(m => m.name.toLowerCase().includes(name.toLowerCase()));
1594
+ if (!match) {
1595
+ console.error(colors.red(`Milestone not found: ${name}`));
1596
+ process.exit(1);
1597
+ }
1598
+ orderedMilestones.push(match);
1599
+ }
1600
+
1601
+ // Assign new sortOrders
1602
+ const baseSort = Math.max(...allMilestones.map(m => m.sortOrder || 0)) + 1000;
1603
+ const mutations = [];
1604
+
1605
+ for (let i = 0; i < orderedMilestones.length; i++) {
1606
+ const newSortOrder = baseSort - (i * 1000);
1607
+ mutations.push(gql(`
1608
+ mutation {
1609
+ projectMilestoneUpdate(id: "${orderedMilestones[i].id}", input: { sortOrder: ${newSortOrder} }) {
1610
+ success
1611
+ }
1612
+ }
1613
+ `));
1614
+ }
1615
+
1616
+ await Promise.all(mutations);
1617
+ console.log(colors.green(`Reordered milestones in ${project.name}:`));
1618
+ orderedMilestones.forEach((m, i) => console.log(` ${i + 1}. ${m.name}`));
1619
+ }
1620
+
1621
+ async function cmdMilestoneMove(args) {
1622
+ const opts = parseArgs(args, {
1623
+ before: 'string',
1624
+ after: 'string',
1625
+ });
1626
+
1627
+ const milestoneName = opts._[0];
1628
+ const beforeName = opts.before;
1629
+ const afterName = opts.after;
1630
+
1631
+ if (!milestoneName) {
1632
+ console.error(colors.red('Error: Milestone name required'));
1633
+ process.exit(1);
1634
+ }
1635
+
1636
+ if (!beforeName && !afterName) {
1637
+ console.error(colors.red('Error: --before or --after required'));
1638
+ process.exit(1);
1639
+ }
1640
+
1641
+ // Get all projects with milestones
1642
+ const projectsResult = await gql(`{
1643
+ team(id: "${TEAM_KEY}") {
1644
+ projects(first: 50) {
1645
+ nodes {
1646
+ id name
1647
+ projectMilestones { nodes { id name sortOrder } }
1648
+ }
1649
+ }
1650
+ }
1651
+ }`);
1652
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1653
+
1654
+ // Find milestone and its project
1655
+ let milestone = null;
1656
+ let projectMilestones = [];
1657
+ for (const p of projects) {
1658
+ const m = p.projectMilestones?.nodes?.find(m =>
1659
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
1660
+ );
1661
+ if (m) {
1662
+ milestone = m;
1663
+ projectMilestones = p.projectMilestones.nodes;
1664
+ projectMilestones.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1665
+ break;
1666
+ }
1667
+ }
1668
+
1669
+ if (!milestone) {
1670
+ console.error(colors.red(`Milestone not found: ${milestoneName}`));
1671
+ process.exit(1);
1672
+ }
1673
+
1674
+ const target = projectMilestones.find(m =>
1675
+ m.name.toLowerCase().includes((beforeName || afterName).toLowerCase())
1676
+ );
1677
+
1678
+ if (!target) {
1679
+ console.error(colors.red(`Target milestone not found: ${beforeName || afterName}`));
1680
+ process.exit(1);
1681
+ }
1682
+
1683
+ const targetIdx = projectMilestones.findIndex(m => m.id === target.id);
1684
+ let newSortOrder;
1685
+
1686
+ if (beforeName) {
1687
+ const prevMilestone = projectMilestones[targetIdx - 1];
1688
+ if (prevMilestone) {
1689
+ newSortOrder = (target.sortOrder + prevMilestone.sortOrder) / 2;
1690
+ } else {
1691
+ newSortOrder = target.sortOrder + 1000;
1692
+ }
1693
+ } else {
1694
+ const nextMilestone = projectMilestones[targetIdx + 1];
1695
+ if (nextMilestone) {
1696
+ newSortOrder = (target.sortOrder + nextMilestone.sortOrder) / 2;
1697
+ } else {
1698
+ newSortOrder = target.sortOrder - 1000;
1699
+ }
1700
+ }
1701
+
1702
+ await gql(`
1703
+ mutation {
1704
+ projectMilestoneUpdate(id: "${milestone.id}", input: { sortOrder: ${newSortOrder} }) {
1705
+ success
1706
+ }
1707
+ }
1708
+ `);
1709
+
1710
+ console.log(colors.green(`Moved "${milestone.name}" ${beforeName ? 'before' : 'after'} "${target.name}"`));
1711
+ }
1712
+
1713
+ async function cmdIssuesReorder(args) {
1714
+ if (args.length < 2) {
1715
+ console.error(colors.red('Error: At least 2 issue IDs required'));
1716
+ console.error('Usage: linear issues reorder ISSUE-1 ISSUE-2 ISSUE-3');
1717
+ process.exit(1);
1718
+ }
1719
+
1720
+ // Get issues to verify they exist
1721
+ const query = `{
1722
+ team(id: "${TEAM_KEY}") {
1723
+ issues(first: 100) {
1724
+ nodes { id identifier sortOrder }
1725
+ }
1726
+ }
1727
+ }`;
1728
+
1729
+ const result = await gql(query);
1730
+ const allIssues = result.data?.team?.issues?.nodes || [];
1731
+
1732
+ // Match provided IDs to issues
1733
+ const orderedIssues = [];
1734
+ for (const id of args) {
1735
+ const match = allIssues.find(i => i.identifier === id.toUpperCase());
1736
+ if (!match) {
1737
+ console.error(colors.red(`Issue not found: ${id}`));
1738
+ process.exit(1);
1739
+ }
1740
+ orderedIssues.push(match);
1741
+ }
1742
+
1743
+ // Assign new sortOrders
1744
+ const baseSort = Math.max(...allIssues.map(i => i.sortOrder || 0)) + 1000;
1745
+ const mutations = [];
1746
+
1747
+ for (let i = 0; i < orderedIssues.length; i++) {
1748
+ const newSortOrder = baseSort - (i * 1000);
1749
+ mutations.push(gql(`
1750
+ mutation {
1751
+ issueUpdate(id: "${orderedIssues[i].identifier}", input: { sortOrder: ${newSortOrder} }) {
1752
+ success
1753
+ }
1754
+ }
1755
+ `));
1756
+ }
1757
+
1758
+ await Promise.all(mutations);
1759
+ console.log(colors.green('Reordered issues:'));
1760
+ orderedIssues.forEach((i, idx) => console.log(` ${idx + 1}. ${i.identifier}`));
1761
+ }
1762
+
1763
+ async function cmdIssueMove(args) {
1764
+ const opts = parseArgs(args, {
1765
+ before: 'string',
1766
+ after: 'string',
1767
+ });
1768
+
1769
+ const issueId = opts._[0];
1770
+ const beforeId = opts.before;
1771
+ const afterId = opts.after;
1772
+
1773
+ if (!issueId) {
1774
+ console.error(colors.red('Error: Issue ID required'));
1775
+ console.error('Usage: linear issue move ISSUE-1 --before ISSUE-2 or --after ISSUE-2');
1776
+ process.exit(1);
1777
+ }
1778
+
1779
+ if (!beforeId && !afterId) {
1780
+ console.error(colors.red('Error: --before or --after required'));
1781
+ process.exit(1);
1782
+ }
1783
+
1784
+ // Get issues
1785
+ const query = `{
1786
+ team(id: "${TEAM_KEY}") {
1787
+ issues(first: 100) {
1788
+ nodes { id identifier sortOrder }
1789
+ }
1790
+ }
1791
+ }`;
1792
+
1793
+ const result = await gql(query);
1794
+ const issues = result.data?.team?.issues?.nodes || [];
1795
+ issues.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1796
+
1797
+ const issue = issues.find(i => i.identifier === issueId.toUpperCase());
1798
+ const target = issues.find(i => i.identifier === (beforeId || afterId).toUpperCase());
1799
+
1800
+ if (!issue) {
1801
+ console.error(colors.red(`Issue not found: ${issueId}`));
1802
+ process.exit(1);
1803
+ }
1804
+ if (!target) {
1805
+ console.error(colors.red(`Target issue not found: ${beforeId || afterId}`));
1806
+ process.exit(1);
1807
+ }
1808
+
1809
+ const targetIdx = issues.findIndex(i => i.identifier === target.identifier);
1810
+ let newSortOrder;
1811
+
1812
+ if (beforeId) {
1813
+ const prevIssue = issues[targetIdx - 1];
1814
+ if (prevIssue) {
1815
+ newSortOrder = (target.sortOrder + prevIssue.sortOrder) / 2;
1816
+ } else {
1817
+ newSortOrder = target.sortOrder + 1000;
1818
+ }
1819
+ } else {
1820
+ const nextIssue = issues[targetIdx + 1];
1821
+ if (nextIssue) {
1822
+ newSortOrder = (target.sortOrder + nextIssue.sortOrder) / 2;
1823
+ } else {
1824
+ newSortOrder = target.sortOrder - 1000;
1825
+ }
1826
+ }
1827
+
1828
+ await gql(`
1829
+ mutation {
1830
+ issueUpdate(id: "${issue.identifier}", input: { sortOrder: ${newSortOrder} }) {
1831
+ success
1832
+ }
1833
+ }
1834
+ `);
1835
+
1836
+ console.log(colors.green(`Moved ${issue.identifier} ${beforeId ? 'before' : 'after'} ${target.identifier}`));
1837
+ }
1838
+
988
1839
  // ============================================================================
989
1840
  // LABELS
990
1841
  // ============================================================================
@@ -1687,7 +2538,11 @@ async function cmdLogin(args) {
1687
2538
  console.log(' 2. Global, for all projects (~/.linear)');
1688
2539
  console.log('');
1689
2540
 
1690
- const locationChoice = await prompt('Enter number [1]: ') || '1';
2541
+ const locationChoice = await prompt('Enter number: ');
2542
+ if (locationChoice !== '1' && locationChoice !== '2') {
2543
+ console.error(colors.red('Error: Please enter 1 or 2'));
2544
+ process.exit(1);
2545
+ }
1691
2546
  const saveGlobal = locationChoice === '2';
1692
2547
  console.log('');
1693
2548
 
@@ -1729,10 +2584,15 @@ async function cmdLogin(args) {
1729
2584
  console.log(` ${teams.length + 1}. Create a new team...`);
1730
2585
  console.log('');
1731
2586
 
1732
- const selection = await prompt('Enter number [1]: ') || '1';
2587
+ const selection = await prompt('Enter number: ');
2588
+ const selectionNum = parseInt(selection);
2589
+ if (!selection || isNaN(selectionNum) || selectionNum < 1 || selectionNum > teams.length + 1) {
2590
+ console.error(colors.red('Error: Invalid selection'));
2591
+ process.exit(1);
2592
+ }
1733
2593
  let selectedKey = '';
1734
2594
 
1735
- if (parseInt(selection) === teams.length + 1) {
2595
+ if (selectionNum === teams.length + 1) {
1736
2596
  // Create new team
1737
2597
  console.log('');
1738
2598
  const teamName = await prompt('Team name: ');
@@ -1764,12 +2624,7 @@ async function cmdLogin(args) {
1764
2624
  process.exit(1);
1765
2625
  }
1766
2626
  } else {
1767
- const idx = parseInt(selection) - 1;
1768
- if (idx < 0 || idx >= teams.length) {
1769
- console.error(colors.red('Error: Invalid selection'));
1770
- process.exit(1);
1771
- }
1772
- selectedKey = teams[idx].key;
2627
+ selectedKey = teams[selectionNum - 1].key;
1773
2628
  }
1774
2629
 
1775
2630
  // Save config
@@ -1877,15 +2732,23 @@ AUTHENTICATION:
1877
2732
  logout Remove saved credentials
1878
2733
  whoami Show current user and team
1879
2734
 
2735
+ PLANNING:
2736
+ roadmap [options] Overview of projects, milestones, and issues
2737
+ --all, -a Include completed projects
2738
+
1880
2739
  ISSUES:
1881
- issues [options] List issues (yours shown first)
2740
+ issues [options] List issues (default: backlog, yours first)
1882
2741
  --unblocked, -u Show only unblocked issues
1883
2742
  --open, -o Show all non-completed/canceled issues
2743
+ --backlog, -b Show only backlog issues
1884
2744
  --all, -a Show all states (including completed)
1885
2745
  --mine, -m Show only issues assigned to you
1886
2746
  --in-progress Show issues in progress
1887
- --state, -s <state> Filter by state (default: backlog)
2747
+ --project, -p <name> Filter by project
2748
+ --milestone <name> Filter by milestone
2749
+ --state, -s <state> Filter by state
1888
2750
  --label, -l <name> Filter by label
2751
+ issues reorder <ids...> Reorder issues by listing IDs in order
1889
2752
 
1890
2753
  issue show <id> Show issue details with parent context
1891
2754
  issue start <id> Assign to yourself and set to In Progress
@@ -1893,6 +2756,7 @@ ISSUES:
1893
2756
  --title, -t <title> Issue title (required)
1894
2757
  --description, -d <desc> Issue description
1895
2758
  --project, -p <name> Add to project
2759
+ --milestone <name> Add to milestone
1896
2760
  --parent <id> Parent issue (for sub-issues)
1897
2761
  --assign Assign to yourself
1898
2762
  --estimate, -e <size> Estimate: XS, S, M, L, XL
@@ -1903,20 +2767,47 @@ ISSUES:
1903
2767
  --title, -t <title> New title
1904
2768
  --description, -d <desc> New description
1905
2769
  --state, -s <state> New state
2770
+ --project, -p <name> Move to project
2771
+ --milestone <name> Move to milestone
1906
2772
  --append, -a <text> Append to description
1907
2773
  --blocks <id> Add blocking relation
1908
2774
  --blocked-by <id> Add blocked-by relation
1909
2775
  issue close <id> Mark issue as done
1910
2776
  issue comment <id> <body> Add a comment
2777
+ issue move <id> Move issue in sort order
2778
+ --before <id> Move before this issue
2779
+ --after <id> Move after this issue
1911
2780
 
1912
2781
  PROJECTS:
1913
2782
  projects [options] List projects
1914
2783
  --all, -a Include completed projects
1915
- project show <name> Show project details
2784
+ projects reorder <names..> Reorder projects by listing names in order
2785
+
2786
+ project show <name> Show project details with issues
1916
2787
  project create [options] Create a new project
1917
2788
  --name, -n <name> Project name (required)
1918
2789
  --description, -d <desc> Project description
1919
2790
  project complete <name> Mark project as completed
2791
+ project move <name> Move project in sort order
2792
+ --before <name> Move before this project
2793
+ --after <name> Move after this project
2794
+
2795
+ MILESTONES:
2796
+ milestones [options] List milestones by project
2797
+ --project, -p <name> Filter by project
2798
+ --all, -a Include completed projects
2799
+ milestones reorder <names> Reorder milestones (requires --project)
2800
+ --project, -p <name> Project containing milestones
2801
+
2802
+ milestone show <name> Show milestone with issues
2803
+ milestone create [options] Create a new milestone
2804
+ --name, -n <name> Milestone name (required)
2805
+ --project, -p <name> Project (required)
2806
+ --description, -d <desc> Milestone description
2807
+ --target-date <date> Target date (YYYY-MM-DD)
2808
+ milestone move <name> Move milestone in sort order
2809
+ --before <name> Move before this milestone
2810
+ --after <name> Move after this milestone
1920
2811
 
1921
2812
  LABELS:
1922
2813
  labels List all labels
@@ -1948,15 +2839,12 @@ CONFIGURATION:
1948
2839
  team=ISSUE
1949
2840
 
1950
2841
  EXAMPLES:
1951
- linear login # First-time setup
2842
+ linear roadmap # See all projects and milestones
1952
2843
  linear issues --unblocked # Find workable issues
1953
- linear issues --in-progress # See what you're working on
1954
- linear issue show ISSUE-1 # View with parent context
1955
- linear issue start ISSUE-1 # Assign and start working
1956
- linear issue create --title "Fix bug" --project "Phase 1" --assign --estimate M
1957
- linear issue create --title "Needs API key" --blocked-by ISSUE-5
1958
- linear issue update ISSUE-1 --append "Found the root cause..."
1959
- linear branch ISSUE-5 # Create git branch for issue
2844
+ linear issues --project "Phase 1" # Issues in a project
2845
+ linear issue create --title "Fix bug" --milestone "Beta" --estimate M
2846
+ linear projects reorder "Phase 1" "Phase 2" "Phase 3"
2847
+ linear project move "Phase 3" --before "Phase 1"
1960
2848
  `);
1961
2849
  }
1962
2850
 
@@ -1986,10 +2874,16 @@ async function main() {
1986
2874
  case 'whoami':
1987
2875
  await cmdWhoami();
1988
2876
  break;
1989
- case 'issues':
2877
+ case 'issues': {
1990
2878
  checkAuth();
1991
- await cmdIssues(args.slice(1));
2879
+ // Check for "issues reorder" subcommand
2880
+ if (args[1] === 'reorder') {
2881
+ await cmdIssuesReorder(args.slice(2));
2882
+ } else {
2883
+ await cmdIssues(args.slice(1));
2884
+ }
1992
2885
  break;
2886
+ }
1993
2887
  case 'issue': {
1994
2888
  checkAuth();
1995
2889
  const subcmd = args[1];
@@ -2001,16 +2895,23 @@ async function main() {
2001
2895
  case 'start': await cmdIssueStart(subargs); break;
2002
2896
  case 'close': await cmdIssueClose(subargs); break;
2003
2897
  case 'comment': await cmdIssueComment(subargs); break;
2898
+ case 'move': await cmdIssueMove(subargs); break;
2004
2899
  default:
2005
2900
  console.error(`Unknown issue command: ${subcmd}`);
2006
2901
  process.exit(1);
2007
2902
  }
2008
2903
  break;
2009
2904
  }
2010
- case 'projects':
2905
+ case 'projects': {
2011
2906
  checkAuth();
2012
- await cmdProjects(args.slice(1));
2907
+ // Check for "projects reorder" subcommand
2908
+ if (args[1] === 'reorder') {
2909
+ await cmdProjectsReorder(args.slice(2));
2910
+ } else {
2911
+ await cmdProjects(args.slice(1));
2912
+ }
2013
2913
  break;
2914
+ }
2014
2915
  case 'project': {
2015
2916
  checkAuth();
2016
2917
  const subcmd = args[1];
@@ -2019,12 +2920,41 @@ async function main() {
2019
2920
  case 'show': await cmdProjectShow(subargs); break;
2020
2921
  case 'create': await cmdProjectCreate(subargs); break;
2021
2922
  case 'complete': await cmdProjectComplete(subargs); break;
2923
+ case 'move': await cmdProjectMove(subargs); break;
2022
2924
  default:
2023
2925
  console.error(`Unknown project command: ${subcmd}`);
2024
2926
  process.exit(1);
2025
2927
  }
2026
2928
  break;
2027
2929
  }
2930
+ case 'milestones': {
2931
+ checkAuth();
2932
+ // Check for "milestones reorder" subcommand
2933
+ if (args[1] === 'reorder') {
2934
+ await cmdMilestonesReorder(args.slice(2));
2935
+ } else {
2936
+ await cmdMilestones(args.slice(1));
2937
+ }
2938
+ break;
2939
+ }
2940
+ case 'milestone': {
2941
+ checkAuth();
2942
+ const subcmd = args[1];
2943
+ const subargs = args.slice(2);
2944
+ switch (subcmd) {
2945
+ case 'show': await cmdMilestoneShow(subargs); break;
2946
+ case 'create': await cmdMilestoneCreate(subargs); break;
2947
+ case 'move': await cmdMilestoneMove(subargs); break;
2948
+ default:
2949
+ console.error(`Unknown milestone command: ${subcmd}`);
2950
+ process.exit(1);
2951
+ }
2952
+ break;
2953
+ }
2954
+ case 'roadmap':
2955
+ checkAuth();
2956
+ await cmdRoadmap(args.slice(1));
2957
+ break;
2028
2958
  case 'labels':
2029
2959
  checkAuth();
2030
2960
  await cmdLabels();