forge-cc 0.1.41 → 1.0.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 +454 -338
- package/dist/cli.js +194 -935
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +49 -56
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +37 -125
- package/dist/config/schema.js +13 -28
- package/dist/config/schema.js.map +1 -1
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +148 -0
- package/dist/doctor.js.map +1 -0
- package/dist/gates/index.d.ts +14 -12
- package/dist/gates/index.js +53 -105
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/lint-gate.d.ts +2 -2
- package/dist/gates/lint-gate.js +60 -66
- package/dist/gates/lint-gate.js.map +1 -1
- package/dist/gates/tests-gate.d.ts +2 -4
- package/dist/gates/tests-gate.js +75 -203
- package/dist/gates/tests-gate.js.map +1 -1
- package/dist/gates/types-gate.d.ts +2 -2
- package/dist/gates/types-gate.js +53 -59
- package/dist/gates/types-gate.js.map +1 -1
- package/dist/linear/client.d.ts +31 -108
- package/dist/linear/client.js +88 -388
- package/dist/linear/client.js.map +1 -1
- package/dist/linear/sync.d.ts +15 -0
- package/dist/linear/sync.js +102 -0
- package/dist/linear/sync.js.map +1 -0
- package/dist/runner/loop.d.ts +4 -0
- package/dist/runner/loop.js +168 -0
- package/dist/runner/loop.js.map +1 -0
- package/dist/runner/prompt.d.ts +14 -0
- package/dist/runner/prompt.js +59 -0
- package/dist/runner/prompt.js.map +1 -0
- package/dist/runner/update.d.ts +1 -0
- package/dist/runner/update.js +72 -0
- package/dist/runner/update.js.map +1 -0
- package/dist/server.d.ts +6 -2
- package/dist/server.js +43 -101
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +208 -0
- package/dist/setup.js.map +1 -0
- package/dist/state/cache.d.ts +3 -0
- package/dist/state/cache.js +23 -0
- package/dist/state/cache.js.map +1 -0
- package/dist/state/status.d.ts +66 -0
- package/dist/state/status.js +96 -0
- package/dist/state/status.js.map +1 -0
- package/dist/types.d.ts +46 -114
- package/dist/worktree/manager.d.ts +6 -103
- package/dist/worktree/manager.js +25 -296
- package/dist/worktree/manager.js.map +1 -1
- package/hooks/pre-commit-verify.js +109 -109
- package/package.json +3 -2
- package/skills/forge-go.md +20 -13
- package/skills/forge-setup.md +149 -388
- package/skills/forge-spec.md +367 -342
- package/skills/forge-triage.md +179 -133
- package/skills/forge-update.md +87 -93
package/dist/gates/tests-gate.js
CHANGED
|
@@ -1,219 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Common test failure patterns with file/line info:
|
|
7
|
-
* FAIL src/foo.test.ts > suite > test name
|
|
8
|
-
* at src/foo.test.ts:42:10
|
|
9
|
-
*/
|
|
10
|
-
const TEST_FILE_RE = /^FAIL\s+(.+?)(?:\s+>|$)/;
|
|
11
|
-
const STACKTRACE_RE = /at\s+.*?([^\s(]+):(\d+):\d+/;
|
|
12
|
-
export async function verifyTests(projectDir, options) {
|
|
13
|
-
const start = Date.now();
|
|
14
|
-
const errors = [];
|
|
15
|
-
const warnings = [];
|
|
16
|
-
// Load config and run test analysis
|
|
17
|
-
const config = loadConfig(options?.configRoot ?? projectDir);
|
|
18
|
-
const testingConfig = config.testing;
|
|
19
|
-
const analysis = await analyzeTestCoverage(projectDir);
|
|
20
|
-
// Detect whether a test script exists in package.json
|
|
21
|
-
let hasTestScript = false;
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Detect test runner from package.json dependencies. */
|
|
5
|
+
async function detectRunner(projectDir) {
|
|
22
6
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
const content = await readFile(join(projectDir, "package.json"), "utf-8");
|
|
8
|
+
const pkg = JSON.parse(content);
|
|
9
|
+
const deps = {
|
|
10
|
+
...pkg.dependencies,
|
|
11
|
+
...pkg.devDependencies,
|
|
12
|
+
};
|
|
13
|
+
if ("vitest" in deps)
|
|
14
|
+
return "vitest";
|
|
15
|
+
if ("jest" in deps)
|
|
16
|
+
return "jest";
|
|
30
17
|
}
|
|
31
18
|
catch {
|
|
32
|
-
// No package.json
|
|
33
|
-
}
|
|
34
|
-
// -----------------------------------------------------------------------
|
|
35
|
-
// Baseline check: If zero test files AND no test script, FAIL immediately
|
|
36
|
-
// -----------------------------------------------------------------------
|
|
37
|
-
if (analysis.coverage.testFiles === 0 && !hasTestScript) {
|
|
38
|
-
const categoryNames = analysis.categories.map(c => c.name).join(", ");
|
|
39
|
-
const msg = `No tests found. ${analysis.coverage.sourceFiles} source file${analysis.coverage.sourceFiles === 1 ? "" : "s"} across ${analysis.categories.length} categor${analysis.categories.length === 1 ? "y" : "ies"} (${categoryNames || "none"}) have no test coverage. Run \`/forge:setup\` to scaffold tests.`;
|
|
40
|
-
const error = { message: msg };
|
|
41
|
-
error.remediation = buildTestCoverageRemediation(error);
|
|
42
|
-
errors.push(error);
|
|
43
|
-
return {
|
|
44
|
-
gate: "tests",
|
|
45
|
-
passed: false,
|
|
46
|
-
errors,
|
|
47
|
-
warnings,
|
|
48
|
-
duration_ms: Date.now() - start,
|
|
49
|
-
};
|
|
19
|
+
// No package.json — fall through
|
|
50
20
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
errors.push(error);
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
async function runTestRunner(projectDir) {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
const runner = await detectRunner(projectDir);
|
|
26
|
+
if (runner === "unknown") {
|
|
58
27
|
return {
|
|
59
28
|
gate: "tests",
|
|
60
29
|
passed: false,
|
|
61
|
-
errors,
|
|
62
|
-
|
|
63
|
-
duration_ms: Date.now() - start,
|
|
30
|
+
errors: [{ file: "", line: 0, message: "No test runner detected (install vitest or jest)" }],
|
|
31
|
+
durationMs: Date.now() - start,
|
|
64
32
|
};
|
|
65
33
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const stdout = err instanceof Error && "stdout" in err
|
|
89
|
-
? String(err.stdout)
|
|
90
|
-
: "";
|
|
91
|
-
const stderr = err instanceof Error && "stderr" in err
|
|
92
|
-
? String(err.stderr)
|
|
93
|
-
: "";
|
|
94
|
-
const output = `${stdout}\n${stderr}`;
|
|
95
|
-
let lastFailFile;
|
|
96
|
-
for (const line of output.split("\n")) {
|
|
97
|
-
const trimmed = line.trim();
|
|
98
|
-
if (!trimmed)
|
|
99
|
-
continue;
|
|
100
|
-
// Track which test file we're in
|
|
101
|
-
const failMatch = TEST_FILE_RE.exec(trimmed);
|
|
102
|
-
if (failMatch) {
|
|
103
|
-
lastFailFile = failMatch[1];
|
|
104
|
-
}
|
|
105
|
-
// Try to extract stack trace location
|
|
106
|
-
const stackMatch = STACKTRACE_RE.exec(trimmed);
|
|
107
|
-
if (stackMatch) {
|
|
108
|
-
lastFailFile = stackMatch[1];
|
|
109
|
-
}
|
|
110
|
-
if (trimmed.includes("FAIL") ||
|
|
111
|
-
trimmed.includes("AssertionError") ||
|
|
112
|
-
trimmed.includes("AssertionError") ||
|
|
113
|
-
trimmed.includes("Expected") ||
|
|
114
|
-
trimmed.includes("Received")) {
|
|
34
|
+
const cmd = runner === "vitest" ? ["vitest", "run"] : ["jest", "--ci"];
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const child = spawn("npx", cmd, {
|
|
37
|
+
cwd: projectDir,
|
|
38
|
+
shell: true,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
child.stdout.on("data", (data) => {
|
|
44
|
+
stdout += data.toString();
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on("data", (data) => {
|
|
47
|
+
stderr += data.toString();
|
|
48
|
+
});
|
|
49
|
+
child.on("close", (code) => {
|
|
50
|
+
const errors = [];
|
|
51
|
+
if (code !== 0) {
|
|
52
|
+
const output = stdout + stderr;
|
|
53
|
+
const failRegex = /FAIL\s+(.+)/g;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = failRegex.exec(output)) !== null) {
|
|
115
56
|
errors.push({
|
|
116
|
-
file:
|
|
117
|
-
line:
|
|
118
|
-
message:
|
|
57
|
+
file: match[1].trim(),
|
|
58
|
+
line: 0,
|
|
59
|
+
message: `Test suite failed: ${match[1].trim()}`,
|
|
119
60
|
});
|
|
120
61
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
warnings.push(`${passed} passed, ${failed} failed`);
|
|
128
|
-
}
|
|
129
|
-
if (errors.length === 0) {
|
|
130
|
-
errors.push({ message: "Test runner exited with non-zero status" });
|
|
131
|
-
}
|
|
132
|
-
// Enrich errors with remediation hints
|
|
133
|
-
for (const error of errors) {
|
|
134
|
-
error.remediation = buildTestRemediation(error);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// -----------------------------------------------------------------------
|
|
139
|
-
// Enforcement check: Verify changed files have corresponding tests
|
|
140
|
-
// -----------------------------------------------------------------------
|
|
141
|
-
if (testingConfig?.enforce) {
|
|
142
|
-
const changedSourceFiles = getChangedSourceFiles(projectDir);
|
|
143
|
-
if (changedSourceFiles.length > 0) {
|
|
144
|
-
const untestedSet = new Set(analysis.coverage.untestedFiles);
|
|
145
|
-
for (const file of changedSourceFiles) {
|
|
146
|
-
const normalized = file.replace(/\\/g, "/");
|
|
147
|
-
if (untestedSet.has(normalized)) {
|
|
148
|
-
const error = {
|
|
149
|
-
file: normalized,
|
|
150
|
-
message: `Missing test file for changed source: ${normalized}`,
|
|
151
|
-
};
|
|
152
|
-
error.remediation = buildTestCoverageRemediation(error);
|
|
153
|
-
errors.push(error);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// -----------------------------------------------------------------------
|
|
159
|
-
// Thin coverage advisory
|
|
160
|
-
// -----------------------------------------------------------------------
|
|
161
|
-
if (analysis.coverage.testFiles > 0 && analysis.coverage.ratio < 0.3) {
|
|
162
|
-
warnings.push(`Thin test coverage: ratio ${analysis.coverage.ratio} (${analysis.coverage.testFiles} test file${analysis.coverage.testFiles === 1 ? "" : "s"} for ${analysis.coverage.sourceFiles} source file${analysis.coverage.sourceFiles === 1 ? "" : "s"}). Consider adding tests for untested files.`);
|
|
163
|
-
}
|
|
164
|
-
const passed = testsRanSuccessfully && errors.length === 0;
|
|
165
|
-
return {
|
|
166
|
-
gate: "tests",
|
|
167
|
-
passed,
|
|
168
|
-
errors,
|
|
169
|
-
warnings,
|
|
170
|
-
duration_ms: Date.now() - start,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Get source files changed relative to HEAD~1 or the staging area.
|
|
175
|
-
* Returns paths relative to projectDir, normalized with forward slashes.
|
|
176
|
-
*/
|
|
177
|
-
function getChangedSourceFiles(projectDir) {
|
|
178
|
-
const files = [];
|
|
179
|
-
// Try git diff against HEAD~1 first, fall back to cached diff
|
|
180
|
-
for (const cmd of [
|
|
181
|
-
"git diff --name-only HEAD~1",
|
|
182
|
-
"git diff --cached --name-only",
|
|
183
|
-
]) {
|
|
184
|
-
try {
|
|
185
|
-
const output = execSync(cmd, {
|
|
186
|
-
cwd: projectDir,
|
|
187
|
-
stdio: "pipe",
|
|
188
|
-
timeout: 10_000,
|
|
189
|
-
}).toString().trim();
|
|
190
|
-
if (output) {
|
|
191
|
-
for (const line of output.split("\n")) {
|
|
192
|
-
const trimmed = line.trim();
|
|
193
|
-
if (!trimmed)
|
|
194
|
-
continue;
|
|
195
|
-
// Only include source files (not test files, not configs)
|
|
196
|
-
if (isSourceFilePath(trimmed)) {
|
|
197
|
-
files.push(trimmed.replace(/\\/g, "/"));
|
|
198
|
-
}
|
|
62
|
+
if (errors.length === 0) {
|
|
63
|
+
errors.push({
|
|
64
|
+
file: "",
|
|
65
|
+
line: 0,
|
|
66
|
+
message: `Test runner exited with code ${code}`,
|
|
67
|
+
});
|
|
199
68
|
}
|
|
200
|
-
break; // Use the first successful command
|
|
201
69
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
70
|
+
resolve({
|
|
71
|
+
gate: "tests",
|
|
72
|
+
passed: code === 0,
|
|
73
|
+
errors,
|
|
74
|
+
durationMs: Date.now() - start,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
child.on("error", (err) => {
|
|
78
|
+
resolve({
|
|
79
|
+
gate: "tests",
|
|
80
|
+
passed: false,
|
|
81
|
+
errors: [{ file: "", line: 0, message: err.message }],
|
|
82
|
+
durationMs: Date.now() - start,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
218
86
|
}
|
|
87
|
+
export const testsGate = {
|
|
88
|
+
name: "tests",
|
|
89
|
+
run: runTestRunner,
|
|
90
|
+
};
|
|
219
91
|
//# sourceMappingURL=tests-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tests-gate.js","sourceRoot":"","sources":["../../src/gates/tests-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"tests-gate.js","sourceRoot":"","sources":["../../src/gates/tests-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
1
|
+
import type { Gate } from "./index.js";
|
|
2
|
+
export declare const typesGate: Gate;
|
package/dist/gates/types-gate.js
CHANGED
|
@@ -1,64 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const TSC_ERROR_RE = /^(.+?)\((\d+),\d+\):\s*(.+)$/;
|
|
5
|
-
export async function verifyTypes(projectDir) {
|
|
6
|
-
const start = Date.now();
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** Parse tsc output lines into structured errors. */
|
|
3
|
+
function parseTscOutput(output) {
|
|
7
4
|
const errors = [];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
// tsc error format: file(line,col): error TSxxxx: message
|
|
6
|
+
const regex = /^(.+?)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)$/gm;
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = regex.exec(output)) !== null) {
|
|
9
|
+
errors.push({
|
|
10
|
+
file: match[1],
|
|
11
|
+
line: parseInt(match[2], 10),
|
|
12
|
+
column: parseInt(match[3], 10),
|
|
13
|
+
message: match[4],
|
|
14
14
|
});
|
|
15
|
-
return {
|
|
16
|
-
gate: "types",
|
|
17
|
-
passed: true,
|
|
18
|
-
errors,
|
|
19
|
-
warnings,
|
|
20
|
-
duration_ms: Date.now() - start,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
const output = err instanceof Error && "stdout" in err
|
|
25
|
-
? String(err.stdout)
|
|
26
|
-
: "";
|
|
27
|
-
for (const line of output.split("\n")) {
|
|
28
|
-
const trimmed = line.trim();
|
|
29
|
-
if (!trimmed)
|
|
30
|
-
continue;
|
|
31
|
-
if (trimmed.toLowerCase().includes("warning")) {
|
|
32
|
-
warnings.push(trimmed);
|
|
33
|
-
}
|
|
34
|
-
else if (trimmed.includes("error TS")) {
|
|
35
|
-
const match = TSC_ERROR_RE.exec(trimmed);
|
|
36
|
-
if (match) {
|
|
37
|
-
errors.push({
|
|
38
|
-
file: match[1],
|
|
39
|
-
line: Number.parseInt(match[2], 10),
|
|
40
|
-
message: match[3],
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
errors.push({ message: trimmed });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (errors.length === 0) {
|
|
49
|
-
errors.push({ message: "tsc exited with non-zero status but no TS errors were parsed" });
|
|
50
|
-
}
|
|
51
|
-
// Enrich errors with remediation hints
|
|
52
|
-
for (const error of errors) {
|
|
53
|
-
error.remediation = buildTypeRemediation(error);
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
gate: "types",
|
|
57
|
-
passed: false,
|
|
58
|
-
errors,
|
|
59
|
-
warnings,
|
|
60
|
-
duration_ms: Date.now() - start,
|
|
61
|
-
};
|
|
62
15
|
}
|
|
16
|
+
return errors;
|
|
17
|
+
}
|
|
18
|
+
function runTsc(projectDir) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const child = spawn("npx", ["tsc", "--noEmit"], {
|
|
22
|
+
cwd: projectDir,
|
|
23
|
+
shell: true,
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
});
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stdout.on("data", (data) => {
|
|
29
|
+
stdout += data.toString();
|
|
30
|
+
});
|
|
31
|
+
child.stderr.on("data", (data) => {
|
|
32
|
+
stderr += data.toString();
|
|
33
|
+
});
|
|
34
|
+
child.on("close", (code) => {
|
|
35
|
+
const output = stdout + stderr;
|
|
36
|
+
const errors = parseTscOutput(output);
|
|
37
|
+
resolve({
|
|
38
|
+
gate: "types",
|
|
39
|
+
passed: code === 0,
|
|
40
|
+
errors,
|
|
41
|
+
durationMs: Date.now() - start,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
child.on("error", (err) => {
|
|
45
|
+
resolve({
|
|
46
|
+
gate: "types",
|
|
47
|
+
passed: false,
|
|
48
|
+
errors: [{ file: "", line: 0, message: err.message }],
|
|
49
|
+
durationMs: Date.now() - start,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
63
53
|
}
|
|
54
|
+
export const typesGate = {
|
|
55
|
+
name: "types",
|
|
56
|
+
run: runTsc,
|
|
57
|
+
};
|
|
64
58
|
//# sourceMappingURL=types-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-gate.js","sourceRoot":"","sources":["../../src/gates/types-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"types-gate.js","sourceRoot":"","sources":["../../src/gates/types-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAI3C,qDAAqD;AACrD,SAAS,cAAc,CAAC,MAAc;IACpC,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,0DAA0D;IAC1D,MAAM,KAAK,GAAG,mDAAmD,CAAC;IAClE,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACd,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC9B,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;SAClB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,MAAM,CAAC,UAAkB;IAChC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE;YAC9C,GAAG,EAAE,UAAU;YACf,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvC,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YACvC,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;YAC/B,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;YACtC,OAAO,CAAC;gBACN,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,IAAI,KAAK,CAAC;gBAClB,MAAM;gBACN,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;aAC/B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,OAAO,CAAC;gBACN,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC;gBACrD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;aAC/B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAS;IAC7B,IAAI,EAAE,OAAO;IACb,GAAG,EAAE,MAAM;CACZ,CAAC"}
|
package/dist/linear/client.d.ts
CHANGED
|
@@ -1,114 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Used by lifecycle modules (projects.ts, milestones.ts, issues.ts)
|
|
4
|
-
* and the execution engine for programmatic Linear management.
|
|
5
|
-
*/
|
|
6
|
-
export interface LinearProject {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
state: string;
|
|
11
|
-
url: string;
|
|
12
|
-
}
|
|
13
|
-
export interface LinearMilestone {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
progress: number;
|
|
18
|
-
sortOrder: number;
|
|
19
|
-
}
|
|
20
|
-
export interface LinearIssue {
|
|
21
|
-
id: string;
|
|
22
|
-
identifier: string;
|
|
23
|
-
title: string;
|
|
24
|
-
description?: string;
|
|
25
|
-
state: string;
|
|
26
|
-
projectId?: string;
|
|
27
|
-
milestoneId?: string;
|
|
1
|
+
export interface ForgeLinearClientOptions {
|
|
2
|
+
apiKey: string;
|
|
28
3
|
teamId?: string;
|
|
29
|
-
url: string;
|
|
30
|
-
}
|
|
31
|
-
export interface LinearTeam {
|
|
32
|
-
id: string;
|
|
33
|
-
name: string;
|
|
34
|
-
key: string;
|
|
35
|
-
}
|
|
36
|
-
export interface CreateProjectInput {
|
|
37
|
-
name: string;
|
|
38
|
-
description?: string;
|
|
39
|
-
teamIds: string[];
|
|
40
|
-
state?: string;
|
|
41
|
-
}
|
|
42
|
-
export interface UpdateProjectInput {
|
|
43
|
-
name?: string;
|
|
44
|
-
description?: string;
|
|
45
|
-
state?: string;
|
|
46
|
-
}
|
|
47
|
-
export interface CreateMilestoneInput {
|
|
48
|
-
projectId: string;
|
|
49
|
-
name: string;
|
|
50
|
-
description?: string;
|
|
51
|
-
targetDate?: string;
|
|
52
|
-
}
|
|
53
|
-
export interface CreateIssueInput {
|
|
54
|
-
title: string;
|
|
55
|
-
description?: string;
|
|
56
|
-
teamId: string;
|
|
57
|
-
projectId?: string;
|
|
58
|
-
milestoneId?: string;
|
|
59
|
-
priority?: number;
|
|
60
|
-
state?: string;
|
|
61
4
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
listIssues(opts?: {
|
|
91
|
-
projectId?: string;
|
|
92
|
-
milestoneId?: string;
|
|
93
|
-
state?: string;
|
|
94
|
-
}): Promise<LinearIssue[]>;
|
|
95
|
-
createIssue(input: CreateIssueInput): Promise<LinearIssue>;
|
|
96
|
-
updateIssue(id: string, input: UpdateIssueInput): Promise<LinearIssue>;
|
|
97
|
-
createComment(issueId: string, body: string): Promise<void>;
|
|
98
|
-
listTeams(): Promise<LinearTeam[]>;
|
|
99
|
-
listWorkflowStates(teamId: string): Promise<Array<{
|
|
5
|
+
/**
|
|
6
|
+
* Thin wrapper around @linear/sdk's LinearClient, scoped to a team.
|
|
7
|
+
* All public methods degrade gracefully on API errors (warn, don't crash).
|
|
8
|
+
*/
|
|
9
|
+
export declare class ForgeLinearClient {
|
|
10
|
+
private readonly client;
|
|
11
|
+
private readonly teamId;
|
|
12
|
+
constructor(opts: ForgeLinearClientOptions);
|
|
13
|
+
/** Resolve a workflow state UUID by its display name for a given team. */
|
|
14
|
+
resolveStateId(teamId: string, stateName: string): Promise<string>;
|
|
15
|
+
/** Update an issue's workflow state. */
|
|
16
|
+
updateIssueState(issueId: string, stateId: string): Promise<void>;
|
|
17
|
+
/** Update a project's status (state name in Linear projects). */
|
|
18
|
+
updateProjectState(projectId: string, stateId: string): Promise<void>;
|
|
19
|
+
/** List all teams visible to the authenticated user. */
|
|
20
|
+
listTeams(): Promise<Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
key: string;
|
|
24
|
+
}>>;
|
|
25
|
+
/** List issues belonging to a project. Returns identifier (e.g. "MSIG-123") and title. */
|
|
26
|
+
listIssuesByProject(projectId: string): Promise<Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
identifier: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}>>;
|
|
31
|
+
/** List projects filtered by team. */
|
|
32
|
+
listProjects(teamId: string): Promise<Array<{
|
|
100
33
|
id: string;
|
|
101
34
|
name: string;
|
|
35
|
+
state: string;
|
|
102
36
|
}>>;
|
|
103
|
-
/**
|
|
104
|
-
* Execute a GraphQL request against the Linear API with retry + backoff.
|
|
105
|
-
*/
|
|
106
|
-
private request;
|
|
107
|
-
/**
|
|
108
|
-
* Auto-paginate a connection query. The query MUST accept `$after: String`
|
|
109
|
-
* and the root field must return `{ pageInfo { hasNextPage endCursor } nodes { ... } }`.
|
|
110
|
-
*/
|
|
111
|
-
private paginate;
|
|
112
|
-
/** Normalize a raw issue node from GraphQL into our flat LinearIssue shape. */
|
|
113
|
-
private mapIssue;
|
|
114
37
|
}
|