@enrichlayer/el-linear 1.10.0 → 1.16.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 +126 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +27 -6
- package/dist/commands/config.js +178 -5
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/relations.d.ts +3 -14
- package/dist/commands/issues/relations.js +3 -3
- package/dist/commands/issues.js +222 -43
- package/dist/commands/labels.js +2 -1
- package/dist/commands/profile.js +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +91 -7
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +67 -8
- package/dist/commands/search.js +30 -5
- package/dist/commands/users.js +4 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +9 -2
- package/dist/main.js +13 -1
- package/dist/output.d.ts +82 -0
- package/dist/output.js +82 -0
- package/dist/queries/comments-types.d.ts +15 -9
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +4 -3
- package/dist/queries/introspect-types.d.ts +8 -7
- package/dist/queries/issues-types.d.ts +92 -27
- package/dist/queries/issues.d.ts +49 -10
- package/dist/queries/issues.js +125 -5
- package/dist/queries/labels-types.d.ts +7 -6
- package/dist/queries/project-milestones-types.d.ts +5 -4
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +8 -7
- package/dist/queries/releases-types.d.ts +5 -4
- package/dist/queries/search-types.d.ts +28 -12
- package/dist/queries/templates-types.d.ts +3 -2
- package/dist/types/linear.d.ts +13 -1
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +1 -10
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-issues-service.d.ts +101 -45
- package/dist/utils/graphql-issues-service.js +252 -39
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +81 -3
- package/dist/utils/output.js +61 -6
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.js +20 -4
- package/package.json +7 -2
package/dist/config/config.js
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { outputWarning } from "../utils/output.js";
|
|
3
|
-
import { CONFIG_PATH, resolveActiveProfile } from "./paths.js";
|
|
5
|
+
import { CONFIG_PATH, LOCAL_CONFIG_PATH, resolveActiveProfile, } from "./paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* Marker file written by the EL onboarding flow (`link-project.sh`) when a
|
|
8
|
+
* developer wires the tools repo to their machine. We treat its presence as
|
|
9
|
+
* the consent signal for auto-discovering the team config — see
|
|
10
|
+
* `discoverTeamConfigViaMarker` and DEV-4258.
|
|
11
|
+
*
|
|
12
|
+
* `os.homedir()` is evaluated lazily inside the helper so a mocked HOME in
|
|
13
|
+
* tests doesn't get baked into a module-level constant.
|
|
14
|
+
*/
|
|
15
|
+
function elToolsRootMarkerPath() {
|
|
16
|
+
return path.join(os.homedir(), ".config", "el-tools-root");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the shared team config path by reading the `~/.config/el-tools-root`
|
|
20
|
+
* marker file written by the EL onboarding flow (`link-project.sh`). Looks for
|
|
21
|
+
* `<root>/config/el-linear.shared.json` and returns the absolute path when it
|
|
22
|
+
* exists. Returns `undefined` for any of: marker missing, marker unreadable,
|
|
23
|
+
* marker empty, shared file missing — every failure mode is silent because
|
|
24
|
+
* the marker is opt-in and a non-EL user (OSS audience) is expected to miss
|
|
25
|
+
* it.
|
|
26
|
+
*
|
|
27
|
+
* The marker is the consent signal: only developers who explicitly linked
|
|
28
|
+
* their tools repo see this auto-discovery. DEV-4258 / ALL-964.
|
|
29
|
+
*/
|
|
30
|
+
function discoverTeamConfigViaMarker() {
|
|
31
|
+
try {
|
|
32
|
+
const root = fs.readFileSync(elToolsRootMarkerPath(), "utf8").trim();
|
|
33
|
+
if (!root)
|
|
34
|
+
return undefined;
|
|
35
|
+
const shared = path.join(root, "config", "el-linear.shared.json");
|
|
36
|
+
return fs.existsSync(shared) ? shared : undefined;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
4
42
|
const DEFAULT_CONFIG = {
|
|
5
43
|
defaultTeam: "",
|
|
6
44
|
defaultLabels: [],
|
|
@@ -22,77 +60,245 @@ const DEFAULT_CONFIG = {
|
|
|
22
60
|
},
|
|
23
61
|
terms: [],
|
|
24
62
|
};
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// `
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
63
|
+
// Two-level cache to avoid re-reading personal config on every loadConfig() call.
|
|
64
|
+
//
|
|
65
|
+
// Level 1 — team config path resolution:
|
|
66
|
+
// Key: `"${profileKey}::${envPath}"` (both "" when absent)
|
|
67
|
+
// Value: the resolved team config path, or undefined if none is configured.
|
|
68
|
+
// Populated on the first loadConfig() call for a (profile, env) combination.
|
|
69
|
+
// On subsequent calls the cache hit lets us skip reading personal config and
|
|
70
|
+
// go straight to the merged-config cache.
|
|
71
|
+
//
|
|
72
|
+
// Level 2 — merged result (team + personal):
|
|
73
|
+
// Key: `"${profileKey}::${teamConfigPath}"`
|
|
74
|
+
// Value: the fully merged ElLinearConfig (before local.json overlay).
|
|
33
75
|
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
// Level 3 — local config:
|
|
77
|
+
// Key: profile name (null for legacy single-file layout).
|
|
78
|
+
// Value: the parsed ElLinearLocalConfig from local.json.
|
|
79
|
+
//
|
|
80
|
+
// ALL-935 deferred fix note: profile switching mid-process is covered because
|
|
81
|
+
// the profile name is part of all cache keys.
|
|
82
|
+
const cachedTeamConfigPath = new Map();
|
|
83
|
+
const cachedConfig = new Map();
|
|
84
|
+
const cachedLocalConfigByProfile = new Map();
|
|
85
|
+
/** Test seam — resets all caches between test cases. */
|
|
39
86
|
export function _resetConfigCacheForTests() {
|
|
40
|
-
|
|
87
|
+
cachedTeamConfigPath.clear();
|
|
88
|
+
cachedConfig.clear();
|
|
89
|
+
cachedLocalConfigByProfile.clear();
|
|
41
90
|
}
|
|
42
91
|
export function loadConfig() {
|
|
43
|
-
// Profile-aware: read from <CONFIG_DIR>/profiles/<name>/config.json
|
|
44
|
-
// when a profile is active, falling back to the legacy single-file
|
|
45
|
-
// path so existing setups keep working without migration.
|
|
46
92
|
const active = resolveActiveProfile();
|
|
47
|
-
const
|
|
48
|
-
const
|
|
93
|
+
const teamConfigEnvPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
|
|
94
|
+
const profileKey = active.name ?? "";
|
|
95
|
+
const pathCacheKey = `${profileKey}::${teamConfigEnvPath ?? ""}`;
|
|
96
|
+
// Fast path: if we already know the team config path for this
|
|
97
|
+
// (profile, env) combination, skip reading personal config.
|
|
98
|
+
let teamConfigPath;
|
|
99
|
+
let personalRaw;
|
|
100
|
+
if (cachedTeamConfigPath.has(pathCacheKey)) {
|
|
101
|
+
teamConfigPath = cachedTeamConfigPath.get(pathCacheKey);
|
|
102
|
+
}
|
|
103
|
+
else if (teamConfigEnvPath !== undefined) {
|
|
104
|
+
// Env var fully determines the path — no need to read personal config.
|
|
105
|
+
teamConfigPath = teamConfigEnvPath;
|
|
106
|
+
cachedTeamConfigPath.set(pathCacheKey, teamConfigPath);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// First time for this profile without an env var: read personal config
|
|
110
|
+
// to find teamConfigPath, then fall back to the EL onboarding marker
|
|
111
|
+
// (`~/.config/el-tools-root` → `<root>/config/el-linear.shared.json`).
|
|
112
|
+
// The marker is the opt-in consent signal — only developers who ran
|
|
113
|
+
// `link-project.sh` get the auto-discovery; OSS users hit the silent
|
|
114
|
+
// no-op path. DEV-4258 / ALL-964.
|
|
115
|
+
personalRaw = readRawPersonalConfig(active);
|
|
116
|
+
teamConfigPath = personalRaw.teamConfigPath;
|
|
117
|
+
if (teamConfigPath === undefined) {
|
|
118
|
+
teamConfigPath = discoverTeamConfigViaMarker();
|
|
119
|
+
}
|
|
120
|
+
cachedTeamConfigPath.set(pathCacheKey, teamConfigPath);
|
|
121
|
+
}
|
|
122
|
+
const cacheKey = `${profileKey}::${teamConfigPath ?? ""}`;
|
|
123
|
+
const cached = cachedConfig.get(cacheKey);
|
|
49
124
|
if (cached) {
|
|
125
|
+
// Apply local config on top of cached merged result.
|
|
126
|
+
return applyLocalConfig(cached, loadLocalConfig());
|
|
127
|
+
}
|
|
128
|
+
// Cache miss — do the full merge. Reuse personalRaw if we already read it.
|
|
129
|
+
if (!personalRaw) {
|
|
130
|
+
personalRaw = readRawPersonalConfig(active);
|
|
131
|
+
}
|
|
132
|
+
// Load the team config layer (errors are warned, never thrown).
|
|
133
|
+
const teamRaw = teamConfigPath
|
|
134
|
+
? loadTeamConfigRaw(teamConfigPath)
|
|
135
|
+
: {};
|
|
136
|
+
// teamConfigPath inside a team config file would be circular; strip it.
|
|
137
|
+
delete teamRaw.teamConfigPath;
|
|
138
|
+
// Merge order: defaults → team config → personal config.
|
|
139
|
+
// Arrays (terms, defaultLabels, etc.) are concatenated so personal entries
|
|
140
|
+
// extend team entries rather than replace them.
|
|
141
|
+
const afterTeam = deepMerge(DEFAULT_CONFIG, teamRaw);
|
|
142
|
+
const merged = deepMerge(afterTeam, personalRaw);
|
|
143
|
+
cachedConfig.set(cacheKey, merged);
|
|
144
|
+
// Overlay local.json on top — highest priority layer.
|
|
145
|
+
return applyLocalConfig(merged, loadLocalConfig());
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns the team config file path that is active for the current process
|
|
149
|
+
* (resolved via `EL_LINEAR_TEAM_CONFIG` env var, `teamConfigPath` in personal
|
|
150
|
+
* config, or `~/.config/el-tools-root` marker — in that order). Returns
|
|
151
|
+
* `undefined` when no team config is configured.
|
|
152
|
+
*
|
|
153
|
+
* Calling this before `loadConfig()` will trigger a `loadConfig()` internally
|
|
154
|
+
* so the path cache is populated.
|
|
155
|
+
*/
|
|
156
|
+
export function getActiveTeamConfigPath() {
|
|
157
|
+
const active = resolveActiveProfile();
|
|
158
|
+
const teamConfigEnvPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
|
|
159
|
+
const profileKey = active.name ?? "";
|
|
160
|
+
const pathCacheKey = `${profileKey}::${teamConfigEnvPath ?? ""}`;
|
|
161
|
+
if (!cachedTeamConfigPath.has(pathCacheKey)) {
|
|
162
|
+
loadConfig();
|
|
163
|
+
}
|
|
164
|
+
return cachedTeamConfigPath.get(pathCacheKey);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Returns the active team-config path AND the source it was resolved from
|
|
168
|
+
* (env var / personal config field / onboarding-marker auto-discovery).
|
|
169
|
+
* `el-linear config team show` uses this to render which resolver fired —
|
|
170
|
+
* so an operator debugging "why isn't my team config loading?" sees the
|
|
171
|
+
* answer in one command.
|
|
172
|
+
*
|
|
173
|
+
* Re-derives source on each call rather than caching it: the lookup is cheap
|
|
174
|
+
* (one file read at most for personal config, one for the marker) and only
|
|
175
|
+
* `config team show` calls it.
|
|
176
|
+
*/
|
|
177
|
+
export function getActiveTeamConfigInfo() {
|
|
178
|
+
const envPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
|
|
179
|
+
if (envPath !== undefined) {
|
|
180
|
+
return { path: envPath, source: "env" };
|
|
181
|
+
}
|
|
182
|
+
const personalRaw = readRawPersonalConfig(resolveActiveProfile());
|
|
183
|
+
const personalPath = personalRaw.teamConfigPath;
|
|
184
|
+
if (personalPath !== undefined && personalPath !== "") {
|
|
185
|
+
return { path: personalPath, source: "personal" };
|
|
186
|
+
}
|
|
187
|
+
const markerPath = discoverTeamConfigViaMarker();
|
|
188
|
+
if (markerPath !== undefined) {
|
|
189
|
+
return { path: markerPath, source: "marker" };
|
|
190
|
+
}
|
|
191
|
+
return { path: undefined, source: null };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Load user-local overrides from `local.json`. Returns an empty object when
|
|
195
|
+
* no file exists — callers treat it as "no overrides". Errors are silent so
|
|
196
|
+
* a missing or malformed `local.json` never breaks a command.
|
|
197
|
+
*/
|
|
198
|
+
export function loadLocalConfig() {
|
|
199
|
+
const active = resolveActiveProfile();
|
|
200
|
+
const cacheKey = active.name;
|
|
201
|
+
const cached = cachedLocalConfigByProfile.get(cacheKey);
|
|
202
|
+
if (cached !== undefined) {
|
|
50
203
|
return cached;
|
|
51
204
|
}
|
|
52
|
-
const candidates = [active.
|
|
53
|
-
if (active.
|
|
54
|
-
candidates.push(
|
|
205
|
+
const candidates = [active.localConfigPath];
|
|
206
|
+
if (active.localConfigPath !== LOCAL_CONFIG_PATH)
|
|
207
|
+
candidates.push(LOCAL_CONFIG_PATH);
|
|
55
208
|
const sourcePath = candidates.find((p) => fs.existsSync(p));
|
|
56
|
-
let
|
|
209
|
+
let local = {};
|
|
57
210
|
if (sourcePath) {
|
|
58
211
|
try {
|
|
59
|
-
|
|
60
|
-
// Migration: the legacy `brand: { name, reject }` config is auto-promoted to
|
|
61
|
-
// a single entry in `terms[]`. We warn (not throw) so existing users get a
|
|
62
|
-
// grace period to update their config.
|
|
63
|
-
if (userConfig.brand &&
|
|
64
|
-
typeof userConfig.brand === "object" &&
|
|
65
|
-
!Array.isArray(userConfig.brand)) {
|
|
66
|
-
const legacy = userConfig.brand;
|
|
67
|
-
if (typeof legacy.name === "string" && Array.isArray(legacy.reject)) {
|
|
68
|
-
outputWarning("config.brand is deprecated — replace it with config.terms = [{ canonical, reject }]. See README#term-enforcer.");
|
|
69
|
-
const existing = Array.isArray(userConfig.terms)
|
|
70
|
-
? userConfig.terms
|
|
71
|
-
: [];
|
|
72
|
-
userConfig.terms = [
|
|
73
|
-
...existing,
|
|
74
|
-
{ canonical: legacy.name, reject: legacy.reject },
|
|
75
|
-
];
|
|
76
|
-
}
|
|
77
|
-
delete userConfig.brand;
|
|
78
|
-
}
|
|
79
|
-
resolved = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
212
|
+
local = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
80
213
|
}
|
|
81
214
|
catch {
|
|
82
|
-
outputWarning(`Failed to parse ${sourcePath},
|
|
83
|
-
resolved = DEFAULT_CONFIG;
|
|
215
|
+
outputWarning(`Failed to parse ${sourcePath}, ignoring local config`);
|
|
84
216
|
}
|
|
85
217
|
}
|
|
86
|
-
|
|
218
|
+
cachedLocalConfigByProfile.set(cacheKey, local);
|
|
219
|
+
return local;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Merge local config overrides onto the merged team+personal config.
|
|
223
|
+
* `assigneeEmail` maps to `defaultAssignee` as a convenience alias.
|
|
224
|
+
*/
|
|
225
|
+
function applyLocalConfig(base, local) {
|
|
226
|
+
const hasApplicable = local.cacheTTLSeconds !== undefined ||
|
|
227
|
+
local.defaultPriority !== undefined ||
|
|
228
|
+
local.defaultAssignee !== undefined ||
|
|
229
|
+
local.assigneeEmail !== undefined;
|
|
230
|
+
if (!hasApplicable)
|
|
231
|
+
return base;
|
|
232
|
+
const result = { ...base };
|
|
233
|
+
if (local.cacheTTLSeconds !== undefined)
|
|
234
|
+
result.cacheTTLSeconds = local.cacheTTLSeconds;
|
|
235
|
+
if (local.defaultPriority !== undefined)
|
|
236
|
+
result.defaultPriority = local.defaultPriority;
|
|
237
|
+
if (local.defaultAssignee !== undefined) {
|
|
238
|
+
result.defaultAssignee = local.defaultAssignee;
|
|
239
|
+
}
|
|
240
|
+
else if (local.assigneeEmail !== undefined) {
|
|
241
|
+
result.defaultAssignee = local.assigneeEmail;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function readRawPersonalConfig(active) {
|
|
246
|
+
const candidates = [active.configPath];
|
|
247
|
+
if (active.configPath !== CONFIG_PATH)
|
|
248
|
+
candidates.push(CONFIG_PATH);
|
|
249
|
+
const sourcePath = candidates.find((p) => fs.existsSync(p));
|
|
250
|
+
if (!sourcePath) {
|
|
87
251
|
const profileNote = active.name
|
|
88
252
|
? ` (active profile: \`${active.name}\` — expected at ${active.configPath})`
|
|
89
253
|
: "";
|
|
90
254
|
outputWarning(`No config found at ${active.configPath}${profileNote}. Run \`el-linear init\` (or \`el-linear profile add ${active.name ?? "<name>"}\`) to create one.`);
|
|
91
|
-
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const raw = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
259
|
+
// Migration: the legacy `brand: { name, reject }` config is auto-promoted
|
|
260
|
+
// to a single entry in `terms[]`. We warn (not throw) so existing users get
|
|
261
|
+
// a grace period to update their config.
|
|
262
|
+
if (raw.brand &&
|
|
263
|
+
typeof raw.brand === "object" &&
|
|
264
|
+
!Array.isArray(raw.brand)) {
|
|
265
|
+
const legacy = raw.brand;
|
|
266
|
+
if (typeof legacy.name === "string" && Array.isArray(legacy.reject)) {
|
|
267
|
+
outputWarning("config.brand is deprecated — replace it with config.terms = [{ canonical, reject }]. See README#term-enforcer.");
|
|
268
|
+
const existing = Array.isArray(raw.terms) ? raw.terms : [];
|
|
269
|
+
raw.terms = [
|
|
270
|
+
...existing,
|
|
271
|
+
{ canonical: legacy.name, reject: legacy.reject },
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
delete raw.brand;
|
|
275
|
+
}
|
|
276
|
+
return raw;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
outputWarning(`Failed to parse ${sourcePath}, using empty defaults`);
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function loadTeamConfigRaw(teamConfigPath) {
|
|
284
|
+
if (!fs.existsSync(teamConfigPath)) {
|
|
285
|
+
outputWarning(`Team config not found at ${teamConfigPath} — skipping team config layer.`);
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(fs.readFileSync(teamConfigPath, "utf8"));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
outputWarning(`Failed to parse team config at ${teamConfigPath} — skipping team config layer.`);
|
|
293
|
+
return {};
|
|
92
294
|
}
|
|
93
|
-
cachedConfigByProfile.set(cacheKey, resolved);
|
|
94
|
-
return resolved;
|
|
95
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Deep-merge `source` into `target`. Objects are merged recursively; arrays
|
|
298
|
+
* are concatenated (target first, source appended) so that a team config's
|
|
299
|
+
* `terms` and `defaultLabels` are extended by personal config rather than
|
|
300
|
+
* replaced. Scalar values from `source` win over `target`.
|
|
301
|
+
*/
|
|
96
302
|
function deepMerge(target, source) {
|
|
97
303
|
const result = { ...target };
|
|
98
304
|
for (const key of Object.keys(source)) {
|
|
@@ -102,7 +308,13 @@ function deepMerge(target, source) {
|
|
|
102
308
|
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
103
309
|
continue;
|
|
104
310
|
}
|
|
105
|
-
if (source[key] &&
|
|
311
|
+
if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
|
312
|
+
result[key] = [
|
|
313
|
+
...target[key],
|
|
314
|
+
...source[key],
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
else if (source[key] &&
|
|
106
318
|
typeof source[key] === "object" &&
|
|
107
319
|
!Array.isArray(source[key]) &&
|
|
108
320
|
target[key] &&
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation error enrichment.
|
|
3
|
+
*
|
|
4
|
+
* When `validateIssueCreation` flags a missing required field AND `--team`
|
|
5
|
+
* is set, this module fetches concrete team-scoped suggestions (active
|
|
6
|
+
* projects, active members, valid labels) and appends a "Suggestions:" block
|
|
7
|
+
* to each relevant error message.
|
|
8
|
+
*
|
|
9
|
+
* Goal: reduce the agent's loop time. A single error should carry enough
|
|
10
|
+
* concrete options that the agent can rebuild a complete retry command
|
|
11
|
+
* without follow-up `projects list` / `users list` / `labels list` calls.
|
|
12
|
+
*
|
|
13
|
+
* Latency budget: enrichment only runs on the validation-fail path. The
|
|
14
|
+
* success path is unchanged. Fetches run in parallel and only for the fields
|
|
15
|
+
* that are actually missing.
|
|
16
|
+
*/
|
|
17
|
+
import type { GraphQLService } from "../utils/graphql-service.js";
|
|
18
|
+
import type { LinearService } from "../utils/linear-service.js";
|
|
19
|
+
import { type ValidationResult } from "./issue-validation.js";
|
|
20
|
+
interface EnrichOptions {
|
|
21
|
+
team?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Enrich validation errors with team-scoped suggestions.
|
|
26
|
+
*
|
|
27
|
+
* Mutates `result.errors` in place — each enrichable error gets a
|
|
28
|
+
* "Suggestions:" block appended. Verb→type inference is added as a prefixed
|
|
29
|
+
* "Inferred from title:" line on the `--labels` missing error.
|
|
30
|
+
*
|
|
31
|
+
* No-op when `--team` is not set or when there are no errors to enrich.
|
|
32
|
+
* Safe to call on the failure path only — never invoked when validation
|
|
33
|
+
* passes, so the success path's latency is unchanged.
|
|
34
|
+
*
|
|
35
|
+
* Failures (network, unknown team) are swallowed: enrichment is a best-effort
|
|
36
|
+
* UX hint, not part of the validation contract.
|
|
37
|
+
*/
|
|
38
|
+
export declare function enrichValidationErrors(result: ValidationResult, options: EnrichOptions, services: {
|
|
39
|
+
graphQLService: GraphQLService;
|
|
40
|
+
linearService: LinearService;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Enrich a project resolver failure ({@link PROJECT_NOT_FOUND_PATTERN})
|
|
44
|
+
* with the same team-scoped suggestions that {@link enrichValidationErrors}
|
|
45
|
+
* appends to `Missing --project` validation errors. Returns the original
|
|
46
|
+
* message unchanged if the error doesn't match or `--team` is unknown.
|
|
47
|
+
*
|
|
48
|
+
* The validation path covers "user forgot `--project`"; this path covers
|
|
49
|
+
* "user pasted a URL/slug/name that didn't resolve" — same recovery hints
|
|
50
|
+
* apply in both cases. Synthesizes a `Missing --project: …` error string
|
|
51
|
+
* so the existing classifier and suggestion code can be reused without
|
|
52
|
+
* special-casing the resolver shape.
|
|
53
|
+
*
|
|
54
|
+
* Best-effort like `enrichValidationErrors`: any failure (network, unknown
|
|
55
|
+
* team, GraphQL error) returns the original message unchanged. The caller
|
|
56
|
+
* is expected to rethrow with the returned message.
|
|
57
|
+
*/
|
|
58
|
+
export declare function enrichProjectResolverError(originalMessage: string, options: EnrichOptions, services: {
|
|
59
|
+
graphQLService: GraphQLService;
|
|
60
|
+
linearService: LinearService;
|
|
61
|
+
}): Promise<string>;
|
|
62
|
+
export {};
|