linmux 0.1.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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/bin/run.js +4 -0
  4. package/dist/commands/comment/create.js +94 -0
  5. package/dist/commands/comment/delete.js +74 -0
  6. package/dist/commands/comment/list.js +84 -0
  7. package/dist/commands/comment/update.js +80 -0
  8. package/dist/commands/cycle/current.js +78 -0
  9. package/dist/commands/cycle/list.js +84 -0
  10. package/dist/commands/cycle/move.js +91 -0
  11. package/dist/commands/describe.js +65 -0
  12. package/dist/commands/graphql/index.js +92 -0
  13. package/dist/commands/install-skill.js +54 -0
  14. package/dist/commands/issue/archive.js +75 -0
  15. package/dist/commands/issue/create.js +115 -0
  16. package/dist/commands/issue/get.js +84 -0
  17. package/dist/commands/issue/list.js +93 -0
  18. package/dist/commands/issue/purge.js +81 -0
  19. package/dist/commands/issue/search.js +109 -0
  20. package/dist/commands/issue/transition.js +91 -0
  21. package/dist/commands/issue/trash.js +75 -0
  22. package/dist/commands/issue/update.js +126 -0
  23. package/dist/commands/label/create.js +91 -0
  24. package/dist/commands/label/list.js +76 -0
  25. package/dist/commands/list-tools.js +47 -0
  26. package/dist/commands/me.js +71 -0
  27. package/dist/commands/project/create.js +101 -0
  28. package/dist/commands/project/get.js +83 -0
  29. package/dist/commands/project/list.js +75 -0
  30. package/dist/commands/project/update-status.js +99 -0
  31. package/dist/commands/project/update.js +99 -0
  32. package/dist/commands/raw/batch.js +85 -0
  33. package/dist/commands/raw/index.js +72 -0
  34. package/dist/commands/schema.js +69 -0
  35. package/dist/commands/state/list.js +77 -0
  36. package/dist/commands/team/get.js +73 -0
  37. package/dist/commands/team/list.js +73 -0
  38. package/dist/commands/whoami.js +71 -0
  39. package/dist/commands/workspace/add.js +97 -0
  40. package/dist/commands/workspace/list.js +47 -0
  41. package/dist/commands/workspace/remove.js +63 -0
  42. package/dist/commands/workspace/replace-token.js +89 -0
  43. package/dist/commands/workspace/use.js +54 -0
  44. package/dist/core/client/factory.js +28 -0
  45. package/dist/core/client/index.js +2 -0
  46. package/dist/core/config/index.js +4 -0
  47. package/dist/core/config/paths.js +30 -0
  48. package/dist/core/config/schema.js +36 -0
  49. package/dist/core/config/store.js +149 -0
  50. package/dist/core/errors/error.js +142 -0
  51. package/dist/core/errors/exit-codes.js +70 -0
  52. package/dist/core/output/envelope.js +53 -0
  53. package/dist/core/output/format.js +42 -0
  54. package/dist/core/output/index.js +3 -0
  55. package/dist/core/pagination/flags.js +29 -0
  56. package/dist/core/pagination/index.js +2 -0
  57. package/dist/core/projection/presets.js +116 -0
  58. package/dist/core/projection/project.js +282 -0
  59. package/dist/core/redact/redact.js +45 -0
  60. package/dist/core/resolvers/cycle.js +60 -0
  61. package/dist/core/resolvers/index.js +7 -0
  62. package/dist/core/resolvers/label.js +54 -0
  63. package/dist/core/resolvers/project-status.js +42 -0
  64. package/dist/core/resolvers/project.js +43 -0
  65. package/dist/core/resolvers/state.js +46 -0
  66. package/dist/core/resolvers/team.js +50 -0
  67. package/dist/core/transport/fetch-interceptor.js +109 -0
  68. package/dist/core/transport/index.js +3 -0
  69. package/dist/core/transport/rate-limit.js +167 -0
  70. package/dist/core/workspace/resolver.js +70 -0
  71. package/dist/core/workspace/write-guard.js +43 -0
  72. package/dist/generated/graphql.js +89428 -0
  73. package/dist/generated/operations.js +3013 -0
  74. package/dist/lib/comment-create-runtime.js +96 -0
  75. package/dist/lib/comment-delete-runtime.js +46 -0
  76. package/dist/lib/comment-list-runtime.js +182 -0
  77. package/dist/lib/comment-update-runtime.js +93 -0
  78. package/dist/lib/cycle-current-runtime.js +90 -0
  79. package/dist/lib/cycle-list-runtime.js +151 -0
  80. package/dist/lib/cycle-move-runtime.js +142 -0
  81. package/dist/lib/describe-runtime.js +180 -0
  82. package/dist/lib/filter-heuristics.js +59 -0
  83. package/dist/lib/graphql-runtime.js +202 -0
  84. package/dist/lib/include-fragments.js +73 -0
  85. package/dist/lib/install-skill-runtime.js +228 -0
  86. package/dist/lib/introspection-registry.js +488 -0
  87. package/dist/lib/issue-archive-runtime.js +89 -0
  88. package/dist/lib/issue-create-runtime.js +175 -0
  89. package/dist/lib/issue-get-runtime.js +153 -0
  90. package/dist/lib/issue-list-runtime.js +164 -0
  91. package/dist/lib/issue-purge-runtime.js +89 -0
  92. package/dist/lib/issue-search-runtime.js +114 -0
  93. package/dist/lib/issue-transition-runtime.js +131 -0
  94. package/dist/lib/issue-trash-runtime.js +84 -0
  95. package/dist/lib/issue-update-runtime.js +164 -0
  96. package/dist/lib/label-create-runtime.js +113 -0
  97. package/dist/lib/label-list-runtime.js +97 -0
  98. package/dist/lib/levenshtein.js +42 -0
  99. package/dist/lib/list-tools-runtime.js +38 -0
  100. package/dist/lib/me-runtime.js +55 -0
  101. package/dist/lib/project-create-runtime.js +103 -0
  102. package/dist/lib/project-get-runtime.js +134 -0
  103. package/dist/lib/project-list-runtime.js +84 -0
  104. package/dist/lib/project-update-runtime.js +110 -0
  105. package/dist/lib/project-update-status-runtime.js +91 -0
  106. package/dist/lib/raw-batch-runtime.js +229 -0
  107. package/dist/lib/raw-runtime.js +171 -0
  108. package/dist/lib/schema-loader.js +41 -0
  109. package/dist/lib/schema-runtime.js +65 -0
  110. package/dist/lib/state-list-runtime.js +93 -0
  111. package/dist/lib/team-get-runtime.js +55 -0
  112. package/dist/lib/team-list-runtime.js +52 -0
  113. package/dist/lib/workspace-runtime.js +112 -0
  114. package/dist/operations/_registry.zod.js +5337 -0
  115. package/oclif.manifest.json +3631 -0
  116. package/package.json +99 -0
  117. package/schema.graphql +30772 -0
  118. package/skills/linmux/SKILL.md +186 -0
@@ -0,0 +1,91 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { getLastComplexity, withFetchInterception } from "../core/transport/fetch-interceptor.js";
3
+ import { withRateLimitRetry } from "../core/transport/rate-limit.js";
4
+ import "../core/transport/index.js";
5
+ import { createLinearClient } from "../core/client/factory.js";
6
+ import "../core/client/index.js";
7
+ import { loadConfig } from "../core/config/store.js";
8
+ import "../core/config/index.js";
9
+ import { parseFields, project } from "../core/projection/project.js";
10
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
11
+ import { requireExplicitWorkspaceForWrite } from "../core/workspace/write-guard.js";
12
+ import { resolveProjectId } from "../core/resolvers/project.js";
13
+ import { resolveProjectStatusId } from "../core/resolvers/project-status.js";
14
+ import "../core/resolvers/index.js";
15
+ //#region src/lib/project-update-status-runtime.ts
16
+ async function projectUpdateStatusRuntime(input) {
17
+ const config = (input.loadConfigOverride ?? loadConfig)();
18
+ const envForResolver = {};
19
+ if (input.env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = input.env.LINEAR_WORKSPACE;
20
+ if (input.env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = input.env.LINEAR_API_KEY;
21
+ const resolved = resolveWorkspace({
22
+ flags: input.flags.workspace ? { workspace: input.flags.workspace } : {},
23
+ env: envForResolver,
24
+ config
25
+ });
26
+ requireExplicitWorkspaceForWrite(resolved, input.flags.allowActiveWorkspaceWrite ?? false);
27
+ const fields = parseFields(input.flags.fields ?? "defaults", "project");
28
+ const client = (input.clientFactoryOverride ?? createLinearClient)(resolved);
29
+ return withFetchInterception(async () => {
30
+ const workspaceKey = resolved.name ?? "_api-key-env_";
31
+ const [projectId, statusId] = await Promise.all([resolveProjectId(client, workspaceKey, input.args.ref, input.retryOptsOverride), resolveProjectStatusId(client, workspaceKey, input.args.status, input.retryOptsOverride)]);
32
+ const payload = await withRateLimitRetry(() => client.updateProject(projectId, { statusId }), input.retryOptsOverride);
33
+ if (!payload.success) throw LinearAgentError.linear.apiError({
34
+ message: "updateProject returned success=false",
35
+ details: { lastSyncId: payload.lastSyncId }
36
+ });
37
+ let updated;
38
+ if (payload.project !== void 0) {
39
+ const u = await Promise.resolve(payload.project);
40
+ if (u !== void 0 && u !== null) updated = u;
41
+ }
42
+ let data;
43
+ if (updated) data = project(await hydrateForProjection(updated, fields), fields);
44
+ else data = {
45
+ id: projectId,
46
+ statusId
47
+ };
48
+ const complexity = getLastComplexity();
49
+ const meta = {
50
+ workspace: resolved.name,
51
+ workspaceSource: resolved.source,
52
+ ...complexity !== void 0 ? { complexity } : {}
53
+ };
54
+ return {
55
+ data,
56
+ meta
57
+ };
58
+ });
59
+ }
60
+ const RELATION_KEYS = new Set(["lead", "creator"]);
61
+ async function hydrateForProjection(proj, spec) {
62
+ const needs = neededRelations(spec);
63
+ if (needs.size === 0) {
64
+ const out = {};
65
+ for (const k of Object.keys(proj)) if (!RELATION_KEYS.has(k)) out[k] = proj[k];
66
+ return out;
67
+ }
68
+ const hydrated = {};
69
+ for (const k of Object.keys(proj)) if (RELATION_KEYS.has(k)) {
70
+ if (needs.has(k)) {
71
+ const value = proj[k];
72
+ hydrated[k] = await resolveLazy(value);
73
+ }
74
+ } else hydrated[k] = proj[k];
75
+ return hydrated;
76
+ }
77
+ function neededRelations(spec) {
78
+ if (spec === "*") return new Set(RELATION_KEYS);
79
+ const out = /* @__PURE__ */ new Set();
80
+ for (const path of spec) {
81
+ const head = path.split(".")[0];
82
+ if (head && RELATION_KEYS.has(head)) out.add(head);
83
+ }
84
+ return out;
85
+ }
86
+ async function resolveLazy(value) {
87
+ if (value && typeof value.then === "function") return await value;
88
+ return value;
89
+ }
90
+ //#endregion
91
+ export { projectUpdateStatusRuntime };
@@ -0,0 +1,229 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { OPERATION_REGISTRY } from "../generated/operations.js";
3
+ import { loadConfig } from "../core/config/store.js";
4
+ import "../core/config/index.js";
5
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
6
+ import { requireExplicitWorkspaceForWrite } from "../core/workspace/write-guard.js";
7
+ import { runRaw } from "./raw-runtime.js";
8
+ import { z } from "zod";
9
+ import { readFile } from "node:fs/promises";
10
+ //#region src/lib/raw-batch-runtime.ts
11
+ /**
12
+ * `raw batch` runtime — Phase 3 PLAN 03-05, RAW-05.
13
+ *
14
+ * Safety-gated batch dispatcher. Runs a JSON plan of operations in sequence.
15
+ * Owns:
16
+ * - RAW-05: `raw batch --plan=@file.json` with default dry-run + explicit --yes to execute
17
+ *
18
+ * **9-step pipeline:**
19
+ * 1. resolveWorkspace (S2 verbatim)
20
+ * 2. Validate plan flag has @ prefix → load + parse + Zod-validate → BATCH_PLAN_INVALID
21
+ * 3. Registry lookup each entry's operation → BATCH_PLAN_INVALID with details.entry_index on miss
22
+ * 4. Compute kinds = { query: N, mutation: M }
23
+ * 5. If M > 0: requireExplicitWorkspaceForWrite (WSP-06 FIRST — Pitfall 7)
24
+ * 6. If M > 0 && !allow-mutations: RAW_MUTATION_REQUIRES_FLAG
25
+ * 7. Determine intent:
26
+ * - dryRun = flags['dry-run'] !== false (default true; --dry-run --yes still dry-run)
27
+ * - if dryRun: emit dry-run envelope; STOP (ZERO SDK calls)
28
+ * - if !dryRun && !yes: BATCH_REQUIRES_YES (exit 2)
29
+ * - if !dryRun && yes: dispatch sequentially via runRaw
30
+ * 8. Sequential dispatch (NOT parallel — RESEARCH line 769; keeps rate-limit pressure manageable)
31
+ * 9. Aggregate results; top-level ok:true (batch ran); per-entry ok:false on failure
32
+ *
33
+ * **Threat mitigations:**
34
+ * - T-03-05-WSP06: WSP-06 fires AFTER plan validation but BEFORE any dispatch (Pitfall 7)
35
+ * - T-03-05-DRY-RUN: --dry-run is the DEFAULT; both --dry-run + --yes → still dry-run
36
+ * - T-03-05-PLAN-FILE-INJECTION: Zod validates plan shape; max(100) caps blast radius (Pitfall 6)
37
+ * - T-03-05-D-RATELIMIT: Sequential dispatch + 100-entry cap
38
+ *
39
+ * **Mutation gates apply to dry-run too (REVIEW WR-03 — intentional):**
40
+ * Mutation-containing plans require --workspace (WSP-06) and
41
+ * --allow-mutations to dry-run, not just to execute. Rationale: the
42
+ * safety contract is "any plan touching mutations declares both target
43
+ * and intent upfront, regardless of whether it would run". A dry-run
44
+ * that side-steps the gates would let agents inspect plans they're not
45
+ * authorized to run. Uniform contract beats UX convenience.
46
+ */
47
+ const PlanEntrySchema = z.object({
48
+ operation: z.string(),
49
+ vars: z.record(z.string(), z.unknown())
50
+ });
51
+ const PlanSchema = z.array(PlanEntrySchema).min(1).max(100);
52
+ async function runRawBatch(input) {
53
+ const { flags, env = process.env } = input;
54
+ const config = (input.loadConfigOverride ?? loadConfig)();
55
+ const envForResolver = {};
56
+ if (env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = env.LINEAR_WORKSPACE;
57
+ if (env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = env.LINEAR_API_KEY;
58
+ const resolved = resolveWorkspace({
59
+ flags: flags.workspace ? { workspace: flags.workspace } : {},
60
+ env: envForResolver,
61
+ config
62
+ });
63
+ const plan = await loadAndValidatePlan(flags.plan);
64
+ const registry = OPERATION_REGISTRY;
65
+ const enriched = plan.map((entry, index) => {
66
+ const reg = registry[entry.operation];
67
+ if (!reg) throw new LinearAgentError({
68
+ code: "BATCH_PLAN_INVALID",
69
+ message: `entry ${index}: unknown operation '${entry.operation}'`,
70
+ details: {
71
+ entry_index: index,
72
+ reason: `unknown_operation: '${entry.operation}' not in OPERATION_REGISTRY`,
73
+ operation: entry.operation
74
+ }
75
+ });
76
+ const varsResult = reg.varsSchema.safeParse(entry.vars);
77
+ if (!varsResult.success) throw new LinearAgentError({
78
+ code: "BATCH_PLAN_INVALID",
79
+ message: `entry ${index}: vars failed validation for operation '${entry.operation}'`,
80
+ details: {
81
+ entry_index: index,
82
+ reason: "vars_invalid",
83
+ operation: entry.operation,
84
+ issues: varsResult.error.issues.map((i) => ({
85
+ path: i.path,
86
+ message: i.message,
87
+ code: i.code
88
+ }))
89
+ }
90
+ });
91
+ return {
92
+ ...entry,
93
+ kind: reg.kind
94
+ };
95
+ });
96
+ const kinds = {
97
+ query: enriched.filter((e) => e.kind === "query").length,
98
+ mutation: enriched.filter((e) => e.kind === "mutation").length
99
+ };
100
+ const batchMeta = {
101
+ count: enriched.length,
102
+ kinds
103
+ };
104
+ if (kinds.mutation > 0) requireExplicitWorkspaceForWrite(resolved, flags["allow-active-workspace-write"] ?? false);
105
+ if (kinds.mutation > 0 && !flags["allow-mutations"]) throw new LinearAgentError({
106
+ code: "RAW_MUTATION_REQUIRES_FLAG",
107
+ message: `batch contains ${kinds.mutation} mutation(s) and requires --allow-mutations`,
108
+ details: { mutation_count: kinds.mutation }
109
+ });
110
+ if (flags["dry-run"] !== false) return {
111
+ data: { plan: enriched.map((e) => ({
112
+ operation: e.operation,
113
+ vars: e.vars,
114
+ kind: e.kind,
115
+ workspace: resolved.name
116
+ })) },
117
+ meta: {
118
+ workspace: resolved.name,
119
+ workspaceSource: resolved.source,
120
+ batch: batchMeta
121
+ }
122
+ };
123
+ if (!flags.yes) throw new LinearAgentError({
124
+ code: "BATCH_REQUIRES_YES",
125
+ message: "batch execution requires --yes to confirm (or remove --no-dry-run for a dry-run preview)",
126
+ details: { hint: "pass --yes to confirm execution, or remove --no-dry-run for a dry-run preview" }
127
+ });
128
+ const results = [];
129
+ for (const entry of enriched) try {
130
+ const result = await runRaw({
131
+ args: { operation: entry.operation },
132
+ flags: {
133
+ workspace: flags.workspace,
134
+ "allow-active-workspace-write": flags["allow-active-workspace-write"],
135
+ "allow-mutations": flags["allow-mutations"],
136
+ vars: JSON.stringify(entry.vars)
137
+ },
138
+ env,
139
+ loadConfigOverride: input.loadConfigOverride,
140
+ retryOptsOverride: input.retryOptsOverride
141
+ });
142
+ results.push({
143
+ ok: true,
144
+ operation: entry.operation,
145
+ data: result.data
146
+ });
147
+ } catch (err) {
148
+ if (err instanceof LinearAgentError) results.push({
149
+ ok: false,
150
+ operation: entry.operation,
151
+ error: {
152
+ code: err.code,
153
+ message: err.message,
154
+ ...err.details !== void 0 ? { details: err.details } : {}
155
+ }
156
+ });
157
+ else {
158
+ const e = err;
159
+ results.push({
160
+ ok: false,
161
+ operation: entry.operation,
162
+ error: {
163
+ code: e.code ?? "GENERIC_ERROR",
164
+ message: e.message ?? "unknown error"
165
+ }
166
+ });
167
+ }
168
+ }
169
+ return {
170
+ data: { results },
171
+ meta: {
172
+ workspace: resolved.name,
173
+ workspaceSource: resolved.source,
174
+ batch: batchMeta
175
+ }
176
+ };
177
+ }
178
+ /**
179
+ * Load, parse, and Zod-validate the plan file.
180
+ * Plan flag MUST start with '@' (it's a required file ref).
181
+ */
182
+ async function loadAndValidatePlan(planFlag) {
183
+ if (!planFlag.startsWith("@")) throw new LinearAgentError({
184
+ code: "BATCH_PLAN_INVALID",
185
+ message: `--plan must be an @file path (e.g. --plan=@plan.json); got: '${planFlag}'`,
186
+ details: { reason: "@file path required: prefix the path with @" }
187
+ });
188
+ const filePath = planFlag.slice(1);
189
+ let rawContent;
190
+ try {
191
+ rawContent = await readFile(filePath, "utf8");
192
+ } catch (err) {
193
+ const code = err.code;
194
+ throw new LinearAgentError({
195
+ code: "BATCH_PLAN_INVALID",
196
+ message: `plan file not found or not readable: ${filePath}`,
197
+ details: {
198
+ path: filePath,
199
+ reason: "file_not_readable",
200
+ cause: code ?? err.message
201
+ }
202
+ });
203
+ }
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(rawContent);
207
+ } catch (err) {
208
+ throw new LinearAgentError({
209
+ code: "BATCH_PLAN_INVALID",
210
+ message: `plan file is not valid JSON: ${err.message}`,
211
+ details: { reason: "parse_error" }
212
+ });
213
+ }
214
+ const result = PlanSchema.safeParse(parsed);
215
+ if (!result.success) {
216
+ const firstIssue = result.error.issues[0];
217
+ const entryIndex = firstIssue?.path[0] !== void 0 && typeof firstIssue.path[0] === "number" ? firstIssue.path[0] : void 0;
218
+ const details = { reason: firstIssue?.message ?? "validation_failed" };
219
+ if (entryIndex !== void 0) details.entry_index = entryIndex;
220
+ throw new LinearAgentError({
221
+ code: "BATCH_PLAN_INVALID",
222
+ message: `plan file validation failed: ${firstIssue?.message ?? "invalid shape"}`,
223
+ details
224
+ });
225
+ }
226
+ return result.data;
227
+ }
228
+ //#endregion
229
+ export { runRawBatch };
@@ -0,0 +1,171 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { OPERATION_REGISTRY } from "../generated/operations.js";
3
+ import { suggestClosest } from "./levenshtein.js";
4
+ import { redact } from "../core/redact/redact.js";
5
+ import { getLastComplexity, withFetchInterception } from "../core/transport/fetch-interceptor.js";
6
+ import { withRateLimitRetry } from "../core/transport/rate-limit.js";
7
+ import "../core/transport/index.js";
8
+ import { createLinearClient } from "../core/client/factory.js";
9
+ import "../core/client/index.js";
10
+ import { loadConfig } from "../core/config/store.js";
11
+ import "../core/config/index.js";
12
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
13
+ import { requireExplicitWorkspaceForWrite } from "../core/workspace/write-guard.js";
14
+ import { readFile } from "node:fs/promises";
15
+ //#region src/lib/raw-runtime.ts
16
+ /**
17
+ * `raw <Operation>` runtime — Phase 3 PLAN 03-02, RAW-01 / RAW-02.
18
+ *
19
+ * Dispatches any operation in the generated GraphQL registry via
20
+ * `client.client.rawRequest(entry.source, vars)`. Owns:
21
+ * - RAW-01: full registry surface (501 ops) accessible from CLI
22
+ * - RAW-02: mutation safety with WSP-06 + --allow-mutations gates
23
+ *
24
+ * **Gate ordering is load-bearing (Test 3 — WSP-06 BEFORE --allow-mutations):**
25
+ * 1. resolveWorkspace
26
+ * 2. registry lookup (RAW_OPERATION_NOT_FOUND on miss; closest-match suggestions)
27
+ * 3. subscription guard (OPERATION_SUBSCRIPTIONS_UNSUPPORTED — defensive)
28
+ * 4. requireExplicitWorkspaceForWrite (WSP-06 FIRST — precedent: issue-purge-runtime.ts:99 Test 11)
29
+ * 5. --allow-mutations check (RAW_MUTATION_REQUIRES_FLAG)
30
+ * 6. vars parse + Zod validation (RAW_VARS_INVALID)
31
+ * 7. createLinearClient
32
+ * 8. client.client.rawRequest(entry.source, vars) wrapped in S3 transport
33
+ * 9. response.error → LINEAR_API_ERROR (Pitfall 2: STRING not LinearError instance)
34
+ * 10. meta build with opt-in complexity spread + return envelope
35
+ *
36
+ * Why WSP-06 first: a missing --workspace is the more dangerous mistake
37
+ * (cross-workspace permanent write) vs a missing --allow-mutations flag.
38
+ * Test 3 pins this ordering. Precedent: issue-purge-runtime.ts:99 Test 11.
39
+ *
40
+ * Pitfall 2 (RESEARCH § Pitfall 2): `client.client.rawRequest` does NOT
41
+ * throw on GraphQL errors — it returns `LinearRawResponse` with `error?`
42
+ * populated as a STRING. withRateLimitRetry still handles HTTP-layer
43
+ * rate-limits via `instanceof RatelimitedLinearError`. Runtime checks
44
+ * `response.error` explicitly → LINEAR_API_ERROR with `details.cause`.
45
+ */
46
+ async function runRaw(input) {
47
+ const config = (input.loadConfigOverride ?? loadConfig)();
48
+ const env = input.env ?? process.env;
49
+ const envForResolver = {};
50
+ if (env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = env.LINEAR_WORKSPACE;
51
+ if (env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = env.LINEAR_API_KEY;
52
+ const resolved = resolveWorkspace({
53
+ flags: input.flags.workspace ? { workspace: input.flags.workspace } : {},
54
+ env: envForResolver,
55
+ config
56
+ });
57
+ const registry = OPERATION_REGISTRY;
58
+ const entry = Object.hasOwn(registry, input.args.operation) ? registry[input.args.operation] : void 0;
59
+ if (!entry) {
60
+ const suggestions = suggestClosest(input.args.operation, Object.keys(registry));
61
+ throw new LinearAgentError({
62
+ code: "RAW_OPERATION_NOT_FOUND",
63
+ message: `unknown operation: ${input.args.operation}. Did you mean: ${suggestions.join(", ")}?`,
64
+ details: {
65
+ operation: input.args.operation,
66
+ suggestions
67
+ }
68
+ });
69
+ }
70
+ if (entry.kind === "subscription") throw new LinearAgentError({
71
+ code: "OPERATION_SUBSCRIPTIONS_UNSUPPORTED",
72
+ message: `subscription operations are not supported: ${input.args.operation}`,
73
+ details: {
74
+ operation: input.args.operation,
75
+ kind: "subscription"
76
+ }
77
+ });
78
+ if (entry.kind === "mutation") requireExplicitWorkspaceForWrite(resolved, input.flags["allow-active-workspace-write"] ?? false);
79
+ if (entry.kind === "mutation" && !input.flags["allow-mutations"]) throw new LinearAgentError({
80
+ code: "RAW_MUTATION_REQUIRES_FLAG",
81
+ message: `mutation '${input.args.operation}' requires --allow-mutations`,
82
+ details: {
83
+ operation: input.args.operation,
84
+ kind: "mutation"
85
+ }
86
+ });
87
+ const rawVars = await loadVars(input.flags.vars);
88
+ const parsed = entry.varsSchema.safeParse(rawVars);
89
+ if (!parsed.success) throw new LinearAgentError({
90
+ code: "RAW_VARS_INVALID",
91
+ message: `vars failed validation for operation '${input.args.operation}'`,
92
+ details: {
93
+ operation: input.args.operation,
94
+ issues: parsed.error.issues.map((i) => ({
95
+ path: i.path,
96
+ message: i.message,
97
+ code: i.code
98
+ }))
99
+ }
100
+ });
101
+ const client = (input.clientFactoryOverride ?? createLinearClient)(resolved);
102
+ return withFetchInterception(async () => {
103
+ const response = await withRateLimitRetry(() => client.client.rawRequest(entry.source, parsed.data), input.retryOptsOverride);
104
+ if (response.error !== void 0 || response.data === void 0) throw new LinearAgentError({
105
+ code: "LINEAR_API_ERROR",
106
+ message: redact(response.error ?? `rawRequest returned no data for operation '${input.args.operation}'`),
107
+ details: {
108
+ cause: response.error !== void 0 ? redact(response.error) : void 0,
109
+ operation: input.args.operation,
110
+ status: response.status
111
+ }
112
+ });
113
+ const complexity = getLastComplexity();
114
+ const meta = {
115
+ workspace: resolved.name,
116
+ workspaceSource: resolved.source,
117
+ ...complexity !== void 0 ? { complexity } : {}
118
+ };
119
+ return {
120
+ data: response.data,
121
+ meta
122
+ };
123
+ });
124
+ }
125
+ /**
126
+ * Load and parse vars from inline JSON string or @file.json path.
127
+ * File path takes precedence (CONTEXT.md line 51 contract).
128
+ */
129
+ async function loadVars(varsArg) {
130
+ if (!varsArg) return {};
131
+ if (varsArg.startsWith("@")) {
132
+ const path = varsArg.slice(1);
133
+ let raw;
134
+ try {
135
+ raw = await readFile(path, "utf8");
136
+ } catch (err) {
137
+ const code = err.code;
138
+ throw new LinearAgentError({
139
+ code: "GRAPHQL_QUERY_FILE_NOT_FOUND",
140
+ message: `vars file not found or not readable: ${path}`,
141
+ details: {
142
+ path,
143
+ cause: code ?? err.message
144
+ }
145
+ });
146
+ }
147
+ try {
148
+ return JSON.parse(raw);
149
+ } catch (err) {
150
+ throw new LinearAgentError({
151
+ code: "RAW_VARS_INVALID",
152
+ message: `vars file is not valid JSON: ${err.message}`,
153
+ details: {
154
+ path,
155
+ reason: "parse_error"
156
+ }
157
+ });
158
+ }
159
+ }
160
+ try {
161
+ return JSON.parse(varsArg);
162
+ } catch (err) {
163
+ throw new LinearAgentError({
164
+ code: "RAW_VARS_INVALID",
165
+ message: `--vars is not valid JSON: ${err.message}`,
166
+ details: { reason: "parse_error" }
167
+ });
168
+ }
169
+ }
170
+ //#endregion
171
+ export { runRaw };
@@ -0,0 +1,41 @@
1
+ import { buildSchema } from "graphql";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/lib/schema-loader.ts
6
+ /**
7
+ * Lazy module-level cached Linear schema loader.
8
+ *
9
+ * Only the `graphql` command pays the ~50–150ms `buildSchema` cost. The `raw`
10
+ * and `raw batch` commands never import this module, so the cold-start budget
11
+ * for the dominant use case remains unaffected (Phase 5 DST-03).
12
+ *
13
+ * The path resolver works from BOTH:
14
+ * - `dist/lib/schema-loader.js` (built CLI) → ../../schema.graphql
15
+ * - `src/lib/schema-loader.ts` (vitest) → ../../schema.graphql
16
+ *
17
+ * Both sit two levels under the repo root, so the relative path `../../schema.graphql`
18
+ * resolves correctly in both contexts.
19
+ */
20
+ let cachedSchema;
21
+ /**
22
+ * Returns the Linear GraphQLSchema, building it lazily on first call
23
+ * and returning the cached instance on subsequent calls.
24
+ */
25
+ function getLinearSchema() {
26
+ if (cachedSchema) return cachedSchema;
27
+ cachedSchema = buildSchema(readFileSync(resolveSchemaPath(), "utf8"));
28
+ return cachedSchema;
29
+ }
30
+ function resolveSchemaPath() {
31
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "schema.graphql");
32
+ }
33
+ /**
34
+ * Reset the module-level schema cache. For test isolation only — do not use
35
+ * in production code.
36
+ */
37
+ function _resetSchemaCacheForTesting() {
38
+ cachedSchema = void 0;
39
+ }
40
+ //#endregion
41
+ export { _resetSchemaCacheForTesting, getLinearSchema };
@@ -0,0 +1,65 @@
1
+ import { getLinearSchema } from "./schema-loader.js";
2
+ import { introspectionFromSchema, printSchema } from "graphql";
3
+ //#region src/lib/schema-runtime.ts
4
+ /**
5
+ * Schema runtime — Phase 4 PLAN 04-04, INT-03.
6
+ *
7
+ * Exposes the Linear GraphQL schema in two forms:
8
+ * - Default: compact SDL (triple-quoted descriptions stripped) for token-budget efficiency
9
+ * - --full: SDL with descriptions included (opt-in verbose mode)
10
+ * - --json: Standard introspection JSON (__schema format) via `introspectionFromSchema`
11
+ *
12
+ * Zero network calls — reads the committed vendored `schema.graphql` via
13
+ * `getLinearSchema()` (lazy-cached in schema-loader.ts).
14
+ *
15
+ * Counts included in all modes:
16
+ * types_count = 1081 (user types, excluding __* introspection builtins)
17
+ * queries_count = 151
18
+ * mutations_count = 351
19
+ * subscriptions_count = 75
20
+ */
21
+ /**
22
+ * Returns type counts from the Linear GraphQL schema.
23
+ * Excludes built-in `__*` introspection types.
24
+ */
25
+ function getTypeCounts(schema) {
26
+ const typeMap = schema.getTypeMap();
27
+ return {
28
+ types_count: Object.values(typeMap).filter((t) => !t.name.startsWith("__")).length,
29
+ queries_count: Object.keys(schema.getQueryType()?.getFields() ?? {}).length,
30
+ mutations_count: Object.keys(schema.getMutationType()?.getFields() ?? {}).length,
31
+ subscriptions_count: Object.keys(schema.getSubscriptionType()?.getFields() ?? {}).length
32
+ };
33
+ }
34
+ /**
35
+ * Returns the Linear schema as compact SDL (descriptions stripped) or with descriptions,
36
+ * or as standard introspection JSON.
37
+ *
38
+ * This is the named runtime export used by tests and the oclif command shim.
39
+ *
40
+ * Returns `{ data }` only — the command shim wraps the result in the Phase 1
41
+ * envelope via `runCommand`, which sets `meta.command` from the commandPath.
42
+ * No `meta` is returned here on purpose: the runtime owns the data shape and
43
+ * the kernel owns the envelope.
44
+ */
45
+ async function schemaRuntime(args) {
46
+ const { flags } = args;
47
+ const schema = getLinearSchema();
48
+ const counts = getTypeCounts(schema);
49
+ if (flags.json) return { data: {
50
+ schema: introspectionFromSchema(schema, {
51
+ descriptions: flags.full ?? false,
52
+ specifiedByUrl: false,
53
+ directiveIsRepeatable: false,
54
+ schemaDescription: false
55
+ }),
56
+ ...counts
57
+ } };
58
+ const printed = printSchema(schema);
59
+ return { data: {
60
+ schema: flags.full ? printed : printed.replace(/"""[\s\S]*?"""\n?/g, ""),
61
+ ...counts
62
+ } };
63
+ }
64
+ //#endregion
65
+ export { schemaRuntime };
@@ -0,0 +1,93 @@
1
+ import { getLastComplexity, withFetchInterception } from "../core/transport/fetch-interceptor.js";
2
+ import { withRateLimitRetry } from "../core/transport/rate-limit.js";
3
+ import "../core/transport/index.js";
4
+ import { createLinearClient } from "../core/client/factory.js";
5
+ import "../core/client/index.js";
6
+ import { loadConfig } from "../core/config/store.js";
7
+ import "../core/config/index.js";
8
+ import { parseFields, project } from "../core/projection/project.js";
9
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
10
+ import { parsePagination } from "../core/pagination/flags.js";
11
+ import "../core/pagination/index.js";
12
+ import { TEAM_KEY_RE, UUID_RE } from "./filter-heuristics.js";
13
+ //#region src/lib/state-list-runtime.ts
14
+ async function stateListRuntime(input) {
15
+ const config = (input.loadConfigOverride ?? loadConfig)();
16
+ const envForResolver = {};
17
+ if (input.env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = input.env.LINEAR_WORKSPACE;
18
+ if (input.env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = input.env.LINEAR_API_KEY;
19
+ const resolved = resolveWorkspace({
20
+ flags: input.flags.workspace ? { workspace: input.flags.workspace } : {},
21
+ env: envForResolver,
22
+ config
23
+ });
24
+ const fields = parseFields(input.flags.fields ?? "defaults", "state");
25
+ const { first, after } = parsePagination({
26
+ ...input.flags.limit !== void 0 ? { limit: input.flags.limit } : {},
27
+ ...input.flags.cursor !== void 0 ? { cursor: input.flags.cursor } : {}
28
+ });
29
+ let filter;
30
+ if (input.flags.team !== void 0 && input.flags.team !== "") filter = buildTeamFilter(input.flags.team);
31
+ const client = (input.clientFactoryOverride ?? createLinearClient)(resolved);
32
+ return withFetchInterception(async () => {
33
+ const args = { first };
34
+ if (after !== void 0) args.after = after;
35
+ if (filter !== void 0) args.filter = filter;
36
+ const connection = await withRateLimitRetry(() => client.workflowStates(args), input.retryOptsOverride);
37
+ const projected = await Promise.all(connection.nodes.map(async (node) => {
38
+ return project(await hydrateForProjection(node, fields), fields);
39
+ }));
40
+ const complexity = getLastComplexity();
41
+ return {
42
+ data: projected,
43
+ meta: {
44
+ workspace: resolved.name,
45
+ workspaceSource: resolved.source,
46
+ pageInfo: {
47
+ hasNextPage: Boolean(connection.pageInfo?.hasNextPage),
48
+ endCursor: connection.pageInfo?.endCursor ?? null,
49
+ hasPreviousPage: Boolean(connection.pageInfo?.hasPreviousPage),
50
+ startCursor: connection.pageInfo?.startCursor ?? null
51
+ },
52
+ ...complexity !== void 0 ? { complexity } : {}
53
+ }
54
+ };
55
+ });
56
+ }
57
+ function buildTeamFilter(t) {
58
+ if (UUID_RE.test(t)) return { team: { id: { eq: t } } };
59
+ if (TEAM_KEY_RE.test(t)) return { team: { key: { eq: t.toUpperCase() } } };
60
+ return { team: { name: { eq: t } } };
61
+ }
62
+ const RELATION_KEYS = new Set(["team"]);
63
+ async function hydrateForProjection(state, spec) {
64
+ const needs = neededRelations(spec);
65
+ if (needs.size === 0) {
66
+ const out = {};
67
+ for (const k of Object.keys(state)) if (!RELATION_KEYS.has(k)) out[k] = state[k];
68
+ return out;
69
+ }
70
+ const hydrated = {};
71
+ for (const k of Object.keys(state)) if (RELATION_KEYS.has(k)) {
72
+ if (needs.has(k)) {
73
+ const value = state[k];
74
+ hydrated[k] = await resolveLazy(value);
75
+ }
76
+ } else hydrated[k] = state[k];
77
+ return hydrated;
78
+ }
79
+ function neededRelations(spec) {
80
+ if (spec === "*") return new Set(RELATION_KEYS);
81
+ const out = /* @__PURE__ */ new Set();
82
+ for (const path of spec) {
83
+ const head = path.split(".")[0];
84
+ if (head && RELATION_KEYS.has(head)) out.add(head);
85
+ }
86
+ return out;
87
+ }
88
+ async function resolveLazy(value) {
89
+ if (value && typeof value.then === "function") return await value;
90
+ return value;
91
+ }
92
+ //#endregion
93
+ export { stateListRuntime };