mindsystem-cc 3.0.0 → 3.2.0
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/README.md +179 -332
- package/agents/ms-executor.md +0 -1
- package/agents/ms-roadmapper.md +0 -1
- package/bin/install.js +2 -2
- package/commands/ms/help.md +0 -30
- package/commands/ms/linear.md +231 -0
- package/commands/ms/progress.md +16 -2
- package/commands/ms/remove-phase.md +0 -2
- package/mindsystem/templates/state.md +0 -13
- package/mindsystem/workflows/execute-plan.md +0 -1
- package/mindsystem/workflows/transition.md +0 -14
- package/package.json +1 -1
- package/scripts/ms-linear/ms_linear/__init__.py +3 -0
- package/scripts/ms-linear/ms_linear/__main__.py +6 -0
- package/scripts/ms-linear/ms_linear/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/__main__.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/cli.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/client.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/config.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/errors.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/__pycache__/output.cpython-314.pyc +0 -0
- package/scripts/ms-linear/ms_linear/cli.py +604 -0
- package/scripts/ms-linear/ms_linear/client.py +503 -0
- package/scripts/ms-linear/ms_linear/config.py +102 -0
- package/scripts/ms-linear/ms_linear/errors.py +29 -0
- package/scripts/ms-linear/ms_linear/output.py +45 -0
- package/scripts/ms-linear/pyproject.toml +16 -0
- package/scripts/ms-linear/uv.lock +196 -0
- package/scripts/ms-linear-wrapper.sh +21 -0
- package/commands/ms/pause-work.md +0 -123
- package/commands/ms/resume-work.md +0 -40
- package/mindsystem/templates/continue-here.md +0 -78
- package/mindsystem/workflows/resume-project.md +0 -311
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""Main CLI entry point for ms-linear."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ms_linear import __version__
|
|
9
|
+
from ms_linear.client import LinearClient
|
|
10
|
+
from ms_linear.config import load_config
|
|
11
|
+
from ms_linear.errors import MsLinearError
|
|
12
|
+
from ms_linear.output import format_error, format_success, output_json
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="ms-linear",
|
|
16
|
+
help="Mindsystem Linear CLI - create, update, and manage Linear issues",
|
|
17
|
+
add_completion=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def version_callback(value: bool) -> None:
|
|
22
|
+
"""Print version and exit."""
|
|
23
|
+
if value:
|
|
24
|
+
typer.echo(f"ms-linear {__version__}")
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def main(
|
|
30
|
+
version: Optional[bool] = typer.Option(
|
|
31
|
+
None,
|
|
32
|
+
"--version",
|
|
33
|
+
"-v",
|
|
34
|
+
callback=version_callback,
|
|
35
|
+
is_eager=True,
|
|
36
|
+
help="Show version and exit",
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Mindsystem Linear CLI - create, update, and manage Linear issues."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def create(
|
|
45
|
+
title: str = typer.Argument(..., help="Issue title"),
|
|
46
|
+
description: Optional[str] = typer.Option(
|
|
47
|
+
None,
|
|
48
|
+
"--description",
|
|
49
|
+
"-d",
|
|
50
|
+
help="Issue description (markdown)",
|
|
51
|
+
),
|
|
52
|
+
priority: Optional[int] = typer.Option(
|
|
53
|
+
None,
|
|
54
|
+
"--priority",
|
|
55
|
+
"-p",
|
|
56
|
+
help="Priority: 0=None, 1=Urgent, 2=High, 3=Normal, 4=Low",
|
|
57
|
+
),
|
|
58
|
+
estimate: Optional[int] = typer.Option(
|
|
59
|
+
None,
|
|
60
|
+
"--estimate",
|
|
61
|
+
"-e",
|
|
62
|
+
help="Estimate (team-specific scale)",
|
|
63
|
+
),
|
|
64
|
+
parent: Optional[str] = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
"--parent",
|
|
67
|
+
help="Parent issue ID for sub-issues",
|
|
68
|
+
),
|
|
69
|
+
project: Optional[str] = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"--project",
|
|
72
|
+
help="Project name (resolved to ID) - overrides config default",
|
|
73
|
+
),
|
|
74
|
+
no_project: bool = typer.Option(
|
|
75
|
+
False,
|
|
76
|
+
"--no-project",
|
|
77
|
+
help="Don't assign to any project (ignores config default)",
|
|
78
|
+
),
|
|
79
|
+
json_pretty: bool = typer.Option(
|
|
80
|
+
False,
|
|
81
|
+
"--json-pretty",
|
|
82
|
+
"-P",
|
|
83
|
+
help="Pretty-print JSON output",
|
|
84
|
+
),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Create a new issue.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
ms-linear create "Fix login bug"
|
|
90
|
+
ms-linear create "Add OAuth" -d "Support Google and GitHub OAuth"
|
|
91
|
+
ms-linear create "Auth subtask" --parent ABC-123
|
|
92
|
+
ms-linear create "Mobile bug" --project "Mobile App"
|
|
93
|
+
ms-linear create "Backlog item" --no-project
|
|
94
|
+
"""
|
|
95
|
+
command = "create"
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
config = load_config()
|
|
99
|
+
client = LinearClient()
|
|
100
|
+
|
|
101
|
+
# Resolve project name to ID if provided
|
|
102
|
+
project_id = None
|
|
103
|
+
if project:
|
|
104
|
+
project_info = client.find_project_by_name(project, config.team_id)
|
|
105
|
+
project_id = project_info["id"]
|
|
106
|
+
|
|
107
|
+
issue = client.create_issue(
|
|
108
|
+
config=config,
|
|
109
|
+
title=title,
|
|
110
|
+
description=description,
|
|
111
|
+
priority=priority,
|
|
112
|
+
estimate=estimate,
|
|
113
|
+
parent_id=parent,
|
|
114
|
+
project_id=project_id,
|
|
115
|
+
no_project=no_project,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
metadata = {
|
|
119
|
+
"teamId": config.team_id,
|
|
120
|
+
}
|
|
121
|
+
if project_id:
|
|
122
|
+
metadata["projectId"] = project_id
|
|
123
|
+
elif config.project_id and not no_project:
|
|
124
|
+
metadata["projectId"] = config.project_id
|
|
125
|
+
|
|
126
|
+
response = format_success(
|
|
127
|
+
command=command,
|
|
128
|
+
result={
|
|
129
|
+
"identifier": issue.get("identifier"),
|
|
130
|
+
"title": issue.get("title"),
|
|
131
|
+
"url": issue.get("url"),
|
|
132
|
+
"state": issue.get("state", {}).get("name"),
|
|
133
|
+
},
|
|
134
|
+
metadata=metadata,
|
|
135
|
+
)
|
|
136
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
137
|
+
|
|
138
|
+
except MsLinearError as e:
|
|
139
|
+
error_response = format_error(command, e)
|
|
140
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
141
|
+
raise typer.Exit(code=1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def update(
|
|
146
|
+
issue_id: str = typer.Argument(..., help="Issue ID (e.g., ABC-123)"),
|
|
147
|
+
title: Optional[str] = typer.Option(
|
|
148
|
+
None,
|
|
149
|
+
"--title",
|
|
150
|
+
"-t",
|
|
151
|
+
help="New title",
|
|
152
|
+
),
|
|
153
|
+
description: Optional[str] = typer.Option(
|
|
154
|
+
None,
|
|
155
|
+
"--description",
|
|
156
|
+
"-d",
|
|
157
|
+
help="New description",
|
|
158
|
+
),
|
|
159
|
+
priority: Optional[int] = typer.Option(
|
|
160
|
+
None,
|
|
161
|
+
"--priority",
|
|
162
|
+
"-p",
|
|
163
|
+
help="New priority: 0=None, 1=Urgent, 2=High, 3=Normal, 4=Low",
|
|
164
|
+
),
|
|
165
|
+
json_pretty: bool = typer.Option(
|
|
166
|
+
False,
|
|
167
|
+
"--json-pretty",
|
|
168
|
+
"-P",
|
|
169
|
+
help="Pretty-print JSON output",
|
|
170
|
+
),
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Update an existing issue.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
ms-linear update ABC-123 -t "New title"
|
|
176
|
+
ms-linear update ABC-123 -d "Updated description"
|
|
177
|
+
ms-linear update ABC-123 -p 2
|
|
178
|
+
"""
|
|
179
|
+
command = "update"
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
client = LinearClient()
|
|
183
|
+
|
|
184
|
+
issue = client.update_issue(
|
|
185
|
+
issue_id=issue_id,
|
|
186
|
+
title=title,
|
|
187
|
+
description=description,
|
|
188
|
+
priority=priority,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
response = format_success(
|
|
192
|
+
command=command,
|
|
193
|
+
result={
|
|
194
|
+
"identifier": issue.get("identifier"),
|
|
195
|
+
"title": issue.get("title"),
|
|
196
|
+
"url": issue.get("url"),
|
|
197
|
+
"state": issue.get("state", {}).get("name"),
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
201
|
+
|
|
202
|
+
except MsLinearError as e:
|
|
203
|
+
error_response = format_error(command, e)
|
|
204
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
205
|
+
raise typer.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def done(
|
|
210
|
+
issue_id: str = typer.Argument(..., help="Issue ID (e.g., ABC-123)"),
|
|
211
|
+
json_pretty: bool = typer.Option(
|
|
212
|
+
False,
|
|
213
|
+
"--json-pretty",
|
|
214
|
+
"-P",
|
|
215
|
+
help="Pretty-print JSON output",
|
|
216
|
+
),
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Mark an issue as completed.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
ms-linear done ABC-123
|
|
222
|
+
"""
|
|
223
|
+
command = "done"
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
client = LinearClient()
|
|
227
|
+
issue = client.mark_done(issue_id)
|
|
228
|
+
|
|
229
|
+
response = format_success(
|
|
230
|
+
command=command,
|
|
231
|
+
result={
|
|
232
|
+
"identifier": issue.get("identifier"),
|
|
233
|
+
"title": issue.get("title"),
|
|
234
|
+
"url": issue.get("url"),
|
|
235
|
+
"state": issue.get("state", {}).get("name"),
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
239
|
+
|
|
240
|
+
except MsLinearError as e:
|
|
241
|
+
error_response = format_error(command, e)
|
|
242
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
243
|
+
raise typer.Exit(code=1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@app.command()
|
|
247
|
+
def state(
|
|
248
|
+
issue_id: str = typer.Argument(..., help="Issue ID (e.g., ABC-123)"),
|
|
249
|
+
state_name: str = typer.Argument(..., help="Target state name"),
|
|
250
|
+
json_pretty: bool = typer.Option(
|
|
251
|
+
False,
|
|
252
|
+
"--json-pretty",
|
|
253
|
+
"-P",
|
|
254
|
+
help="Pretty-print JSON output",
|
|
255
|
+
),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Change an issue's state.
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
ms-linear state ABC-123 "In Progress"
|
|
261
|
+
ms-linear state ABC-123 "Done"
|
|
262
|
+
ms-linear state ABC-123 "Backlog"
|
|
263
|
+
"""
|
|
264
|
+
command = "state"
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
client = LinearClient()
|
|
268
|
+
issue = client.change_state(issue_id, state_name)
|
|
269
|
+
|
|
270
|
+
response = format_success(
|
|
271
|
+
command=command,
|
|
272
|
+
result={
|
|
273
|
+
"identifier": issue.get("identifier"),
|
|
274
|
+
"title": issue.get("title"),
|
|
275
|
+
"url": issue.get("url"),
|
|
276
|
+
"state": issue.get("state", {}).get("name"),
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
280
|
+
|
|
281
|
+
except MsLinearError as e:
|
|
282
|
+
error_response = format_error(command, e)
|
|
283
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
284
|
+
raise typer.Exit(code=1)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command("break")
|
|
288
|
+
def break_issue(
|
|
289
|
+
issue_id: str = typer.Argument(..., help="Parent issue ID (e.g., ABC-123)"),
|
|
290
|
+
issues: str = typer.Option(
|
|
291
|
+
...,
|
|
292
|
+
"--issues",
|
|
293
|
+
"-i",
|
|
294
|
+
help='JSON array of sub-issues: [{"title": "...", "description": "...", "priority": N, "estimate": N}]',
|
|
295
|
+
),
|
|
296
|
+
project: Optional[str] = typer.Option(
|
|
297
|
+
None,
|
|
298
|
+
"--project",
|
|
299
|
+
help="Project name for sub-issues (default: inherit from parent)",
|
|
300
|
+
),
|
|
301
|
+
no_project: bool = typer.Option(
|
|
302
|
+
False,
|
|
303
|
+
"--no-project",
|
|
304
|
+
help="Don't assign sub-issues to any project",
|
|
305
|
+
),
|
|
306
|
+
json_pretty: bool = typer.Option(
|
|
307
|
+
False,
|
|
308
|
+
"--json-pretty",
|
|
309
|
+
"-P",
|
|
310
|
+
help="Pretty-print JSON output",
|
|
311
|
+
),
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Break down an issue into sub-issues.
|
|
314
|
+
|
|
315
|
+
The --issues parameter must be a JSON array of objects with at minimum a "title" field.
|
|
316
|
+
Optional fields: description, priority, estimate.
|
|
317
|
+
|
|
318
|
+
By default, sub-issues inherit the parent's project. Use --project to override
|
|
319
|
+
or --no-project to create without project assignment.
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
ms-linear break ABC-123 --issues '[{"title": "Design"}, {"title": "Implement"}, {"title": "Test"}]'
|
|
323
|
+
ms-linear break ABC-123 --issues '[{"title": "Task"}]' --project "Backend"
|
|
324
|
+
"""
|
|
325
|
+
command = "break"
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
config = load_config()
|
|
329
|
+
client = LinearClient()
|
|
330
|
+
|
|
331
|
+
# Parse JSON issues
|
|
332
|
+
try:
|
|
333
|
+
issues_data = json.loads(issues)
|
|
334
|
+
except json.JSONDecodeError as e:
|
|
335
|
+
from ms_linear.errors import ErrorCode, MsLinearError
|
|
336
|
+
|
|
337
|
+
raise MsLinearError(
|
|
338
|
+
code=ErrorCode.INVALID_INPUT,
|
|
339
|
+
message=f"Invalid JSON for --issues: {e}",
|
|
340
|
+
suggestions=["Ensure --issues is valid JSON array"],
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if not isinstance(issues_data, list):
|
|
344
|
+
from ms_linear.errors import ErrorCode, MsLinearError
|
|
345
|
+
|
|
346
|
+
raise MsLinearError(
|
|
347
|
+
code=ErrorCode.INVALID_INPUT,
|
|
348
|
+
message="--issues must be a JSON array",
|
|
349
|
+
suggestions=['Use format: [{"title": "..."}, {"title": "..."}]'],
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Resolve project name to ID if provided
|
|
353
|
+
project_id = None
|
|
354
|
+
if project:
|
|
355
|
+
project_info = client.find_project_by_name(project, config.team_id)
|
|
356
|
+
project_id = project_info["id"]
|
|
357
|
+
|
|
358
|
+
created = client.create_sub_issues(
|
|
359
|
+
config, issue_id, issues_data, project_id=project_id, no_project=no_project
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
response = format_success(
|
|
363
|
+
command=command,
|
|
364
|
+
result={
|
|
365
|
+
"parent": issue_id,
|
|
366
|
+
"created": [
|
|
367
|
+
{
|
|
368
|
+
"identifier": i.get("identifier"),
|
|
369
|
+
"title": i.get("title"),
|
|
370
|
+
"url": i.get("url"),
|
|
371
|
+
}
|
|
372
|
+
for i in created
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
metadata={
|
|
376
|
+
"count": len(created),
|
|
377
|
+
"teamId": config.team_id,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
381
|
+
|
|
382
|
+
except MsLinearError as e:
|
|
383
|
+
error_response = format_error(command, e)
|
|
384
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
385
|
+
raise typer.Exit(code=1)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@app.command()
|
|
389
|
+
def get(
|
|
390
|
+
issue_id: str = typer.Argument(..., help="Issue ID (e.g., ABC-123)"),
|
|
391
|
+
json_pretty: bool = typer.Option(
|
|
392
|
+
False,
|
|
393
|
+
"--json-pretty",
|
|
394
|
+
"-P",
|
|
395
|
+
help="Pretty-print JSON output",
|
|
396
|
+
),
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Fetch issue details.
|
|
399
|
+
|
|
400
|
+
Examples:
|
|
401
|
+
ms-linear get ABC-123
|
|
402
|
+
"""
|
|
403
|
+
command = "get"
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
client = LinearClient()
|
|
407
|
+
issue = client.get_issue(issue_id)
|
|
408
|
+
|
|
409
|
+
# Format children if present
|
|
410
|
+
children = issue.get("children", {}).get("nodes", [])
|
|
411
|
+
children_data = [
|
|
412
|
+
{
|
|
413
|
+
"identifier": c.get("identifier"),
|
|
414
|
+
"title": c.get("title"),
|
|
415
|
+
"state": c.get("state", {}).get("name"),
|
|
416
|
+
}
|
|
417
|
+
for c in children
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
result = {
|
|
421
|
+
"identifier": issue.get("identifier"),
|
|
422
|
+
"title": issue.get("title"),
|
|
423
|
+
"description": issue.get("description"),
|
|
424
|
+
"priority": issue.get("priority"),
|
|
425
|
+
"estimate": issue.get("estimate"),
|
|
426
|
+
"url": issue.get("url"),
|
|
427
|
+
"state": {
|
|
428
|
+
"id": issue.get("state", {}).get("id"),
|
|
429
|
+
"name": issue.get("state", {}).get("name"),
|
|
430
|
+
"type": issue.get("state", {}).get("type"),
|
|
431
|
+
},
|
|
432
|
+
"team": {
|
|
433
|
+
"id": issue.get("team", {}).get("id"),
|
|
434
|
+
"key": issue.get("team", {}).get("key"),
|
|
435
|
+
"name": issue.get("team", {}).get("name"),
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Add parent if exists
|
|
440
|
+
parent = issue.get("parent")
|
|
441
|
+
if parent:
|
|
442
|
+
result["parent"] = {
|
|
443
|
+
"identifier": parent.get("identifier"),
|
|
444
|
+
"title": parent.get("title"),
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# Add children if any
|
|
448
|
+
if children_data:
|
|
449
|
+
result["children"] = children_data
|
|
450
|
+
|
|
451
|
+
# Add project if exists
|
|
452
|
+
project = issue.get("project")
|
|
453
|
+
if project:
|
|
454
|
+
result["project"] = {
|
|
455
|
+
"id": project.get("id"),
|
|
456
|
+
"name": project.get("name"),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
response = format_success(command=command, result=result)
|
|
460
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
461
|
+
|
|
462
|
+
except MsLinearError as e:
|
|
463
|
+
error_response = format_error(command, e)
|
|
464
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
465
|
+
raise typer.Exit(code=1)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@app.command()
|
|
469
|
+
def states(
|
|
470
|
+
team_id: Optional[str] = typer.Option(
|
|
471
|
+
None,
|
|
472
|
+
"--team",
|
|
473
|
+
"-t",
|
|
474
|
+
help="Filter by team ID (optional)",
|
|
475
|
+
),
|
|
476
|
+
json_pretty: bool = typer.Option(
|
|
477
|
+
False,
|
|
478
|
+
"--json-pretty",
|
|
479
|
+
"-P",
|
|
480
|
+
help="Pretty-print JSON output",
|
|
481
|
+
),
|
|
482
|
+
) -> None:
|
|
483
|
+
"""List workflow states.
|
|
484
|
+
|
|
485
|
+
Without --team, returns all states across all teams.
|
|
486
|
+
With --team, returns states for that specific team.
|
|
487
|
+
|
|
488
|
+
Examples:
|
|
489
|
+
ms-linear states
|
|
490
|
+
ms-linear states --team abc123-team-uuid
|
|
491
|
+
"""
|
|
492
|
+
command = "states"
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
client = LinearClient()
|
|
496
|
+
states_list = client.get_workflow_states(team_id)
|
|
497
|
+
|
|
498
|
+
# Group by team for cleaner output
|
|
499
|
+
teams: dict[str, dict] = {}
|
|
500
|
+
for state in states_list:
|
|
501
|
+
team = state.get("team", {})
|
|
502
|
+
team_key = team.get("key", "unknown")
|
|
503
|
+
if team_key not in teams:
|
|
504
|
+
teams[team_key] = {
|
|
505
|
+
"id": team.get("id"),
|
|
506
|
+
"key": team_key,
|
|
507
|
+
"name": team.get("name"),
|
|
508
|
+
"states": [],
|
|
509
|
+
}
|
|
510
|
+
teams[team_key]["states"].append(
|
|
511
|
+
{
|
|
512
|
+
"id": state.get("id"),
|
|
513
|
+
"name": state.get("name"),
|
|
514
|
+
"type": state.get("type"),
|
|
515
|
+
"position": state.get("position"),
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Sort states by position within each team
|
|
520
|
+
for team_data in teams.values():
|
|
521
|
+
team_data["states"].sort(key=lambda s: s.get("position", 0))
|
|
522
|
+
|
|
523
|
+
response = format_success(
|
|
524
|
+
command=command,
|
|
525
|
+
result={"teams": list(teams.values())},
|
|
526
|
+
metadata={"totalStates": len(states_list)},
|
|
527
|
+
)
|
|
528
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
529
|
+
|
|
530
|
+
except MsLinearError as e:
|
|
531
|
+
error_response = format_error(command, e)
|
|
532
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
533
|
+
raise typer.Exit(code=1)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@app.command()
|
|
537
|
+
def projects(
|
|
538
|
+
team_id: Optional[str] = typer.Option(
|
|
539
|
+
None,
|
|
540
|
+
"--team",
|
|
541
|
+
"-t",
|
|
542
|
+
help="Filter by team ID (optional)",
|
|
543
|
+
),
|
|
544
|
+
json_pretty: bool = typer.Option(
|
|
545
|
+
False,
|
|
546
|
+
"--json-pretty",
|
|
547
|
+
"-P",
|
|
548
|
+
help="Pretty-print JSON output",
|
|
549
|
+
),
|
|
550
|
+
) -> None:
|
|
551
|
+
"""List projects.
|
|
552
|
+
|
|
553
|
+
Without --team, returns all projects across all teams.
|
|
554
|
+
With --team, returns projects for that specific team.
|
|
555
|
+
|
|
556
|
+
Examples:
|
|
557
|
+
ms-linear projects
|
|
558
|
+
ms-linear projects --team abc123-team-uuid
|
|
559
|
+
"""
|
|
560
|
+
command = "projects"
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
client = LinearClient()
|
|
564
|
+
projects_list = client.get_projects(team_id)
|
|
565
|
+
|
|
566
|
+
# Group by team for cleaner output
|
|
567
|
+
teams: dict[str, dict] = {}
|
|
568
|
+
for project in projects_list:
|
|
569
|
+
team = project.get("team", {})
|
|
570
|
+
team_key = team.get("key", "unknown")
|
|
571
|
+
if team_key not in teams:
|
|
572
|
+
teams[team_key] = {
|
|
573
|
+
"id": team.get("id"),
|
|
574
|
+
"key": team_key,
|
|
575
|
+
"name": team.get("name"),
|
|
576
|
+
"projects": [],
|
|
577
|
+
}
|
|
578
|
+
teams[team_key]["projects"].append(
|
|
579
|
+
{
|
|
580
|
+
"id": project.get("id"),
|
|
581
|
+
"name": project.get("name"),
|
|
582
|
+
"state": project.get("state"),
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Sort projects by name within each team
|
|
587
|
+
for team_data in teams.values():
|
|
588
|
+
team_data["projects"].sort(key=lambda p: p.get("name", "").lower())
|
|
589
|
+
|
|
590
|
+
response = format_success(
|
|
591
|
+
command=command,
|
|
592
|
+
result={"teams": list(teams.values())},
|
|
593
|
+
metadata={"totalProjects": len(projects_list)},
|
|
594
|
+
)
|
|
595
|
+
typer.echo(output_json(response, pretty=json_pretty))
|
|
596
|
+
|
|
597
|
+
except MsLinearError as e:
|
|
598
|
+
error_response = format_error(command, e)
|
|
599
|
+
typer.echo(output_json(error_response, pretty=json_pretty))
|
|
600
|
+
raise typer.Exit(code=1)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
if __name__ == "__main__":
|
|
604
|
+
app()
|