@phamvuhoang/otto-core 0.3.0 → 0.4.1

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 (45) hide show
  1. package/dist/cli-help.d.ts +22 -3
  2. package/dist/cli-help.d.ts.map +1 -1
  3. package/dist/cli-help.js +9 -7
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/linear-api.d.ts +161 -0
  10. package/dist/linear-api.d.ts.map +1 -0
  11. package/dist/linear-api.js +296 -0
  12. package/dist/linear-api.js.map +1 -0
  13. package/dist/linear-auth.d.ts +44 -0
  14. package/dist/linear-auth.d.ts.map +1 -0
  15. package/dist/linear-auth.js +145 -0
  16. package/dist/linear-auth.js.map +1 -0
  17. package/dist/linear-cli.d.ts +37 -0
  18. package/dist/linear-cli.d.ts.map +1 -0
  19. package/dist/linear-cli.js +180 -0
  20. package/dist/linear-cli.js.map +1 -0
  21. package/dist/linear-main.d.ts +5 -0
  22. package/dist/linear-main.d.ts.map +1 -0
  23. package/dist/linear-main.js +27 -0
  24. package/dist/linear-main.js.map +1 -0
  25. package/dist/preflight.d.ts +3 -0
  26. package/dist/preflight.d.ts.map +1 -1
  27. package/dist/preflight.js +13 -1
  28. package/dist/preflight.js.map +1 -1
  29. package/dist/run-bin.d.ts +21 -1
  30. package/dist/run-bin.d.ts.map +1 -1
  31. package/dist/run-bin.js +18 -6
  32. package/dist/run-bin.js.map +1 -1
  33. package/dist/stages.d.ts +10 -0
  34. package/dist/stages.d.ts.map +1 -1
  35. package/dist/stages.js +10 -0
  36. package/dist/stages.js.map +1 -1
  37. package/dist/watch.d.ts +37 -2
  38. package/dist/watch.d.ts.map +1 -1
  39. package/dist/watch.js +39 -4
  40. package/dist/watch.js.map +1 -1
  41. package/package.json +1 -1
  42. package/templates/linear-completion.md +17 -0
  43. package/templates/linearafk-issue.md +31 -0
  44. package/templates/linearafk.md +29 -0
  45. package/templates/linearprompt.md +39 -0
@@ -0,0 +1,296 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ // A Linear issue identifier: team key (letter then letters/digits) + "-" +
5
+ // positive number (no leading zero — mirrors parseIssueRef strictness).
6
+ const IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]*-[1-9]\d*$/;
7
+ // Issue UUID (RFC-4122 shape; we don't validate the version nibble).
8
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
9
+ // `https://linear.app/<workspace>/issue/<IDENTIFIER>[/<slug>...]` — the
10
+ // identifier is the path segment after `/issue/`. Anchored to the linear.app
11
+ // host so an arbitrary URL merely *containing* `/issue/ENG-1/` (e.g.
12
+ // `https://evil.example/issue/ENG-1/`) does not silently resolve to a ref.
13
+ const URL_IDENTIFIER_RE = /^https?:\/\/linear\.app\/[^/]+\/issue\/([A-Za-z][A-Za-z0-9]*-[1-9]\d*)(?:[/?#]|$)/;
14
+ /**
15
+ * Normalize a user-supplied Linear issue reference to a {@link LinearRef}.
16
+ * Accepts a Linear identifier (`ENG-123`), an issue UUID, or a Linear issue
17
+ * URL (`https://linear.app/acme/issue/ENG-123/slug`). Team keys are uppercased
18
+ * to Linear's canonical form; UUIDs are lowercased. Throws on anything else.
19
+ *
20
+ * SECURITY: like {@link parseIssueRef} in cli-help.ts, the normalized value is
21
+ * the only part of a ref that may reach a shell (via a static template command
22
+ * reading an env var). The identifier/UUID regexes admit only `[A-Za-z0-9-]`,
23
+ * so a value like `$(rm -rf ~)` can never survive parsing.
24
+ */
25
+ export function parseLinearRef(raw) {
26
+ const s = raw.trim();
27
+ const urlMatch = s.match(URL_IDENTIFIER_RE);
28
+ if (urlMatch) {
29
+ return { kind: "identifier", identifier: urlMatch[1].toUpperCase() };
30
+ }
31
+ if (UUID_RE.test(s)) {
32
+ return { kind: "uuid", uuid: s.toLowerCase() };
33
+ }
34
+ if (IDENTIFIER_RE.test(s)) {
35
+ return { kind: "identifier", identifier: s.toUpperCase() };
36
+ }
37
+ throw new Error(`--issue must be a Linear identifier (ENG-123), an issue UUID, or a Linear issue URL, got: ${JSON.stringify(raw)}`);
38
+ }
39
+ /**
40
+ * Parse + canonicalize a `--issue` value to the single shell-safe string Otto
41
+ * exports as `OTTO_ISSUE` for the Linear single-issue template. Identifiers are
42
+ * uppercased, UUIDs lowercased; both admit only `[A-Za-z0-9-]`, so the result is
43
+ * safe to interpolate into the static `otto-linear view "$OTTO_ISSUE"` command.
44
+ * Throws (via {@link parseLinearRef}) on anything malformed.
45
+ */
46
+ export function parseLinearIssueArg(raw) {
47
+ const ref = parseLinearRef(raw);
48
+ return ref.kind === "uuid" ? ref.uuid : ref.identifier;
49
+ }
50
+ /** Canonical location of the stored Linear API key (outside any repo). */
51
+ export function linearConfigPath(home) {
52
+ return join(home, ".config", "otto", "linear.json");
53
+ }
54
+ const defaultAuthDeps = {
55
+ env: process.env,
56
+ readFile: (p) => {
57
+ try {
58
+ return readFileSync(p, "utf8");
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ },
64
+ home: homedir(),
65
+ };
66
+ /**
67
+ * Resolve a Linear API key with precedence `OTTO_LINEAR_API_KEY` →
68
+ * `LINEAR_API_KEY` → `~/.config/otto/linear.json` (`{ "type": "apiKey",
69
+ * "token": "..." }`). Returns the token and its source, or null when no source
70
+ * yields a usable (non-empty) token. The config shape is kept extensible for a
71
+ * future OAuth `type`.
72
+ */
73
+ export function resolveLinearAuth(deps = defaultAuthDeps) {
74
+ const { env, readFile, home } = deps;
75
+ for (const name of ["OTTO_LINEAR_API_KEY", "LINEAR_API_KEY"]) {
76
+ const token = env[name]?.trim();
77
+ if (token)
78
+ return { token, source: name };
79
+ }
80
+ const path = linearConfigPath(home);
81
+ const raw = readFile(path);
82
+ if (raw != null) {
83
+ try {
84
+ const token = JSON.parse(raw).token;
85
+ if (typeof token === "string" && token.trim()) {
86
+ return { token: token.trim(), source: path };
87
+ }
88
+ }
89
+ catch {
90
+ // malformed config → no credential from this source
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // GraphQL client
97
+ // ---------------------------------------------------------------------------
98
+ /** Linear's GraphQL endpoint. */
99
+ const LINEAR_ENDPOINT = "https://api.linear.app/graphql";
100
+ /** A typed Linear API failure carrying a {@link LinearErrorKind}. */
101
+ export class LinearApiError extends Error {
102
+ kind;
103
+ status;
104
+ constructor(message, kind, status) {
105
+ super(message);
106
+ this.name = "LinearApiError";
107
+ this.kind = kind;
108
+ this.status = status;
109
+ }
110
+ }
111
+ function isAuthError(status, errors) {
112
+ if (status === 401 || status === 403)
113
+ return true;
114
+ return errors.some((e) => /authenticat/i.test(e?.message ?? "") ||
115
+ /^AUTHENTICATION/i.test(e?.extensions?.code ?? ""));
116
+ }
117
+ const ISSUE_FIELDS = `id identifier title url description state { name type }`;
118
+ const COMMENT_FIELDS = `comments { nodes { body createdAt user { name } } }`;
119
+ function mapDetail(raw) {
120
+ return {
121
+ id: raw.id,
122
+ identifier: raw.identifier,
123
+ title: raw.title,
124
+ url: raw.url,
125
+ description: raw.description ?? "",
126
+ state: raw.state.name,
127
+ comments: (raw.comments?.nodes ?? []).map((c) => ({
128
+ author: c.user?.name ?? "unknown",
129
+ body: c.body,
130
+ createdAt: c.createdAt,
131
+ })),
132
+ };
133
+ }
134
+ /**
135
+ * Build a Linear GraphQL client bound to one API key. Each method issues a
136
+ * single GraphQL request over injectable `fetch`, authenticating with the
137
+ * personal-API-key scheme (`Authorization: <key>`, no `Bearer`). Failures throw
138
+ * a {@link LinearApiError} classified as `auth`/`request`/`network`.
139
+ *
140
+ * Ref→id resolution beyond {@link viewIssue} (e.g. resolving a done-state id)
141
+ * is intentionally left to higher layers; these ops stay thin and 1:1 with the
142
+ * underlying GraphQL operations so they are trivial to mock and reason about.
143
+ */
144
+ export function createLinearClient(deps) {
145
+ const fetchImpl = deps.fetch ?? fetch;
146
+ const endpoint = deps.endpoint ?? LINEAR_ENDPOINT;
147
+ const token = deps.token;
148
+ async function request(query, variables) {
149
+ let res;
150
+ try {
151
+ res = await fetchImpl(endpoint, {
152
+ method: "POST",
153
+ headers: {
154
+ "content-type": "application/json",
155
+ authorization: token,
156
+ },
157
+ body: JSON.stringify({ query, variables }),
158
+ });
159
+ }
160
+ catch (e) {
161
+ throw new LinearApiError(`Linear request failed: ${e.message}`, "network");
162
+ }
163
+ let json;
164
+ try {
165
+ json = (await res.json());
166
+ }
167
+ catch {
168
+ json = undefined;
169
+ }
170
+ const errors = json?.errors ?? [];
171
+ if (!res.ok || errors.length > 0) {
172
+ const kind = isAuthError(res.status, errors) ? "auth" : "request";
173
+ const msg = errors[0]?.message ?? `HTTP ${res.status}`;
174
+ throw new LinearApiError(`Linear GraphQL error: ${msg}`, kind, res.status);
175
+ }
176
+ return json.data;
177
+ }
178
+ return {
179
+ async whoami() {
180
+ const data = await request(`query { viewer { id name email } }`, {});
181
+ return data.viewer;
182
+ },
183
+ async listIssues({ label, team, limit }) {
184
+ const filter = {
185
+ labels: { some: { name: { eq: label } } },
186
+ state: { type: { nin: ["completed", "canceled"] } },
187
+ };
188
+ if (team)
189
+ filter.team = { key: { eq: team } };
190
+ const data = await request(`query ListIssues($filter: IssueFilter!, $first: Int!) {
191
+ issues(filter: $filter, first: $first) {
192
+ nodes { id identifier title url state { name type } }
193
+ }
194
+ }`, { filter, first: limit });
195
+ return data.issues.nodes.map((n) => ({
196
+ id: n.id,
197
+ identifier: n.identifier,
198
+ title: n.title,
199
+ url: n.url,
200
+ state: n.state.name,
201
+ }));
202
+ },
203
+ async viewIssue(ref) {
204
+ if (ref.kind === "uuid") {
205
+ const data = await request(`query ViewIssue($id: String!) {
206
+ issue(id: $id) { ${ISSUE_FIELDS} ${COMMENT_FIELDS} }
207
+ }`, { id: ref.uuid });
208
+ if (!data.issue) {
209
+ throw new LinearApiError(`Linear issue not found: ${ref.uuid}`, "request");
210
+ }
211
+ return mapDetail(data.issue);
212
+ }
213
+ const [, team, num] = ref.identifier.match(/^([A-Za-z][A-Za-z0-9]*)-([1-9]\d*)$/);
214
+ const data = await request(`query FindIssue($team: String!, $number: Float!) {
215
+ issues(filter: { team: { key: { eq: $team } }, number: { eq: $number } }, first: 1) {
216
+ nodes { ${ISSUE_FIELDS} ${COMMENT_FIELDS} }
217
+ }
218
+ }`, { team, number: Number(num) });
219
+ const node = data.issues.nodes[0];
220
+ if (!node) {
221
+ throw new LinearApiError(`Linear issue not found: ${ref.identifier}`, "request");
222
+ }
223
+ return mapDetail(node);
224
+ },
225
+ async addComment(issueId, body) {
226
+ const data = await request(`mutation AddComment($issueId: String!, $body: String!) {
227
+ commentCreate(input: { issueId: $issueId, body: $body }) {
228
+ success comment { id }
229
+ }
230
+ }`, { issueId, body });
231
+ if (!data.commentCreate.success || !data.commentCreate.comment) {
232
+ throw new LinearApiError(`Linear refused to add comment on ${issueId} (success=${data.commentCreate.success})`, "request");
233
+ }
234
+ return { id: data.commentCreate.comment.id };
235
+ },
236
+ async listWorkflowStates(team) {
237
+ const data = await request(`query WorkflowStates($team: String!) {
238
+ workflowStates(filter: { team: { key: { eq: $team } } }, first: 100) {
239
+ nodes { id name type position }
240
+ }
241
+ }`, { team });
242
+ return data.workflowStates.nodes.map((n) => ({
243
+ id: n.id,
244
+ name: n.name,
245
+ type: n.type,
246
+ position: n.position,
247
+ }));
248
+ },
249
+ async moveToDone(issueId, stateId) {
250
+ const data = await request(`mutation MoveToDone($id: String!, $stateId: String!) {
251
+ issueUpdate(id: $id, input: { stateId: $stateId }) {
252
+ success issue { id state { name type } }
253
+ }
254
+ }`, { id: issueId, stateId });
255
+ if (!data.issueUpdate.success || !data.issueUpdate.issue) {
256
+ throw new LinearApiError(`Linear refused to move ${issueId} (success=${data.issueUpdate.success})`, "request");
257
+ }
258
+ return { id: data.issueUpdate.issue.id, state: data.issueUpdate.issue.state.name };
259
+ },
260
+ };
261
+ }
262
+ /**
263
+ * Resolve which workflow state a completed issue should move to, given a team's
264
+ * states and an optional preferred state name (`OTTO_LINEAR_DONE_STATE`):
265
+ *
266
+ * 1. If `preferredName` is set, the state whose name matches it
267
+ * (case-insensitively) wins; if none matches the result is `ambiguous` — we
268
+ * never silently pick a different state than the user named.
269
+ * 2. Otherwise the first `type: "completed"` state by ascending `position`.
270
+ * 3. If neither yields a state the result is `ambiguous`; callers should
271
+ * comment on the issue and leave a human to move it rather than guess.
272
+ */
273
+ export function resolveDoneState(states, preferredName) {
274
+ const wanted = preferredName?.trim();
275
+ if (wanted) {
276
+ const match = states.find((s) => s.name.toLowerCase() === wanted.toLowerCase());
277
+ if (match)
278
+ return { kind: "resolved", state: match };
279
+ const names = states.map((s) => s.name).join(", ") || "none";
280
+ return {
281
+ kind: "ambiguous",
282
+ reason: `no workflow state named "${wanted}" (have: ${names})`,
283
+ };
284
+ }
285
+ const completed = states
286
+ .filter((s) => s.type === "completed")
287
+ .sort((a, b) => a.position - b.position);
288
+ if (completed.length === 0) {
289
+ return {
290
+ kind: "ambiguous",
291
+ reason: 'no workflow state of type "completed"',
292
+ };
293
+ }
294
+ return { kind: "resolved", state: completed[0] };
295
+ }
296
+ //# sourceMappingURL=linear-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-api.js","sourceRoot":"","sources":["../src/linear-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAUvC,2EAA2E;AAC3E,wEAAwE;AACxE,MAAM,aAAa,GAAG,iCAAiC,CAAC;AACxD,qEAAqE;AACrE,MAAM,OAAO,GACX,+EAA+E,CAAC;AAClF,wEAAwE;AACxE,6EAA6E;AAC7E,qEAAqE;AACrE,2EAA2E;AAC3E,MAAM,iBAAiB,GACrB,mFAAmF,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAErB,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IACvE,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IACjD,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IAC7D,CAAC;IACD,MAAM,IAAI,KAAK,CACb,6FAA6F,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CACnH,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,GAAG,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC;AACzD,CAAC;AAiBD,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,eAAe,GAAmB;IACtC,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACd,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,IAAI,EAAE,OAAO,EAAE;CAChB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAuB,eAAe;IAEtC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,CAAC,qBAAqB,EAAE,gBAAgB,CAAU,EAAE,CAAC;QACtE,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;QAChC,IAAI,KAAK;YAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC5C,CAAC;IAED,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC,KAAK,CAAC;YAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC9C,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YAC/C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,iCAAiC;AACjC,MAAM,eAAe,GAAG,gCAAgC,CAAC;AASzD,qEAAqE;AACrE,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAkB;IACtB,MAAM,CAAU;IACzB,YAAY,OAAe,EAAE,IAAqB,EAAE,MAAe;QACjE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AA6DD,SAAS,WAAW,CAAC,MAAc,EAAE,MAAsB;IACzD,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,MAAM,CAAC,IAAI,CAChB,CAAC,CAAC,EAAE,EAAE,CACJ,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC;QACrC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,CACrD,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,yDAAyD,CAAC;AAC/E,MAAM,cAAc,GAAG,qDAAqD,CAAC;AAa7E,SAAS,SAAS,CAAC,GAAa;IAC9B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,EAAE;QAClC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI;QACrB,QAAQ,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChD,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,SAAS;YACjC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAsB;IACvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAEzB,KAAK,UAAU,OAAO,CACpB,KAAa,EACb,SAAkC;QAElC,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE;gBAC9B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,KAAK;iBACrB;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;aAC3C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,cAAc,CACtB,0BAA2B,CAAW,CAAC,OAAO,EAAE,EAChD,SAAS,CACV,CAAC;QACJ,CAAC;QAED,IAAI,IAAuD,CAAC;QAC5D,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAgB,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YAClE,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;YACvD,MAAM,IAAI,cAAc,CAAC,yBAAyB,GAAG,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,IAAK,CAAC,IAAS,CAAC;IACzB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM;YACV,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB,oCAAoC,EACpC,EAAE,CACH,CAAC;YACF,OAAO,IAAI,CAAC,MAAM,CAAC;QACrB,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE;YACrC,MAAM,MAAM,GAA4B;gBACtC,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;gBACzC,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC,EAAE,EAAE;aACpD,CAAC;YACF,IAAI,IAAI;gBAAE,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,MAAM,OAAO,CAGxB;;;;WAIG,EACH,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CACzB,CAAC;YACF,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACnC,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI;aACpB,CAAC,CAAC,CAAC;QACN,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACxB,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB;gCACsB,YAAY,IAAI,cAAc;aACjD,EACH,EAAE,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,CACjB,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;oBAChB,MAAM,IAAI,cAAc,CACtB,2BAA2B,GAAG,CAAC,IAAI,EAAE,EACrC,SAAS,CACV,CAAC;gBACJ,CAAC;gBACD,OAAO,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YACD,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,KAAK,CACxC,qCAAqC,CACrC,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB;;uBAEe,YAAY,IAAI,cAAc;;WAE1C,EACH,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAC9B,CAAC;YACF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,cAAc,CACtB,2BAA2B,GAAG,CAAC,UAAU,EAAE,EAC3C,SAAS,CACV,CAAC;YACJ,CAAC;YACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI;YAC5B,MAAM,IAAI,GAAG,MAAM,OAAO,CAGxB;;;;WAIG,EACH,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;gBAC/D,MAAM,IAAI,cAAc,CACtB,oCAAoC,OAAO,aAAa,IAAI,CAAC,aAAa,CAAC,OAAO,GAAG,EACrF,SAAS,CACV,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAC/C,CAAC;QAED,KAAK,CAAC,kBAAkB,CAAC,IAAI;YAC3B,MAAM,IAAI,GAAG,MAAM,OAAO,CAGxB;;;;WAIG,EACH,EAAE,IAAI,EAAE,CACT,CAAC;YACF,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3C,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;aACrB,CAAC,CAAC,CAAC;QACN,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO;YAC/B,MAAM,IAAI,GAAG,MAAM,OAAO,CAGxB;;;;WAIG,EACH,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CACzB,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;gBACzD,MAAM,IAAI,cAAc,CACtB,0BAA0B,OAAO,aAAa,IAAI,CAAC,WAAW,CAAC,OAAO,GAAG,EACzE,SAAS,CACV,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACrF,CAAC;KACF,CAAC;AACJ,CAAC;AAOD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA6B,EAC7B,aAAsB;IAEtB,MAAM,MAAM,GAAG,aAAa,EAAE,IAAI,EAAE,CAAC;IACrC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CACvB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,WAAW,EAAE,CACrD,CAAC;QACF,IAAI,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC;QAC7D,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EAAE,4BAA4B,MAAM,YAAY,KAAK,GAAG;SAC/D,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,MAAM;SACrB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;SACrC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EAAE,uCAAuC;SAChD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;AACnD,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { type LinearViewer } from "./linear-api.js";
2
+ /**
3
+ * Injectable host surface for {@link runLinearAuth} so the credential tool stays
4
+ * pure-ish and unit-testable without touching the real home dir, stdin, or the
5
+ * network. {@link defaultLinearAuthDeps} wires up the host.
6
+ */
7
+ export type LinearAuthCliDeps = {
8
+ env: NodeJS.ProcessEnv;
9
+ /** Home directory holding the credential file. */
10
+ home: string;
11
+ /** Read a file's contents, or null if it is absent/unreadable. */
12
+ readFile: (path: string) => string | null;
13
+ /** Write `contents` to `path`, creating parent dirs, with the given mode. */
14
+ writeFile: (path: string, contents: string, mode: number) => void;
15
+ /** Delete `path`; return true if a file was actually removed. */
16
+ removeFile: (path: string) => boolean;
17
+ /** Read the API key the user pastes on stdin (login). */
18
+ readStdin: () => Promise<string>;
19
+ /** Whether stdin is an interactive TTY (gates the paste/Ctrl-D hint). */
20
+ stdinIsTTY: boolean;
21
+ /** Print one line to stdout. */
22
+ out: (msg: string) => void;
23
+ /** Print one line to stderr. */
24
+ err: (msg: string) => void;
25
+ /** Live credential check for `status --verify-live`; injectable for tests. */
26
+ verify: (token: string) => Promise<LinearViewer>;
27
+ };
28
+ /** Host-wired defaults for {@link runLinearAuth}. */
29
+ export declare const defaultLinearAuthDeps: LinearAuthCliDeps;
30
+ /**
31
+ * Drive the `otto-linear-auth` credential tool. Resolves to the process exit
32
+ * code so the thin bin wrapper can `process.exit(code)`.
33
+ *
34
+ * - `login` — read a Linear personal API key from stdin and store it at
35
+ * `~/.config/otto/linear.json` (`0600`, outside any repo). The secret is
36
+ * never echoed back.
37
+ * - `status` — report whether a credential resolves (and from where, via
38
+ * {@link resolveLinearAuth}) without printing the token; `--verify-live`
39
+ * additionally calls the Linear API to confirm the key works.
40
+ * - `logout` — delete the stored credential file; warns if an env-var key
41
+ * still takes precedence.
42
+ */
43
+ export declare function runLinearAuth(argv: string[], deps?: LinearAuthCliDeps): Promise<number>;
44
+ //# sourceMappingURL=linear-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-auth.d.ts","sourceRoot":"","sources":["../src/linear-auth.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1C,6EAA6E;IAC7E,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,iEAAiE;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,yDAAyD;IACzD,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,yEAAyE;IACzE,UAAU,EAAE,OAAO,CAAC;IACpB,gCAAgC;IAChC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,gCAAgC;IAChC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,8EAA8E;IAC9E,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CAClD,CAAC;AAUF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,EAAE,iBA2CnC,CAAC;AAKF;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,iBAAyC,GAC9C,OAAO,CAAC,MAAM,CAAC,CA+EjB"}
@@ -0,0 +1,145 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname } from "node:path";
3
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
4
+ import { createLinearClient, linearConfigPath, resolveLinearAuth, LinearApiError, } from "./linear-api.js";
5
+ async function readAllStdin() {
6
+ const chunks = [];
7
+ for await (const chunk of process.stdin) {
8
+ chunks.push(Buffer.from(chunk));
9
+ }
10
+ return Buffer.concat(chunks).toString("utf8");
11
+ }
12
+ /** Host-wired defaults for {@link runLinearAuth}. */
13
+ export const defaultLinearAuthDeps = {
14
+ env: process.env,
15
+ home: homedir(),
16
+ readFile: (p) => {
17
+ try {
18
+ return readFileSync(p, "utf8");
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ },
24
+ writeFile: (p, contents, mode) => {
25
+ mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
26
+ // Write to a fresh temp sibling (so `mode` applies on creation) and rename
27
+ // it into place. Writing the secret straight to `p` would (a) leave a
28
+ // pre-existing, possibly looser-permissioned file briefly world-readable
29
+ // before a chmod could tighten it, and (b) risk truncating a valid prior
30
+ // token if the process crashed mid-write. rename() is atomic on the same fs.
31
+ const tmp = `${p}.${process.pid}.tmp`;
32
+ writeFileSync(tmp, contents, { mode });
33
+ try {
34
+ renameSync(tmp, p);
35
+ }
36
+ catch (e) {
37
+ try {
38
+ unlinkSync(tmp);
39
+ }
40
+ catch {
41
+ // best-effort cleanup of the temp file
42
+ }
43
+ throw e;
44
+ }
45
+ },
46
+ removeFile: (p) => {
47
+ try {
48
+ unlinkSync(p);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ },
55
+ readStdin: readAllStdin,
56
+ stdinIsTTY: Boolean(process.stdin.isTTY),
57
+ out: (m) => process.stdout.write(`${m}\n`),
58
+ err: (m) => process.stderr.write(`${m}\n`),
59
+ verify: (token) => createLinearClient({ token }).whoami(),
60
+ };
61
+ const USAGE = "Usage: otto-linear-auth <login|status [--verify-live]|logout>";
62
+ /**
63
+ * Drive the `otto-linear-auth` credential tool. Resolves to the process exit
64
+ * code so the thin bin wrapper can `process.exit(code)`.
65
+ *
66
+ * - `login` — read a Linear personal API key from stdin and store it at
67
+ * `~/.config/otto/linear.json` (`0600`, outside any repo). The secret is
68
+ * never echoed back.
69
+ * - `status` — report whether a credential resolves (and from where, via
70
+ * {@link resolveLinearAuth}) without printing the token; `--verify-live`
71
+ * additionally calls the Linear API to confirm the key works.
72
+ * - `logout` — delete the stored credential file; warns if an env-var key
73
+ * still takes precedence.
74
+ */
75
+ export async function runLinearAuth(argv, deps = defaultLinearAuthDeps) {
76
+ const [sub, ...rest] = argv;
77
+ const path = linearConfigPath(deps.home);
78
+ switch (sub) {
79
+ case "login": {
80
+ // A key on the command line would silently hang (login reads stdin, not
81
+ // argv) and leak into shell history / `ps` — reject it with guidance.
82
+ if (rest.length > 0) {
83
+ deps.err("login reads the API key from stdin, not an argument " +
84
+ "(a key on the command line leaks into shell history and `ps`).\n" +
85
+ "Run `otto-linear-auth login` and paste your key, then press Ctrl-D — " +
86
+ "or pipe it: printf '%s' <key> | otto-linear-auth login");
87
+ return 2;
88
+ }
89
+ // On an interactive terminal stdin blocks until EOF with no output, which
90
+ // looks frozen; tell the user to paste then send EOF (Ctrl-D).
91
+ if (deps.stdinIsTTY) {
92
+ deps.err("Paste your Linear personal API key, then press Ctrl-D:");
93
+ }
94
+ const token = (await deps.readStdin()).trim();
95
+ if (!token) {
96
+ deps.err("No API key provided. Paste your Linear personal API key.");
97
+ return 1;
98
+ }
99
+ const body = JSON.stringify({ type: "apiKey", token }, null, 2);
100
+ deps.writeFile(path, `${body}\n`, 0o600);
101
+ deps.out(`Stored Linear API key at ${path} (0600).`);
102
+ return 0;
103
+ }
104
+ case "status": {
105
+ const auth = resolveLinearAuth({
106
+ env: deps.env,
107
+ readFile: deps.readFile,
108
+ home: deps.home,
109
+ });
110
+ if (!auth) {
111
+ deps.out("Linear auth: not found — run `otto-linear-auth login`.");
112
+ return 1;
113
+ }
114
+ deps.out(`Linear auth: found (source: ${auth.source}).`);
115
+ if (rest.includes("--verify-live")) {
116
+ try {
117
+ const me = await deps.verify(auth.token);
118
+ deps.out(`Verified live as ${me.name} <${me.email}>.`);
119
+ }
120
+ catch (e) {
121
+ const kind = e instanceof LinearApiError ? ` (${e.kind})` : "";
122
+ deps.err(`Live verification failed${kind}: ${e.message}`);
123
+ return 1;
124
+ }
125
+ }
126
+ return 0;
127
+ }
128
+ case "logout": {
129
+ const removed = deps.removeFile(path);
130
+ deps.out(removed
131
+ ? `Removed stored Linear credential at ${path}.`
132
+ : `No stored credential at ${path}; nothing to remove.`);
133
+ for (const name of ["OTTO_LINEAR_API_KEY", "LINEAR_API_KEY"]) {
134
+ if (deps.env[name]?.trim()) {
135
+ deps.out(`Note: ${name} is still set and takes precedence over the file.`);
136
+ }
137
+ }
138
+ return 0;
139
+ }
140
+ default:
141
+ deps.err(USAGE);
142
+ return 2;
143
+ }
144
+ }
145
+ //# sourceMappingURL=linear-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-auth.js","sourceRoot":"","sources":["../src/linear-auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type LinearClient } from "./linear-api.js";
2
+ /**
3
+ * Injectable host surface for {@link runLinear} so the bundled `linear` helper
4
+ * stays pure-ish and unit-testable without touching the real home dir or the
5
+ * network. {@link defaultLinearCliDeps} wires up the host.
6
+ */
7
+ export type LinearCliDeps = {
8
+ env: NodeJS.ProcessEnv;
9
+ /** Home directory holding the credential file. */
10
+ home: string;
11
+ /** Read a file's contents, or null if it is absent/unreadable. */
12
+ readFile: (path: string) => string | null;
13
+ /** Print one line to stdout. */
14
+ out: (msg: string) => void;
15
+ /** Print one line to stderr. */
16
+ err: (msg: string) => void;
17
+ /** Build a Linear client for a resolved token; injectable for tests. */
18
+ makeClient: (token: string) => LinearClient;
19
+ };
20
+ /** Host-wired defaults for {@link runLinear}. */
21
+ export declare const defaultLinearCliDeps: LinearCliDeps;
22
+ /**
23
+ * Drive the bundled `linear` helper CLI — Otto's thin Linear counterpart to the
24
+ * system `gh`, used by the Linear templates and the agent. Resolves to the
25
+ * process exit code so the bin wrapper can `process.exit(code)`.
26
+ *
27
+ * - `list` — print labelled open issues (identifier/title/state/url).
28
+ * - `dump` — emit a JSON array of full issue detail (bodies + comments), for
29
+ * spilling into a prompt (parallel to `gh issue list --json …,body,comments`).
30
+ * - `view` — print one issue's full detail as JSON.
31
+ * - `comment` — add a comment to an issue from `--body-file`.
32
+ * - `done` — move an issue to a completed workflow state, resolved via
33
+ * `OTTO_LINEAR_DONE_STATE` (by name) else the team's first `completed`-type
34
+ * state; exits non-zero (no move) when the target state is ambiguous.
35
+ */
36
+ export declare function runLinear(argv: string[], deps?: LinearCliDeps): Promise<number>;
37
+ //# sourceMappingURL=linear-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-cli.d.ts","sourceRoot":"","sources":["../src/linear-cli.ts"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1C,gCAAgC;IAChC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,gCAAgC;IAChC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,wEAAwE;IACxE,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,CAAC;CAC7C,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,oBAAoB,EAAE,aAalC,CAAC;AAqCF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,aAAoC,GACzC,OAAO,CAAC,MAAM,CAAC,CAoIjB"}