nexus-agents 2.28.0 → 2.29.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.
- package/README.md +11 -10
- package/dist/adaptive-memory-RST6DZYR.js +15 -0
- package/dist/chunk-2UR7YN6T.js +700 -0
- package/dist/chunk-2UR7YN6T.js.map +1 -0
- package/dist/chunk-2UUUKVNR.js +61 -0
- package/dist/chunk-2UUUKVNR.js.map +1 -0
- package/dist/chunk-3EVVQ32X.js +838 -0
- package/dist/chunk-3EVVQ32X.js.map +1 -0
- package/dist/{chunk-UGNLR4NZ.js → chunk-3GXDN4AX.js} +2 -2
- package/dist/{chunk-LKSTILEE.js → chunk-4AGPR6XZ.js} +194 -1461
- package/dist/chunk-4AGPR6XZ.js.map +1 -0
- package/dist/chunk-4HA5PAL7.js +246 -0
- package/dist/chunk-4HA5PAL7.js.map +1 -0
- package/dist/chunk-633WH2ML.js +127 -0
- package/dist/chunk-633WH2ML.js.map +1 -0
- package/dist/chunk-7SKAKA4I.js +90 -0
- package/dist/chunk-7SKAKA4I.js.map +1 -0
- package/dist/chunk-AP2FD37C.js +127 -0
- package/dist/chunk-AP2FD37C.js.map +1 -0
- package/dist/chunk-BC3M4VLP.js +359 -0
- package/dist/chunk-BC3M4VLP.js.map +1 -0
- package/dist/chunk-BQ4YXGGQ.js +127 -0
- package/dist/chunk-BQ4YXGGQ.js.map +1 -0
- package/dist/chunk-C2C5ONFR.js +195 -0
- package/dist/chunk-C2C5ONFR.js.map +1 -0
- package/dist/chunk-CGWRJ4EM.js +12661 -0
- package/dist/chunk-CGWRJ4EM.js.map +1 -0
- package/dist/chunk-CLYZ7FWP.js +30 -0
- package/dist/chunk-CLYZ7FWP.js.map +1 -0
- package/dist/chunk-ED6VQWNG.js +63 -0
- package/dist/chunk-ED6VQWNG.js.map +1 -0
- package/dist/{chunk-L2SHSW4T.js → chunk-ELIFTCYM.js} +2705 -2473
- package/dist/chunk-ELIFTCYM.js.map +1 -0
- package/dist/chunk-FYJVXQHX.js +944 -0
- package/dist/chunk-FYJVXQHX.js.map +1 -0
- package/dist/{chunk-QZEAD6AG.js → chunk-FZFZ77UJ.js} +19434 -38182
- package/dist/chunk-FZFZ77UJ.js.map +1 -0
- package/dist/chunk-IECE6DBS.js +1585 -0
- package/dist/chunk-IECE6DBS.js.map +1 -0
- package/dist/chunk-LLGUX44Z.js +356 -0
- package/dist/chunk-LLGUX44Z.js.map +1 -0
- package/dist/chunk-MRU6L7YJ.js +931 -0
- package/dist/chunk-MRU6L7YJ.js.map +1 -0
- package/dist/chunk-N5SY7V45.js +44 -0
- package/dist/chunk-N5SY7V45.js.map +1 -0
- package/dist/chunk-OOIPRRPX.js +3790 -0
- package/dist/chunk-OOIPRRPX.js.map +1 -0
- package/dist/chunk-SRECH7OQ.js +354 -0
- package/dist/chunk-SRECH7OQ.js.map +1 -0
- package/dist/{chunk-YSDUVCCZ.js → chunk-VKRMXD62.js} +6 -6
- package/dist/{chunk-E7EX2KQJ.js → chunk-XU3NADFE.js} +2 -2
- package/dist/chunk-YSTJEMQX.js +122 -0
- package/dist/chunk-YSTJEMQX.js.map +1 -0
- package/dist/cli-circuit-breaker-5FAODXVY.js +13 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +1485 -683
- package/dist/cli.js.map +1 -1
- package/dist/codebase-search-CZUA37RU.js +9 -0
- package/dist/composite-router-FNW7ZWL7.js +17 -0
- package/dist/composite-router-FNW7ZWL7.js.map +1 -0
- package/dist/consensus-vote-757YULIP.js +25 -0
- package/dist/consensus-vote-757YULIP.js.map +1 -0
- package/dist/{dist-H5XNXVAV.js → dist-CV74KUT7.js} +1521 -972
- package/dist/dist-CV74KUT7.js.map +1 -0
- package/dist/{doctor-deep-BDE2PHVX.js → doctor-deep-LMCEAFU4.js} +4 -3
- package/dist/doctor-deep-LMCEAFU4.js.map +1 -0
- package/dist/expert-bridge-L2D4OXOR.js +10 -0
- package/dist/expert-bridge-L2D4OXOR.js.map +1 -0
- package/dist/expert-config-A5CHKUGI.js +22 -0
- package/dist/expert-config-A5CHKUGI.js.map +1 -0
- package/dist/factory-ELEDP2WD.js +17 -0
- package/dist/factory-ELEDP2WD.js.map +1 -0
- package/dist/factory-IDTIBX6B.js +14 -0
- package/dist/factory-IDTIBX6B.js.map +1 -0
- package/dist/index.d.ts +1814 -777
- package/dist/index.js +785 -355
- package/dist/index.js.map +1 -1
- package/dist/issue-triage-SJPKJLXH.js +15 -0
- package/dist/issue-triage-SJPKJLXH.js.map +1 -0
- package/dist/learning-persistence-WMWZJZ35.js +16 -0
- package/dist/learning-persistence-WMWZJZ35.js.map +1 -0
- package/dist/mcp-config-2OXIOMJ6.js +12 -0
- package/dist/mcp-config-2OXIOMJ6.js.map +1 -0
- package/dist/mobimem-5S3VLNSU.js +13 -0
- package/dist/mobimem-5S3VLNSU.js.map +1 -0
- package/dist/{model-capabilities-types-CSWO27YN.d.ts → model-capabilities-types-B57GZryc.d.ts} +1 -1
- package/dist/repo-analyze-HWMXSK5C.js +24 -0
- package/dist/repo-analyze-HWMXSK5C.js.map +1 -0
- package/dist/repo-security-plan-MUFDGWSQ.js +17 -0
- package/dist/repo-security-plan-MUFDGWSQ.js.map +1 -0
- package/dist/research-helpers-synthesize-OBQJ5BGX.js +10 -0
- package/dist/research-helpers-synthesize-OBQJ5BGX.js.map +1 -0
- package/dist/routing-memory-3QBQTS4A.js +13 -0
- package/dist/routing-memory-3QBQTS4A.js.map +1 -0
- package/dist/session-memory-VXWLOFRC.js +22 -0
- package/dist/session-memory-VXWLOFRC.js.map +1 -0
- package/dist/{setup-command-SS7LMN7Y.js → setup-command-E6MXO5RZ.js} +9 -5
- package/dist/setup-command-E6MXO5RZ.js.map +1 -0
- package/dist/setup-config-O5F3AZBL.js +10 -0
- package/dist/setup-config-O5F3AZBL.js.map +1 -0
- package/dist/shared-memory-AEO2HJLC.js +8 -0
- package/dist/shared-memory-AEO2HJLC.js.map +1 -0
- package/dist/symbol-extractor-UEBANFSN.js +10 -0
- package/dist/symbol-extractor-UEBANFSN.js.map +1 -0
- package/dist/weather-report-MUGSIOU5.js +15 -0
- package/dist/weather-report-MUGSIOU5.js.map +1 -0
- package/package.json +18 -19
- package/dist/chunk-L2SHSW4T.js.map +0 -1
- package/dist/chunk-LKSTILEE.js.map +0 -1
- package/dist/chunk-QZEAD6AG.js.map +0 -1
- package/dist/dist-H5XNXVAV.js.map +0 -1
- package/dist/setup-config-DSMOOLVW.js +0 -9
- /package/dist/{doctor-deep-BDE2PHVX.js.map → adaptive-memory-RST6DZYR.js.map} +0 -0
- /package/dist/{chunk-UGNLR4NZ.js.map → chunk-3GXDN4AX.js.map} +0 -0
- /package/dist/{chunk-YSDUVCCZ.js.map → chunk-VKRMXD62.js.map} +0 -0
- /package/dist/{chunk-E7EX2KQJ.js.map → chunk-XU3NADFE.js.map} +0 -0
- /package/dist/{setup-command-SS7LMN7Y.js.map → cli-circuit-breaker-5FAODXVY.js.map} +0 -0
- /package/dist/{setup-config-DSMOOLVW.js.map → codebase-search-CZUA37RU.js.map} +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger,
|
|
3
|
+
err,
|
|
4
|
+
getErrorMessage,
|
|
5
|
+
ok
|
|
6
|
+
} from "./chunk-ELIFTCYM.js";
|
|
7
|
+
|
|
8
|
+
// src/scm/types.ts
|
|
9
|
+
var ScmError = class extends Error {
|
|
10
|
+
constructor(message, platform, statusCode, context) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.platform = platform;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.name = "ScmError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/scm/github-provider.ts
|
|
20
|
+
import { execFile } from "child_process";
|
|
21
|
+
import { promisify } from "util";
|
|
22
|
+
var execFileAsync = promisify(execFile);
|
|
23
|
+
var logger = createLogger({ component: "GitHubProvider" });
|
|
24
|
+
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
25
|
+
var GH_TIMEOUT_MS = 3e4;
|
|
26
|
+
async function execGh(args, repo) {
|
|
27
|
+
const fullArgs = [...args, "--repo", repo];
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execFileAsync("gh", fullArgs, {
|
|
30
|
+
maxBuffer: MAX_BUFFER,
|
|
31
|
+
timeout: GH_TIMEOUT_MS
|
|
32
|
+
});
|
|
33
|
+
return ok(stdout.trim());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const execError = error;
|
|
36
|
+
return err(
|
|
37
|
+
new ScmError(`gh command failed: ${execError.message}`, "github", void 0, {
|
|
38
|
+
command: `gh ${fullArgs.join(" ")}`,
|
|
39
|
+
stderr: execError.stderr
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function mapIssue(raw) {
|
|
45
|
+
return {
|
|
46
|
+
number: raw.number,
|
|
47
|
+
title: raw.title,
|
|
48
|
+
body: raw.body ?? "",
|
|
49
|
+
labels: raw.labels.map((l) => l.name),
|
|
50
|
+
author: raw.author.login,
|
|
51
|
+
createdAt: raw.createdAt
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function mapComment(raw) {
|
|
55
|
+
return {
|
|
56
|
+
id: raw.id,
|
|
57
|
+
body: raw.body,
|
|
58
|
+
author: raw.author.login,
|
|
59
|
+
createdAt: raw.createdAt
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function mapPRStatus(raw) {
|
|
63
|
+
const mergeable = raw.mergeable === "MERGEABLE";
|
|
64
|
+
let checksStatus = "pending";
|
|
65
|
+
if (raw.statusCheckRollup !== null && raw.statusCheckRollup.length > 0) {
|
|
66
|
+
const hasFailure = raw.statusCheckRollup.some((c) => c.state === "FAILURE");
|
|
67
|
+
const allSuccess = raw.statusCheckRollup.every(
|
|
68
|
+
(c) => c.state === "SUCCESS" || c.state === "NEUTRAL" || c.state === "SKIPPED"
|
|
69
|
+
);
|
|
70
|
+
checksStatus = hasFailure ? "failure" : allSuccess ? "success" : "pending";
|
|
71
|
+
}
|
|
72
|
+
let reviewStatus = "pending";
|
|
73
|
+
if (raw.reviewDecision === "APPROVED") reviewStatus = "approved";
|
|
74
|
+
else if (raw.reviewDecision === "CHANGES_REQUESTED") reviewStatus = "changes_requested";
|
|
75
|
+
return { mergeable, checksStatus, reviewStatus };
|
|
76
|
+
}
|
|
77
|
+
var GitHubProvider = class {
|
|
78
|
+
constructor(repo) {
|
|
79
|
+
this.repo = repo;
|
|
80
|
+
}
|
|
81
|
+
platform = "github";
|
|
82
|
+
async getIssue(number) {
|
|
83
|
+
const fields = "number,title,body,labels,author,createdAt";
|
|
84
|
+
const args = ["issue", "view", String(number), "--json", fields];
|
|
85
|
+
logger.debug("Getting issue", { repo: this.repo, number });
|
|
86
|
+
const result = await execGh(args, this.repo);
|
|
87
|
+
if (!result.ok) return result;
|
|
88
|
+
try {
|
|
89
|
+
return ok(mapIssue(JSON.parse(result.value)));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return err(
|
|
92
|
+
new ScmError(
|
|
93
|
+
`Failed to parse issue JSON: ${getErrorMessage(error)} \u2014 preview: ${result.value.slice(0, 120)}`,
|
|
94
|
+
"github"
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async listIssues(filters) {
|
|
100
|
+
const fields = "number,title,body,labels,author,createdAt";
|
|
101
|
+
const args = ["issue", "list", "--json", fields];
|
|
102
|
+
if (filters?.labels !== void 0 && filters.labels.length > 0) {
|
|
103
|
+
args.push("--label", filters.labels.join(","));
|
|
104
|
+
}
|
|
105
|
+
if (filters?.state !== void 0) {
|
|
106
|
+
args.push("--state", filters.state);
|
|
107
|
+
}
|
|
108
|
+
args.push("--limit", String(filters?.limit ?? 50));
|
|
109
|
+
logger.debug("Listing issues", { repo: this.repo, filters });
|
|
110
|
+
const result = await execGh(args, this.repo);
|
|
111
|
+
if (!result.ok) return result;
|
|
112
|
+
try {
|
|
113
|
+
const issues = JSON.parse(result.value);
|
|
114
|
+
return ok(issues.map(mapIssue));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return err(
|
|
117
|
+
new ScmError(
|
|
118
|
+
`Failed to parse issues JSON: ${getErrorMessage(error)} \u2014 preview: ${result.value.slice(0, 120)}`,
|
|
119
|
+
"github"
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async addLabels(issueNumber, labels) {
|
|
125
|
+
const args = ["issue", "edit", String(issueNumber), "--add-label", labels.join(",")];
|
|
126
|
+
logger.debug("Adding labels", { repo: this.repo, issueNumber, labels });
|
|
127
|
+
const result = await execGh(args, this.repo);
|
|
128
|
+
if (!result.ok) return result;
|
|
129
|
+
return ok(void 0);
|
|
130
|
+
}
|
|
131
|
+
async createPR(options) {
|
|
132
|
+
const fields = "number,title,body,url,author,baseRefName,headRefName";
|
|
133
|
+
const args = [
|
|
134
|
+
"pr",
|
|
135
|
+
"create",
|
|
136
|
+
"--title",
|
|
137
|
+
options.title,
|
|
138
|
+
"--body",
|
|
139
|
+
options.body,
|
|
140
|
+
"--head",
|
|
141
|
+
options.head,
|
|
142
|
+
"--base",
|
|
143
|
+
options.base,
|
|
144
|
+
"--json",
|
|
145
|
+
fields
|
|
146
|
+
];
|
|
147
|
+
logger.info("Creating PR", { repo: this.repo, title: options.title });
|
|
148
|
+
const result = await execGh(args, this.repo);
|
|
149
|
+
if (!result.ok) return result;
|
|
150
|
+
try {
|
|
151
|
+
const raw = JSON.parse(result.value);
|
|
152
|
+
return ok({
|
|
153
|
+
number: raw.number,
|
|
154
|
+
title: raw.title,
|
|
155
|
+
body: raw.body ?? "",
|
|
156
|
+
author: raw.author.login,
|
|
157
|
+
base: raw.baseRefName,
|
|
158
|
+
head: raw.headRefName,
|
|
159
|
+
url: raw.url
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return err(
|
|
163
|
+
new ScmError(
|
|
164
|
+
`Failed to parse PR JSON: ${getErrorMessage(error)} \u2014 preview: ${result.value.slice(0, 120)}`,
|
|
165
|
+
"github"
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async mergePR(prNumber, options) {
|
|
171
|
+
const method = options?.method ?? "squash";
|
|
172
|
+
const args = ["pr", "merge", String(prNumber), `--${method}`];
|
|
173
|
+
if (options?.commitTitle !== void 0) args.push("--subject", options.commitTitle);
|
|
174
|
+
if (options?.commitMessage !== void 0) args.push("--body", options.commitMessage);
|
|
175
|
+
if (options?.deleteBranch === true) args.push("--delete-branch");
|
|
176
|
+
logger.info("Merging PR", { repo: this.repo, prNumber, method });
|
|
177
|
+
const result = await execGh(args, this.repo);
|
|
178
|
+
if (!result.ok) return result;
|
|
179
|
+
return ok(void 0);
|
|
180
|
+
}
|
|
181
|
+
async getPRStatus(prNumber) {
|
|
182
|
+
const fields = "mergeable,statusCheckRollup,reviewDecision";
|
|
183
|
+
const args = ["pr", "view", String(prNumber), "--json", fields];
|
|
184
|
+
logger.debug("Getting PR status", { repo: this.repo, prNumber });
|
|
185
|
+
const result = await execGh(args, this.repo);
|
|
186
|
+
if (!result.ok) return result;
|
|
187
|
+
try {
|
|
188
|
+
return ok(mapPRStatus(JSON.parse(result.value)));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return err(
|
|
191
|
+
new ScmError(
|
|
192
|
+
`Failed to parse PR status JSON: ${getErrorMessage(error)} \u2014 preview: ${result.value.slice(0, 120)}`,
|
|
193
|
+
"github"
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async createIssue(title, body, labels) {
|
|
199
|
+
const args = ["issue", "create", "--title", title, "--body", body];
|
|
200
|
+
if (labels !== void 0 && labels.length > 0) args.push("--label", labels.join(","));
|
|
201
|
+
logger.debug("Creating issue", { repo: this.repo, title });
|
|
202
|
+
const result = await execGh(args, this.repo);
|
|
203
|
+
if (!result.ok) return result;
|
|
204
|
+
const url = result.value.trim();
|
|
205
|
+
const match = /\/(\d+)$/.exec(url);
|
|
206
|
+
const number = match?.[1] !== void 0 ? parseInt(match[1], 10) : 0;
|
|
207
|
+
return ok({
|
|
208
|
+
number,
|
|
209
|
+
title,
|
|
210
|
+
body,
|
|
211
|
+
labels: labels !== void 0 ? [...labels] : [],
|
|
212
|
+
author: "pipeline",
|
|
213
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async addComment(issueNumber, body) {
|
|
217
|
+
const args = ["issue", "comment", String(issueNumber), "--body", body];
|
|
218
|
+
logger.debug("Adding comment", { repo: this.repo, issueNumber });
|
|
219
|
+
const result = await execGh(args, this.repo);
|
|
220
|
+
if (!result.ok) return result;
|
|
221
|
+
return ok(void 0);
|
|
222
|
+
}
|
|
223
|
+
async listComments(issueNumber) {
|
|
224
|
+
const args = ["issue", "view", String(issueNumber), "--json", "comments", "--jq", ".comments"];
|
|
225
|
+
logger.debug("Listing comments", { repo: this.repo, issueNumber });
|
|
226
|
+
const result = await execGh(args, this.repo);
|
|
227
|
+
if (!result.ok) return result;
|
|
228
|
+
try {
|
|
229
|
+
const comments = JSON.parse(result.value);
|
|
230
|
+
return ok(comments.map(mapComment));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return err(
|
|
233
|
+
new ScmError(
|
|
234
|
+
`Failed to parse comments JSON: ${getErrorMessage(error)} \u2014 preview: ${result.value.slice(0, 120)}`,
|
|
235
|
+
"github"
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
ScmError,
|
|
244
|
+
GitHubProvider
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=chunk-4HA5PAL7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scm/types.ts","../src/scm/github-provider.ts"],"sourcesContent":["/**\n * nexus-agents/scm - SCM Provider Types\n *\n * Shared types for the centralized SCM (Source Control Management) module.\n * Supports GitHub (REST API + gh CLI) with extensibility for GitLab/Gitea.\n *\n * @module scm/types\n * (Source: Issue #1136 — Centralized SCM Provider Module)\n */\n\nimport type { Result } from '../core/index.js';\n\n// ============================================================================\n// Token Types\n// ============================================================================\n\n/** Supported SCM platforms. */\nexport type ScmPlatform = 'github' | 'gitlab' | 'gitea';\n\n/** Token resolution strategy. */\nexport type TokenStrategy = 'env' | 'cli' | 'config';\n\n/** Resolved SCM token with metadata. */\nexport interface ScmToken {\n /** The raw token value */\n readonly value: string;\n /** How the token was resolved */\n readonly strategy: TokenStrategy;\n /** SCM platform this token is for */\n readonly platform: ScmPlatform;\n}\n\n/** Token resolution configuration. */\nexport interface TokenResolverConfig {\n /** Explicit token (highest priority) */\n readonly token?: string;\n /** SCM platform to resolve for */\n readonly platform?: ScmPlatform;\n /** Custom env var name override */\n readonly envVar?: string;\n}\n\n// ============================================================================\n// SCM Entity Types\n// ============================================================================\n\n/** SCM issue representation. */\nexport interface ScmIssue {\n readonly number: number;\n readonly title: string;\n readonly body: string;\n readonly labels: readonly string[];\n readonly author: string;\n readonly createdAt: string;\n}\n\n/** SCM pull/merge request representation. */\nexport interface ScmPullRequest {\n readonly number: number;\n readonly title: string;\n readonly body: string;\n readonly author: string;\n readonly base: string;\n readonly head: string;\n readonly url: string;\n}\n\n/** SCM comment representation. */\nexport interface ScmComment {\n readonly id: number;\n readonly body: string;\n readonly author: string;\n readonly createdAt: string;\n}\n\n/** PR creation options. */\nexport interface CreatePROptions {\n readonly title: string;\n readonly body: string;\n readonly head: string;\n readonly base: string;\n}\n\n/** PR merge options. */\nexport interface MergePROptions {\n readonly method?: 'merge' | 'squash' | 'rebase';\n readonly commitTitle?: string;\n readonly commitMessage?: string;\n readonly deleteBranch?: boolean;\n}\n\n/** PR status for merge eligibility. */\nexport interface PRStatus {\n readonly mergeable: boolean;\n readonly checksStatus: 'pending' | 'success' | 'failure';\n readonly reviewStatus: 'approved' | 'pending' | 'changes_requested';\n}\n\n/** Issue filter options. */\nexport interface IssueFilters {\n readonly labels?: readonly string[];\n readonly state?: 'open' | 'closed' | 'all';\n readonly limit?: number;\n}\n\n// ============================================================================\n// SCM Error\n// ============================================================================\n\n/** Unified SCM error with platform-aware context. */\nexport class ScmError extends Error {\n constructor(\n message: string,\n readonly platform: ScmPlatform,\n readonly statusCode?: number,\n readonly context?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'ScmError';\n }\n}\n\n// ============================================================================\n// Extended Entity Types (Trait support)\n// ============================================================================\n\n/** File change in a pull request. */\nexport interface ScmFileChange {\n readonly filename: string;\n readonly status: 'added' | 'removed' | 'modified' | 'renamed' | 'copied';\n readonly additions: number;\n readonly deletions: number;\n readonly patch?: string;\n readonly previousFilename?: string;\n}\n\n/** Extended PR with file diffs and stats. Used by IScmReviewer. */\nexport interface ScmPullRequestDetail extends ScmPullRequest {\n readonly draft: boolean;\n readonly authorAssociation: string;\n readonly labels: readonly string[];\n readonly files: readonly ScmFileChange[];\n readonly additions: number;\n readonly deletions: number;\n readonly headSha: string;\n}\n\n/** Extended issue with association and state. Used by IScmReviewer. */\nexport interface ScmIssueDetail extends ScmIssue {\n readonly authorAssociation: string;\n readonly state: string;\n readonly url: string;\n}\n\n/** Extended comment with author association. */\nexport interface ScmCommentDetail extends ScmComment {\n readonly authorAssociation: string;\n}\n\n/** Review decision for a pull request. */\nexport type ScmReviewDecision = 'approve' | 'request_changes' | 'comment';\n\n/** User metadata for reputation assessment. */\nexport interface ScmUserMetadata {\n readonly login: string;\n readonly name: string | null;\n readonly company: string | null;\n readonly followers: number;\n readonly following: number;\n readonly publicRepos: number;\n readonly createdAt: string;\n}\n\n// ============================================================================\n// Provider Interface (Core)\n// ============================================================================\n\n/**\n * Core SCM provider interface.\n *\n * All methods return `Result<T, ScmError>` for consistent error handling\n * across GitHub REST API, gh CLI, and future GitLab/Gitea backends.\n */\nexport interface IScmProvider {\n /** Platform identifier. */\n readonly platform: ScmPlatform;\n\n /** Repository in owner/repo format. */\n readonly repo: string;\n\n // Issues\n getIssue(number: number): Promise<Result<ScmIssue, ScmError>>;\n listIssues(filters?: IssueFilters): Promise<Result<readonly ScmIssue[], ScmError>>;\n createIssue(\n title: string,\n body: string,\n labels?: readonly string[]\n ): Promise<Result<ScmIssue, ScmError>>;\n addLabels(issueNumber: number, labels: readonly string[]): Promise<Result<void, ScmError>>;\n\n // Pull Requests\n createPR(options: CreatePROptions): Promise<Result<ScmPullRequest, ScmError>>;\n mergePR(prNumber: number, options?: MergePROptions): Promise<Result<void, ScmError>>;\n getPRStatus(prNumber: number): Promise<Result<PRStatus, ScmError>>;\n\n // Comments\n addComment(issueNumber: number, body: string): Promise<Result<void, ScmError>>;\n listComments(issueNumber: number): Promise<Result<readonly ScmComment[], ScmError>>;\n}\n\n// ============================================================================\n// Trait Interfaces (ISP — Interface Segregation Principle)\n// ============================================================================\n\n/**\n * Review trait — PR review capabilities.\n *\n * Implemented by platforms supporting code review workflows.\n * Consumers declare this trait when they need PR file diffs or review posting.\n */\nexport interface IScmReviewer {\n /** Fetch PR with full file diffs and stats. */\n getPullRequestDetail(prNumber: number): Promise<Result<ScmPullRequestDetail, ScmError>>;\n\n /** Post a review on a pull request. */\n createReview(\n prNumber: number,\n body: string,\n decision: ScmReviewDecision\n ): Promise<Result<void, ScmError>>;\n\n /** Fetch issue with author association and state. */\n getIssueDetail(issueNumber: number): Promise<Result<ScmIssueDetail, ScmError>>;\n\n /** List comments with author associations. */\n listCommentDetails(issueNumber: number): Promise<Result<readonly ScmCommentDetail[], ScmError>>;\n}\n\n/**\n * User info trait — user metadata for reputation assessment.\n *\n * Implemented by platforms supporting user profile queries.\n * Consumers declare this trait when they need author reputation data.\n */\nexport interface IScmUserInfo {\n /** Fetch user metadata for reputation assessment. */\n fetchUserMetadata(username: string): Promise<Result<ScmUserMetadata, ScmError>>;\n}\n\n/**\n * Convenience type: provider with review capabilities.\n * Used by PR review workflows.\n */\nexport type ReviewCapableProvider = IScmProvider & IScmReviewer;\n\n/**\n * Convenience type: provider with all capabilities.\n * Used by full triage workflows that need review + user info.\n */\nexport type FullCapableProvider = IScmProvider & IScmReviewer & IScmUserInfo;\n","/**\n * nexus-agents/scm - GitHub Provider\n *\n * Unified GitHub provider using gh CLI. Implements IScmProvider with\n * Result-based error handling. Consolidates the two previous GitHub\n * clients (dogfooding/github-client.ts and workflows/self-development/github-client.ts).\n *\n * @module scm/github-provider\n * (Source: Issue #1136 — Centralized SCM Provider Module)\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { Result } from '../core/index.js';\nimport { ok, err, createLogger, getErrorMessage } from '../core/index.js';\nimport type {\n IScmProvider,\n ScmIssue,\n ScmPullRequest,\n ScmComment,\n CreatePROptions,\n MergePROptions,\n PRStatus,\n IssueFilters,\n} from './types.js';\nimport { ScmError } from './types.js';\n\nconst execFileAsync = promisify(execFile);\nconst logger = createLogger({ component: 'GitHubProvider' });\n\n/** Max buffer for gh CLI output (10MB). */\nconst MAX_BUFFER = 10 * 1024 * 1024;\n\n/** gh CLI timeout in ms. */\nconst GH_TIMEOUT_MS = 30_000;\n\n// ============================================================================\n// gh CLI JSON types (internal)\n// ============================================================================\n\ninterface GhIssueJson {\n number: number;\n title: string;\n body: string | null;\n labels: Array<{ name: string }>;\n author: { login: string };\n createdAt: string;\n}\n\ninterface GhCommentJson {\n id: number;\n body: string;\n author: { login: string };\n createdAt: string;\n}\n\ninterface GhPrJson {\n number: number;\n title: string;\n body: string | null;\n url: string;\n author: { login: string };\n baseRefName: string;\n headRefName: string;\n}\n\ninterface GhPrStatusJson {\n mergeable: string;\n statusCheckRollup: Array<{ state: string }> | null;\n reviewDecision: string | null;\n}\n\n// ============================================================================\n// gh CLI executor\n// ============================================================================\n\nasync function execGh(args: readonly string[], repo: string): Promise<Result<string, ScmError>> {\n const fullArgs = [...args, '--repo', repo];\n\n try {\n const { stdout } = await execFileAsync('gh', fullArgs, {\n maxBuffer: MAX_BUFFER,\n timeout: GH_TIMEOUT_MS,\n });\n return ok(stdout.trim());\n } catch (error) {\n const execError = error as { message: string; stderr?: string };\n return err(\n new ScmError(`gh command failed: ${execError.message}`, 'github', undefined, {\n command: `gh ${fullArgs.join(' ')}`,\n stderr: execError.stderr,\n })\n );\n }\n}\n\n// ============================================================================\n// Mappers\n// ============================================================================\n\nfunction mapIssue(raw: GhIssueJson): ScmIssue {\n return {\n number: raw.number,\n title: raw.title,\n body: raw.body ?? '',\n labels: raw.labels.map((l) => l.name),\n author: raw.author.login,\n createdAt: raw.createdAt,\n };\n}\n\nfunction mapComment(raw: GhCommentJson): ScmComment {\n return {\n id: raw.id,\n body: raw.body,\n author: raw.author.login,\n createdAt: raw.createdAt,\n };\n}\n\nfunction mapPRStatus(raw: GhPrStatusJson): PRStatus {\n const mergeable = raw.mergeable === 'MERGEABLE';\n\n let checksStatus: 'pending' | 'success' | 'failure' = 'pending';\n if (raw.statusCheckRollup !== null && raw.statusCheckRollup.length > 0) {\n const hasFailure = raw.statusCheckRollup.some((c) => c.state === 'FAILURE');\n const allSuccess = raw.statusCheckRollup.every(\n (c) => c.state === 'SUCCESS' || c.state === 'NEUTRAL' || c.state === 'SKIPPED'\n );\n checksStatus = hasFailure ? 'failure' : allSuccess ? 'success' : 'pending';\n }\n\n let reviewStatus: 'approved' | 'pending' | 'changes_requested' = 'pending';\n if (raw.reviewDecision === 'APPROVED') reviewStatus = 'approved';\n else if (raw.reviewDecision === 'CHANGES_REQUESTED') reviewStatus = 'changes_requested';\n\n return { mergeable, checksStatus, reviewStatus };\n}\n\n// ============================================================================\n// Provider Implementation\n// ============================================================================\n\n/**\n * GitHub provider using the gh CLI.\n *\n * Requires: gh CLI installed and authenticated.\n */\nexport class GitHubProvider implements IScmProvider {\n readonly platform = 'github' as const;\n\n constructor(readonly repo: string) {}\n\n async getIssue(number: number): Promise<Result<ScmIssue, ScmError>> {\n const fields = 'number,title,body,labels,author,createdAt';\n const args = ['issue', 'view', String(number), '--json', fields];\n\n logger.debug('Getting issue', { repo: this.repo, number });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n\n try {\n return ok(mapIssue(JSON.parse(result.value) as GhIssueJson));\n } catch (error) {\n return err(\n new ScmError(\n `Failed to parse issue JSON: ${getErrorMessage(error)} — preview: ${result.value.slice(0, 120)}`,\n 'github'\n )\n );\n }\n }\n\n async listIssues(filters?: IssueFilters): Promise<Result<readonly ScmIssue[], ScmError>> {\n const fields = 'number,title,body,labels,author,createdAt';\n const args = ['issue', 'list', '--json', fields];\n\n if (filters?.labels !== undefined && filters.labels.length > 0) {\n args.push('--label', filters.labels.join(','));\n }\n if (filters?.state !== undefined) {\n args.push('--state', filters.state);\n }\n args.push('--limit', String(filters?.limit ?? 50));\n\n logger.debug('Listing issues', { repo: this.repo, filters });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n\n try {\n const issues = JSON.parse(result.value) as GhIssueJson[];\n return ok(issues.map(mapIssue));\n } catch (error) {\n return err(\n new ScmError(\n `Failed to parse issues JSON: ${getErrorMessage(error)} — preview: ${result.value.slice(0, 120)}`,\n 'github'\n )\n );\n }\n }\n\n async addLabels(issueNumber: number, labels: readonly string[]): Promise<Result<void, ScmError>> {\n const args = ['issue', 'edit', String(issueNumber), '--add-label', labels.join(',')];\n\n logger.debug('Adding labels', { repo: this.repo, issueNumber, labels });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n return ok(undefined);\n }\n\n async createPR(options: CreatePROptions): Promise<Result<ScmPullRequest, ScmError>> {\n const fields = 'number,title,body,url,author,baseRefName,headRefName';\n const args = [\n 'pr',\n 'create',\n '--title',\n options.title,\n '--body',\n options.body,\n '--head',\n options.head,\n '--base',\n options.base,\n '--json',\n fields,\n ];\n\n logger.info('Creating PR', { repo: this.repo, title: options.title });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n\n try {\n const raw = JSON.parse(result.value) as GhPrJson;\n return ok({\n number: raw.number,\n title: raw.title,\n body: raw.body ?? '',\n author: raw.author.login,\n base: raw.baseRefName,\n head: raw.headRefName,\n url: raw.url,\n });\n } catch (error) {\n return err(\n new ScmError(\n `Failed to parse PR JSON: ${getErrorMessage(error)} — preview: ${result.value.slice(0, 120)}`,\n 'github'\n )\n );\n }\n }\n\n async mergePR(prNumber: number, options?: MergePROptions): Promise<Result<void, ScmError>> {\n const method = options?.method ?? 'squash';\n const args = ['pr', 'merge', String(prNumber), `--${method}`];\n\n if (options?.commitTitle !== undefined) args.push('--subject', options.commitTitle);\n if (options?.commitMessage !== undefined) args.push('--body', options.commitMessage);\n if (options?.deleteBranch === true) args.push('--delete-branch');\n\n logger.info('Merging PR', { repo: this.repo, prNumber, method });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n return ok(undefined);\n }\n\n async getPRStatus(prNumber: number): Promise<Result<PRStatus, ScmError>> {\n const fields = 'mergeable,statusCheckRollup,reviewDecision';\n const args = ['pr', 'view', String(prNumber), '--json', fields];\n\n logger.debug('Getting PR status', { repo: this.repo, prNumber });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n\n try {\n return ok(mapPRStatus(JSON.parse(result.value) as GhPrStatusJson));\n } catch (error) {\n return err(\n new ScmError(\n `Failed to parse PR status JSON: ${getErrorMessage(error)} — preview: ${result.value.slice(0, 120)}`,\n 'github'\n )\n );\n }\n }\n\n async createIssue(\n title: string,\n body: string,\n labels?: readonly string[]\n ): Promise<Result<ScmIssue, ScmError>> {\n const args = ['issue', 'create', '--title', title, '--body', body];\n if (labels !== undefined && labels.length > 0) args.push('--label', labels.join(','));\n logger.debug('Creating issue', { repo: this.repo, title });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n const url = result.value.trim();\n const match = /\\/(\\d+)$/.exec(url);\n const number = match?.[1] !== undefined ? parseInt(match[1], 10) : 0;\n return ok({\n number,\n title,\n body,\n labels: labels !== undefined ? [...labels] : [],\n author: 'pipeline',\n createdAt: new Date().toISOString(),\n });\n }\n\n async addComment(issueNumber: number, body: string): Promise<Result<void, ScmError>> {\n const args = ['issue', 'comment', String(issueNumber), '--body', body];\n\n logger.debug('Adding comment', { repo: this.repo, issueNumber });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n return ok(undefined);\n }\n\n async listComments(issueNumber: number): Promise<Result<readonly ScmComment[], ScmError>> {\n const args = ['issue', 'view', String(issueNumber), '--json', 'comments', '--jq', '.comments'];\n\n logger.debug('Listing comments', { repo: this.repo, issueNumber });\n const result = await execGh(args, this.repo);\n if (!result.ok) return result;\n\n try {\n const comments = JSON.parse(result.value) as GhCommentJson[];\n return ok(comments.map(mapComment));\n } catch (error) {\n return err(\n new ScmError(\n `Failed to parse comments JSON: ${getErrorMessage(error)} — preview: ${result.value.slice(0, 120)}`,\n 'github'\n )\n );\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/utils/text-utils.ts
|
|
2
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3
|
+
"a",
|
|
4
|
+
"an",
|
|
5
|
+
"and",
|
|
6
|
+
"are",
|
|
7
|
+
"as",
|
|
8
|
+
"at",
|
|
9
|
+
"be",
|
|
10
|
+
"by",
|
|
11
|
+
"for",
|
|
12
|
+
"from",
|
|
13
|
+
"has",
|
|
14
|
+
"he",
|
|
15
|
+
"in",
|
|
16
|
+
"is",
|
|
17
|
+
"it",
|
|
18
|
+
"its",
|
|
19
|
+
"of",
|
|
20
|
+
"on",
|
|
21
|
+
"or",
|
|
22
|
+
"that",
|
|
23
|
+
"the",
|
|
24
|
+
"to",
|
|
25
|
+
"was",
|
|
26
|
+
"were",
|
|
27
|
+
"will",
|
|
28
|
+
"with",
|
|
29
|
+
"this",
|
|
30
|
+
"but",
|
|
31
|
+
"they",
|
|
32
|
+
"have",
|
|
33
|
+
"had",
|
|
34
|
+
"what",
|
|
35
|
+
"when",
|
|
36
|
+
"where",
|
|
37
|
+
"who",
|
|
38
|
+
"which",
|
|
39
|
+
"why",
|
|
40
|
+
"how",
|
|
41
|
+
"all",
|
|
42
|
+
"each",
|
|
43
|
+
"every",
|
|
44
|
+
"both",
|
|
45
|
+
"few",
|
|
46
|
+
"more",
|
|
47
|
+
"most",
|
|
48
|
+
"other",
|
|
49
|
+
"some",
|
|
50
|
+
"such",
|
|
51
|
+
"no",
|
|
52
|
+
"nor",
|
|
53
|
+
"not",
|
|
54
|
+
"only",
|
|
55
|
+
"own",
|
|
56
|
+
"same",
|
|
57
|
+
"so",
|
|
58
|
+
"than",
|
|
59
|
+
"too",
|
|
60
|
+
"very",
|
|
61
|
+
"can",
|
|
62
|
+
"just",
|
|
63
|
+
"should",
|
|
64
|
+
"now",
|
|
65
|
+
"i",
|
|
66
|
+
"you",
|
|
67
|
+
"we",
|
|
68
|
+
"me",
|
|
69
|
+
"my",
|
|
70
|
+
"your",
|
|
71
|
+
"our",
|
|
72
|
+
"their",
|
|
73
|
+
"him",
|
|
74
|
+
"her",
|
|
75
|
+
"them",
|
|
76
|
+
"his",
|
|
77
|
+
"hers",
|
|
78
|
+
"able"
|
|
79
|
+
]);
|
|
80
|
+
function tokenize(text, minLength = 2) {
|
|
81
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= minLength);
|
|
82
|
+
}
|
|
83
|
+
function tokenizeFiltered(text, minLength = 2) {
|
|
84
|
+
return tokenize(text, minLength).filter((t) => !STOPWORDS.has(t));
|
|
85
|
+
}
|
|
86
|
+
function stringifyValue(value) {
|
|
87
|
+
if (typeof value === "string") return value;
|
|
88
|
+
if (value === null || value === void 0) return "";
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
}
|
|
91
|
+
function capitalize(str) {
|
|
92
|
+
if (str.length === 0) return str;
|
|
93
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
94
|
+
}
|
|
95
|
+
function capitalizeWords(str) {
|
|
96
|
+
return str.split(" ").map((word) => capitalize(word)).join(" ");
|
|
97
|
+
}
|
|
98
|
+
function capitalizeKebab(str) {
|
|
99
|
+
return str.split("-").map((word) => capitalize(word)).join(" ");
|
|
100
|
+
}
|
|
101
|
+
function truncateWithInfo(text, maxLength) {
|
|
102
|
+
if (text.length <= maxLength) return text;
|
|
103
|
+
const truncatedBytes = text.length - maxLength;
|
|
104
|
+
return `${text.slice(0, maxLength)}
|
|
105
|
+
... [truncated ${String(truncatedBytes)} bytes]`;
|
|
106
|
+
}
|
|
107
|
+
function truncateSentence(text, maxLength = 150) {
|
|
108
|
+
const sentenceEnd = text.search(/[.!?](?:\s|$)/);
|
|
109
|
+
if (sentenceEnd !== -1 && sentenceEnd < maxLength) {
|
|
110
|
+
return text.slice(0, sentenceEnd + 1).trim();
|
|
111
|
+
}
|
|
112
|
+
if (text.length <= maxLength) return text;
|
|
113
|
+
return text.slice(0, maxLength).trim() + "...";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
STOPWORDS,
|
|
118
|
+
tokenize,
|
|
119
|
+
tokenizeFiltered,
|
|
120
|
+
stringifyValue,
|
|
121
|
+
capitalize,
|
|
122
|
+
capitalizeWords,
|
|
123
|
+
capitalizeKebab,
|
|
124
|
+
truncateWithInfo,
|
|
125
|
+
truncateSentence
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=chunk-633WH2ML.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/text-utils.ts"],"sourcesContent":["/**\n * nexus-agents/utils - Text Processing Utilities\n *\n * Shared utility functions for text tokenization and processing.\n * Consolidates duplicate code from multiple modules per ADR-0013.\n *\n * Used by:\n * - context/agentic-memory-extraction.ts\n * - context/adaptive-memory-helpers.ts\n * - cli-adapters/daao-feature-extraction.ts\n * - cli-adapters/agreement-cascade-helpers.ts\n *\n * @module utils/text-utils\n * @see docs/adr/0013-memory-helpers-consolidation.md\n */\n\n// ============================================================================\n// Stopwords\n// ============================================================================\n\n/**\n * Common English stopwords to filter from keyword extraction.\n */\nexport const STOPWORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'are',\n 'as',\n 'at',\n 'be',\n 'by',\n 'for',\n 'from',\n 'has',\n 'he',\n 'in',\n 'is',\n 'it',\n 'its',\n 'of',\n 'on',\n 'or',\n 'that',\n 'the',\n 'to',\n 'was',\n 'were',\n 'will',\n 'with',\n 'this',\n 'but',\n 'they',\n 'have',\n 'had',\n 'what',\n 'when',\n 'where',\n 'who',\n 'which',\n 'why',\n 'how',\n 'all',\n 'each',\n 'every',\n 'both',\n 'few',\n 'more',\n 'most',\n 'other',\n 'some',\n 'such',\n 'no',\n 'nor',\n 'not',\n 'only',\n 'own',\n 'same',\n 'so',\n 'than',\n 'too',\n 'very',\n 'can',\n 'just',\n 'should',\n 'now',\n 'i',\n 'you',\n 'we',\n 'me',\n 'my',\n 'your',\n 'our',\n 'their',\n 'him',\n 'her',\n 'them',\n 'his',\n 'hers',\n 'able',\n]);\n\n// ============================================================================\n// Tokenization\n// ============================================================================\n\n/**\n * Tokenize text into normalized words.\n *\n * Process:\n * 1. Convert to lowercase\n * 2. Replace non-alphanumeric characters with spaces\n * 3. Split on whitespace\n * 4. Filter tokens by minimum length\n *\n * @param text - Input text to tokenize\n * @param minLength - Minimum token length (default: 2)\n * @returns Array of normalized tokens\n */\nexport function tokenize(text: string, minLength = 2): string[] {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .split(/\\s+/)\n .filter((t) => t.length >= minLength);\n}\n\n/**\n * Tokenize text and return as a Set for fast lookups.\n *\n * @param text - Input text to tokenize\n * @param minLength - Minimum token length (default: 2)\n * @returns Set of normalized tokens\n */\nexport function tokenizeToSet(text: string, minLength = 2): Set<string> {\n return new Set(tokenize(text, minLength));\n}\n\n/**\n * Tokenize text with stopword filtering.\n *\n * @param text - Input text to tokenize\n * @param minLength - Minimum token length (default: 2)\n * @returns Array of tokens with stopwords removed\n */\nexport function tokenizeFiltered(text: string, minLength = 2): string[] {\n return tokenize(text, minLength).filter((t) => !STOPWORDS.has(t));\n}\n\n// ============================================================================\n// Value Stringification\n// ============================================================================\n\n/**\n * Convert a value to string for text processing.\n *\n * @param value - Value to stringify\n * @returns String representation\n */\nexport function stringifyValue(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value === null || value === undefined) return '';\n return JSON.stringify(value);\n}\n\n// ============================================================================\n// String Capitalization\n// ============================================================================\n\n/**\n * Capitalize the first character of a string.\n *\n * @param str - Input string\n * @returns String with first character uppercased\n * @example capitalize('hello') // 'Hello'\n */\nexport function capitalize(str: string): string {\n if (str.length === 0) return str;\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n/**\n * Capitalize the first character of each word in a string.\n *\n * @param str - Input string\n * @returns String with each word capitalized\n * @example capitalizeWords('hello world') // 'Hello World'\n */\nexport function capitalizeWords(str: string): string {\n return str\n .split(' ')\n .map((word) => capitalize(word))\n .join(' ');\n}\n\n/**\n * Convert a kebab-case string to Title Case.\n *\n * @param str - Kebab-case string\n * @returns Title Case string with hyphens replaced by spaces\n * @example capitalizeKebab('hello-world') // 'Hello World'\n */\nexport function capitalizeKebab(str: string): string {\n return str\n .split('-')\n .map((word) => capitalize(word))\n .join(' ');\n}\n\n// ============================================================================\n// Text Truncation\n// ============================================================================\n\n/**\n * Truncate text to a maximum length with a suffix.\n *\n * @param text - Input text\n * @param maxLength - Maximum length including suffix\n * @param suffix - Suffix to append when truncated (default: '...')\n * @returns Truncated text with suffix if needed\n * @example truncateText('hello world', 8) // 'hello...'\n */\nexport function truncateText(text: string, maxLength: number, suffix = '...'): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength - suffix.length) + suffix;\n}\n\n/**\n * Truncate text and include byte count information.\n * Commonly used for output truncation in CLI tools.\n *\n * @param text - Input text\n * @param maxLength - Maximum length before truncation marker\n * @returns Truncated text with byte count info\n * @example truncateWithInfo('hello world...', 5) // 'hello\\n... [truncated 9 bytes]'\n */\nexport function truncateWithInfo(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n const truncatedBytes = text.length - maxLength;\n return `${text.slice(0, maxLength)}\\n... [truncated ${String(truncatedBytes)} bytes]`;\n}\n\n/**\n * Truncate to the first sentence or a maximum length.\n * Useful for extracting descriptions from longer text.\n *\n * @param text - Input text\n * @param maxLength - Maximum length (default: 150)\n * @returns First sentence or truncated text\n * @example truncateSentence('Hello world. More text.', 150) // 'Hello world.'\n */\nexport function truncateSentence(text: string, maxLength = 150): string {\n // Find first sentence ending\n const sentenceEnd = text.search(/[.!?](?:\\s|$)/);\n if (sentenceEnd !== -1 && sentenceEnd < maxLength) {\n return text.slice(0, sentenceEnd + 1).trim();\n }\n // Fall back to truncation\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength).trim() + '...';\n}\n\n// ============================================================================\n// Line Splitting\n// ============================================================================\n\n/**\n * Split text into lines.\n * Handles both Unix (\\n) and Windows (\\r\\n) line endings.\n *\n * @param text - Input text to split\n * @returns Array of lines (may include empty strings)\n * @example splitLines('a\\nb\\nc') // ['a', 'b', 'c']\n * @example splitLines('a\\n\\nb') // ['a', '', 'b']\n */\nexport function splitLines(text: string): string[] {\n return text.split(/\\r?\\n/);\n}\n\n/**\n * Split text into non-empty lines.\n * Filters out empty lines and whitespace-only lines.\n *\n * @param text - Input text to split\n * @returns Array of non-empty lines\n * @example splitNonEmptyLines('a\\n\\nb\\n \\nc') // ['a', 'b', 'c']\n */\nexport function splitNonEmptyLines(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n}\n\n/**\n * Split text into trimmed lines, preserving empty lines.\n * Trims each line but keeps empty lines in the output.\n *\n * @param text - Input text to split\n * @returns Array of trimmed lines\n * @example splitTrimmedLines(' a \\n\\n b ') // ['a', '', 'b']\n */\nexport function splitTrimmedLines(text: string): string[] {\n return text.split(/\\r?\\n/).map((line) => line.trim());\n}\n\n// ============================================================================\n// Sentence/Word Splitting\n// ============================================================================\n\n/**\n * Count the number of sentences in text.\n * Counts sentence-ending punctuation followed by space or end of string.\n *\n * @param text - Input text\n * @returns Number of sentences\n * @example countSentences('Hello world. How are you?') // 2\n */\nexport function countSentences(text: string): number {\n const matches = text.match(/[.!?]+(?:\\s|$)/g);\n return matches !== null ? matches.length : 0;\n}\n\n/**\n * Split text into sentences.\n * Splits on sentence-ending punctuation followed by space.\n *\n * @param text - Input text\n * @returns Array of sentences (trimmed)\n * @example splitSentences('Hello world. How are you?') // ['Hello world.', 'How are you?']\n */\nexport function splitSentences(text: string): string[] {\n // Split on sentence endings followed by space, keeping the punctuation\n return text\n .split(/(?<=[.!?])\\s+/)\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Count the number of words in text.\n * Splits on whitespace and counts non-empty tokens.\n *\n * @param text - Input text\n * @returns Number of words\n * @example countWords('Hello world') // 2\n */\nexport function countWords(text: string): number {\n return text\n .trim()\n .split(/\\s+/)\n .filter((w) => w.length > 0).length;\n}\n"],"mappings":";AAuBO,IAAM,YAAY,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmBM,SAAS,SAAS,MAAc,YAAY,GAAa;AAC9D,SAAO,KACJ,YAAY,EACZ,QAAQ,gBAAgB,GAAG,EAC3B,MAAM,KAAK,EACX,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS;AACxC;AAoBO,SAAS,iBAAiB,MAAc,YAAY,GAAa;AACtE,SAAO,SAAS,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;AAClE;AAYO,SAAS,eAAe,OAAwB;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAO,KAAK,UAAU,KAAK;AAC7B;AAaO,SAAS,WAAW,KAAqB;AAC9C,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,SAAO,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC;AAClD;AASO,SAAS,gBAAgB,KAAqB;AACnD,SAAO,IACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,EAC9B,KAAK,GAAG;AACb;AASO,SAAS,gBAAgB,KAAqB;AACnD,SAAO,IACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,EAC9B,KAAK,GAAG;AACb;AA6BO,SAAS,iBAAiB,MAAc,WAA2B;AACxE,MAAI,KAAK,UAAU,UAAW,QAAO;AACrC,QAAM,iBAAiB,KAAK,SAAS;AACrC,SAAO,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;AAAA,iBAAoB,OAAO,cAAc,CAAC;AAC9E;AAWO,SAAS,iBAAiB,MAAc,YAAY,KAAa;AAEtE,QAAM,cAAc,KAAK,OAAO,eAAe;AAC/C,MAAI,gBAAgB,MAAM,cAAc,WAAW;AACjD,WAAO,KAAK,MAAM,GAAG,cAAc,CAAC,EAAE,KAAK;AAAA,EAC7C;AAEA,MAAI,KAAK,UAAU,UAAW,QAAO;AACrC,SAAO,KAAK,MAAM,GAAG,SAAS,EAAE,KAAK,IAAI;AAC3C;","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger,
|
|
3
|
+
err,
|
|
4
|
+
ok
|
|
5
|
+
} from "./chunk-ELIFTCYM.js";
|
|
6
|
+
|
|
7
|
+
// src/scm/token-resolver.ts
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
var execFileAsync = promisify(execFile);
|
|
11
|
+
var logger = createLogger({ component: "TokenResolver" });
|
|
12
|
+
var ENV_VARS = {
|
|
13
|
+
github: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
14
|
+
gitlab: ["GITLAB_TOKEN", "GL_TOKEN"],
|
|
15
|
+
gitea: ["GITEA_TOKEN"]
|
|
16
|
+
};
|
|
17
|
+
var CLI_AUTH_COMMANDS = {
|
|
18
|
+
github: ["gh", "auth", "token"],
|
|
19
|
+
gitlab: ["glab", "auth", "token"],
|
|
20
|
+
gitea: []
|
|
21
|
+
};
|
|
22
|
+
var CLI_AUTH_TIMEOUT_MS = 5e3;
|
|
23
|
+
function resolveFromEnv(platform, customEnvVar) {
|
|
24
|
+
if (customEnvVar !== void 0) {
|
|
25
|
+
const val = process.env[customEnvVar];
|
|
26
|
+
if (val !== void 0 && val !== "") {
|
|
27
|
+
return { value: val, strategy: "env", platform };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const envVar of ENV_VARS[platform]) {
|
|
31
|
+
const val = process.env[envVar];
|
|
32
|
+
if (val !== void 0 && val !== "") {
|
|
33
|
+
return { value: val, strategy: "env", platform };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
async function resolveFromCli(platform) {
|
|
39
|
+
const cmd = CLI_AUTH_COMMANDS[platform];
|
|
40
|
+
if (cmd.length === 0) return void 0;
|
|
41
|
+
const [bin, ...args] = cmd;
|
|
42
|
+
if (bin === void 0) return void 0;
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync(bin, args, {
|
|
45
|
+
timeout: CLI_AUTH_TIMEOUT_MS
|
|
46
|
+
});
|
|
47
|
+
const token = stdout.trim();
|
|
48
|
+
if (token !== "") {
|
|
49
|
+
return { value: token, strategy: "cli", platform };
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
logger.debug("CLI auth token resolution failed", { platform });
|
|
53
|
+
}
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
async function resolveToken(config) {
|
|
57
|
+
const platform = config?.platform ?? "github";
|
|
58
|
+
if (config?.token !== void 0 && config.token !== "") {
|
|
59
|
+
return ok({ value: config.token, strategy: "config", platform });
|
|
60
|
+
}
|
|
61
|
+
const envToken = resolveFromEnv(platform, config?.envVar);
|
|
62
|
+
if (envToken !== void 0) {
|
|
63
|
+
logger.debug("Token resolved from environment", { platform, strategy: "env" });
|
|
64
|
+
return ok(envToken);
|
|
65
|
+
}
|
|
66
|
+
const cliToken = await resolveFromCli(platform);
|
|
67
|
+
if (cliToken !== void 0) {
|
|
68
|
+
logger.debug("Token resolved from CLI auth", { platform, strategy: "cli" });
|
|
69
|
+
return ok(cliToken);
|
|
70
|
+
}
|
|
71
|
+
const envVarList = ENV_VARS[platform].join(" or ");
|
|
72
|
+
return err(
|
|
73
|
+
new Error(
|
|
74
|
+
`No ${platform} token found. Set ${envVarList} environment variable, or authenticate via CLI (${CLI_AUTH_COMMANDS[platform].join(" ")}).`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function hasToken(platform = "github") {
|
|
79
|
+
return resolveFromEnv(platform) !== void 0;
|
|
80
|
+
}
|
|
81
|
+
function getTokenEnvVars(platform = "github") {
|
|
82
|
+
return ENV_VARS[platform];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
resolveToken,
|
|
87
|
+
hasToken,
|
|
88
|
+
getTokenEnvVars
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=chunk-7SKAKA4I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scm/token-resolver.ts"],"sourcesContent":["/**\n * nexus-agents/scm - Centralized Token Resolver\n *\n * Single source of truth for SCM token resolution. Priority:\n * 1. Explicit config (token passed directly)\n * 2. Environment variables (GITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN)\n * 3. CLI auth (gh auth token, glab auth token)\n *\n * @module scm/token-resolver\n * (Source: Issue #1136 — Centralized SCM Provider Module)\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { Result } from '../core/index.js';\nimport { ok, err, createLogger } from '../core/index.js';\nimport type { ScmToken, ScmPlatform, TokenResolverConfig } from './types.js';\n\nconst execFileAsync = promisify(execFile);\nconst logger = createLogger({ component: 'TokenResolver' });\n\n/** Environment variable names per platform. */\nconst ENV_VARS: Record<ScmPlatform, readonly string[]> = {\n github: ['GITHUB_TOKEN', 'GH_TOKEN'],\n gitlab: ['GITLAB_TOKEN', 'GL_TOKEN'],\n gitea: ['GITEA_TOKEN'],\n} as const;\n\n/** CLI commands to fetch auth tokens per platform. */\nconst CLI_AUTH_COMMANDS: Record<ScmPlatform, readonly string[]> = {\n github: ['gh', 'auth', 'token'],\n gitlab: ['glab', 'auth', 'token'],\n gitea: [],\n} as const;\n\n/** CLI auth timeout in ms. */\nconst CLI_AUTH_TIMEOUT_MS = 5_000;\n\n/**\n * Resolves a token from environment variables.\n */\nfunction resolveFromEnv(platform: ScmPlatform, customEnvVar?: string): ScmToken | undefined {\n // Custom env var takes priority\n if (customEnvVar !== undefined) {\n const val = process.env[customEnvVar];\n if (val !== undefined && val !== '') {\n return { value: val, strategy: 'env', platform };\n }\n }\n\n // Check platform-specific env vars\n for (const envVar of ENV_VARS[platform]) {\n const val = process.env[envVar];\n if (val !== undefined && val !== '') {\n return { value: val, strategy: 'env', platform };\n }\n }\n\n return undefined;\n}\n\n/**\n * Resolves a token from CLI auth.\n */\nasync function resolveFromCli(platform: ScmPlatform): Promise<ScmToken | undefined> {\n const cmd = CLI_AUTH_COMMANDS[platform];\n if (cmd.length === 0) return undefined;\n\n const [bin, ...args] = cmd;\n if (bin === undefined) return undefined;\n\n try {\n const { stdout } = await execFileAsync(bin, args, {\n timeout: CLI_AUTH_TIMEOUT_MS,\n });\n const token = stdout.trim();\n if (token !== '') {\n return { value: token, strategy: 'cli', platform };\n }\n } catch {\n logger.debug('CLI auth token resolution failed', { platform });\n }\n\n return undefined;\n}\n\n/**\n * Resolves an SCM token using the priority chain:\n * 1. Explicit config\n * 2. Environment variables\n * 3. CLI auth\n *\n * @param config - Token resolution configuration\n * @returns Resolved token or error\n */\nexport async function resolveToken(config?: TokenResolverConfig): Promise<Result<ScmToken, Error>> {\n const platform = config?.platform ?? 'github';\n\n // Priority 1: Explicit config\n if (config?.token !== undefined && config.token !== '') {\n return ok({ value: config.token, strategy: 'config' as const, platform });\n }\n\n // Priority 2: Environment variables\n const envToken = resolveFromEnv(platform, config?.envVar);\n if (envToken !== undefined) {\n logger.debug('Token resolved from environment', { platform, strategy: 'env' });\n return ok(envToken);\n }\n\n // Priority 3: CLI auth\n const cliToken = await resolveFromCli(platform);\n if (cliToken !== undefined) {\n logger.debug('Token resolved from CLI auth', { platform, strategy: 'cli' });\n return ok(cliToken);\n }\n\n const envVarList = ENV_VARS[platform].join(' or ');\n return err(\n new Error(\n `No ${platform} token found. Set ${envVarList} environment variable, ` +\n `or authenticate via CLI (${CLI_AUTH_COMMANDS[platform].join(' ')}).`\n )\n );\n}\n\n/**\n * Synchronous check: is any token available for the given platform?\n * Only checks environment variables (no CLI auth, which is async).\n */\nexport function hasToken(platform: ScmPlatform = 'github'): boolean {\n return resolveFromEnv(platform) !== undefined;\n}\n\n/**\n * Returns the list of environment variable names for a platform.\n * Useful for documentation and error messages.\n */\nexport function getTokenEnvVars(platform: ScmPlatform = 'github'): readonly string[] {\n return ENV_VARS[platform];\n}\n"],"mappings":";;;;;;;AAYA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAK1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,SAAS,aAAa,EAAE,WAAW,gBAAgB,CAAC;AAG1D,IAAM,WAAmD;AAAA,EACvD,QAAQ,CAAC,gBAAgB,UAAU;AAAA,EACnC,QAAQ,CAAC,gBAAgB,UAAU;AAAA,EACnC,OAAO,CAAC,aAAa;AACvB;AAGA,IAAM,oBAA4D;AAAA,EAChE,QAAQ,CAAC,MAAM,QAAQ,OAAO;AAAA,EAC9B,QAAQ,CAAC,QAAQ,QAAQ,OAAO;AAAA,EAChC,OAAO,CAAC;AACV;AAGA,IAAM,sBAAsB;AAK5B,SAAS,eAAe,UAAuB,cAA6C;AAE1F,MAAI,iBAAiB,QAAW;AAC9B,UAAM,MAAM,QAAQ,IAAI,YAAY;AACpC,QAAI,QAAQ,UAAa,QAAQ,IAAI;AACnC,aAAO,EAAE,OAAO,KAAK,UAAU,OAAO,SAAS;AAAA,IACjD;AAAA,EACF;AAGA,aAAW,UAAU,SAAS,QAAQ,GAAG;AACvC,UAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,QAAI,QAAQ,UAAa,QAAQ,IAAI;AACnC,aAAO,EAAE,OAAO,KAAK,UAAU,OAAO,SAAS;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAe,eAAe,UAAsD;AAClF,QAAM,MAAM,kBAAkB,QAAQ;AACtC,MAAI,IAAI,WAAW,EAAG,QAAO;AAE7B,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,MAAI,QAAQ,OAAW,QAAO;AAE9B,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,KAAK,MAAM;AAAA,MAChD,SAAS;AAAA,IACX,CAAC;AACD,UAAM,QAAQ,OAAO,KAAK;AAC1B,QAAI,UAAU,IAAI;AAChB,aAAO,EAAE,OAAO,OAAO,UAAU,OAAO,SAAS;AAAA,IACnD;AAAA,EACF,QAAQ;AACN,WAAO,MAAM,oCAAoC,EAAE,SAAS,CAAC;AAAA,EAC/D;AAEA,SAAO;AACT;AAWA,eAAsB,aAAa,QAAgE;AACjG,QAAM,WAAW,QAAQ,YAAY;AAGrC,MAAI,QAAQ,UAAU,UAAa,OAAO,UAAU,IAAI;AACtD,WAAO,GAAG,EAAE,OAAO,OAAO,OAAO,UAAU,UAAmB,SAAS,CAAC;AAAA,EAC1E;AAGA,QAAM,WAAW,eAAe,UAAU,QAAQ,MAAM;AACxD,MAAI,aAAa,QAAW;AAC1B,WAAO,MAAM,mCAAmC,EAAE,UAAU,UAAU,MAAM,CAAC;AAC7E,WAAO,GAAG,QAAQ;AAAA,EACpB;AAGA,QAAM,WAAW,MAAM,eAAe,QAAQ;AAC9C,MAAI,aAAa,QAAW;AAC1B,WAAO,MAAM,gCAAgC,EAAE,UAAU,UAAU,MAAM,CAAC;AAC1E,WAAO,GAAG,QAAQ;AAAA,EACpB;AAEA,QAAM,aAAa,SAAS,QAAQ,EAAE,KAAK,MAAM;AACjD,SAAO;AAAA,IACL,IAAI;AAAA,MACF,MAAM,QAAQ,qBAAqB,UAAU,mDACf,kBAAkB,QAAQ,EAAE,KAAK,GAAG,CAAC;AAAA,IACrE;AAAA,EACF;AACF;AAMO,SAAS,SAAS,WAAwB,UAAmB;AAClE,SAAO,eAAe,QAAQ,MAAM;AACtC;AAMO,SAAS,gBAAgB,WAAwB,UAA6B;AACnF,SAAO,SAAS,QAAQ;AAC1B;","names":[]}
|