@akira-tl/forgerelay 1.1.2 → 1.2.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/CHANGELOG.md +38 -0
- package/README.md +14 -0
- package/dist/activity/runtime/mcp-query-tools.js +15 -5
- package/dist/cli/config/inspect.js +331 -0
- package/dist/cli/config/migrate.js +351 -0
- package/dist/cli/init/setup-config.js +41 -0
- package/dist/cli/init.js +128 -130
- package/dist/cli/maintenance.js +15 -5
- package/dist/cli/mcp/external-mcp.js +14 -10
- package/dist/cli/mcp/status.js +11 -6
- package/dist/cli.js +40 -7
- package/dist/lsp/language-server-config.js +66 -144
- package/dist/lsp/runtime/manager.js +35 -4
- package/dist/mcp/hooks/config.js +141 -0
- package/dist/mcp/hooks/external-mcp-transform.js +22 -13
- package/dist/mcp/hooks/hook-cli.js +62 -52
- package/dist/mcp/hooks/hooks.js +111 -85
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
- package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
- package/dist/mcp/server/core/capability-support.js +7 -0
- package/dist/runtime/config/config.js +142 -64
- package/dist/runtime/config/definition/catalog.js +12 -0
- package/dist/runtime/config/definition/definition.js +115 -0
- package/dist/runtime/config/definition/external-mcp.js +58 -0
- package/dist/runtime/config/definition/general-config.js +258 -0
- package/dist/runtime/config/definition/language-servers.js +116 -0
- package/dist/runtime/config/definition/schema.js +113 -0
- package/dist/runtime/config/definition/types.js +2 -0
- package/dist/runtime/config/external-mcp-auth-store.js +105 -29
- package/dist/runtime/config/external-mcp-config.js +39 -0
- package/dist/runtime/config/external-mcp-registry.js +284 -106
- package/dist/runtime/config/inspection/live.js +77 -0
- package/dist/runtime/config/resolution/general.js +93 -0
- package/dist/runtime/config/resolution/hooks.js +315 -0
- package/dist/runtime/config/resolution/language-servers.js +237 -0
- package/dist/runtime/config/resolution/project-sources.js +159 -0
- package/dist/runtime/config/resolution/resolver.js +485 -0
- package/dist/runtime/config/resolution/types.js +1 -0
- package/dist/runtime/config/runtime/config-runtime.js +74 -0
- package/dist/runtime/config/runtime/source-refresh.js +263 -0
- package/dist/runtime/config/user-config.js +38 -31
- package/dist/runtime/security/project-execution-trust.js +34 -0
- package/dist/runtime/testing/server-fixture.js +9 -8
- package/dist/server.js +10 -6
- package/dist/subagents/profiles.js +336 -53
- package/dist/subagents/sessions/execution.js +6 -35
- package/dist/subagents/sessions/manager.js +14 -2
- package/dist/ui/.vite/manifest.json +275 -275
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
- package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
- package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
- package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
- package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
- package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
- package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
- package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
- package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
- package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
- package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
- package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
- package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
- package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
- package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
- package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
- package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
- package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
- package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
- package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
- package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
- package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
- package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
- package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
- package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
- package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
- package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
- package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
- package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
- package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
- package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
- package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
- package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
- package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
- package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
- package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
- package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
- package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
- package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
- package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
- package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
- package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
- package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
- package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
- package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
- package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
- package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
- package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
- package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
- package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
- package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
- package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
- package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
- package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
- package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
- package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
- package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
- package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
- package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
- package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
- package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
- package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
- package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
- package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
- package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
- package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
- package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
- package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
- package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
- package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
- package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
- package/dist/workspaces/conversation-test-support.js +3 -11
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces/state/project-context.js +221 -0
- package/dist/workspaces.js +3 -1
- package/docs/chatgpt-coding-workflow.md +8 -3
- package/docs/configuration.md +99 -41
- package/docs/gotchas.md +24 -5
- package/docs/security.md +14 -3
- package/docs/setup.md +12 -7
- package/package.json +12 -3
- package/schemas/v1/config.project-local.schema.json +14 -0
- package/schemas/v1/config.project.schema.json +14 -0
- package/schemas/v1/config.user.schema.json +538 -0
- package/schemas/v1/hooks.project-local.schema.json +118 -0
- package/schemas/v1/hooks.project.schema.json +118 -0
- package/schemas/v1/hooks.user.schema.json +118 -0
- package/schemas/v1/language-servers.project-local.schema.json +230 -0
- package/schemas/v1/language-servers.project.schema.json +230 -0
- package/schemas/v1/language-servers.user.schema.json +230 -0
- package/schemas/v1/mcp.project-local.schema.json +143 -0
- package/schemas/v1/mcp.project.schema.json +143 -0
- package/schemas/v1/mcp.user.schema.json +143 -0
- package/scripts/ci/architecture.mjs +54 -4
- package/scripts/ci/cmd-acceptance.mjs +9 -9
- package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
- package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
- package/scripts/ci/gates/core-contract.mjs +27 -0
- package/scripts/ci/gates/platform-tests.mjs +38 -0
- package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
- package/scripts/ci/gates/test-shard.mjs +71 -0
- package/scripts/ci/powershell51-acceptance.mjs +9 -9
- package/scripts/ci/pwsh-acceptance.mjs +9 -9
- package/scripts/ci/verify.mjs +2 -0
- package/scripts/ci/windows-product-acceptance.mjs +9 -10
- package/scripts/config/schema-text.mjs +4 -0
- package/scripts/config/schema-text.test.mjs +18 -0
- package/scripts/config/schema.mjs +74 -0
- package/scripts/lsp/interop.mjs +4 -1
- package/scripts/release/pack.mjs +64 -4
- package/scripts/release/release-gate.test.mjs +93 -27
- package/scripts/release-parity.mjs +19 -2
- package/templates/instructions/cmd.md +12 -0
- package/templates/instructions/fish.md +11 -0
- package/templates/instructions/powershell.md +14 -0
- package/templates/instructions/pwsh.md +14 -0
- package/templates/instructions/zsh.md +11 -0
- package/dist/runtime/instructions/powershell-skill.js +0 -65
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { liveGeneralConfigState } from "../config.js";
|
|
2
|
+
import { ExternalMcpConfigRegistry } from "../external-mcp-registry.js";
|
|
3
|
+
import { resolveHooksConfig } from "../resolution/hooks.js";
|
|
4
|
+
import { resolveLanguageServersConfig } from "../resolution/language-servers.js";
|
|
5
|
+
import { resolveSubagentProfilesConfigSources } from "../../../subagents/profiles.js";
|
|
6
|
+
import { resolveProjectContext } from "../../../workspaces/state/project-context.js";
|
|
7
|
+
export async function liveWorkspaceConfigPanelState(config, workspaceRoot) {
|
|
8
|
+
const general = liveGeneralConfigState(config);
|
|
9
|
+
const restartRequired = Object.values(general.applied.fields)
|
|
10
|
+
.filter((field) => field.restartRequired)
|
|
11
|
+
.map((field) => ({
|
|
12
|
+
logicalPath: field.logicalPath,
|
|
13
|
+
configuredValue: field.configuredValue,
|
|
14
|
+
appliedValue: field.appliedValue,
|
|
15
|
+
}));
|
|
16
|
+
const project = await resolveProjectContext(config.configDir, workspaceRoot);
|
|
17
|
+
const mcp = new ExternalMcpConfigRegistry({
|
|
18
|
+
configDir: config.configDir,
|
|
19
|
+
environment: process.env,
|
|
20
|
+
sourceRuntime: config.configRuntime.sources,
|
|
21
|
+
}).resolveConfiguration({
|
|
22
|
+
projectSharedConfigDir: project.sharedConfigDir,
|
|
23
|
+
projectLocalConfigDir: project.localConfigDir,
|
|
24
|
+
});
|
|
25
|
+
const languageServers = await resolveLanguageServersConfig({
|
|
26
|
+
configDir: config.configDir,
|
|
27
|
+
project,
|
|
28
|
+
environment: process.env,
|
|
29
|
+
sourceRuntime: config.configRuntime.sources,
|
|
30
|
+
});
|
|
31
|
+
const hooks = await resolveHooksConfig({
|
|
32
|
+
configDir: config.configDir,
|
|
33
|
+
project,
|
|
34
|
+
sourceRuntime: config.configRuntime.sources,
|
|
35
|
+
});
|
|
36
|
+
const subagents = resolveSubagentProfilesConfigSources({
|
|
37
|
+
configDir: config.configDir,
|
|
38
|
+
sourceRuntime: config.configRuntime.sources,
|
|
39
|
+
projectSharedConfigDir: project.sharedConfigDir,
|
|
40
|
+
projectLocalConfigDir: project.localConfigDir,
|
|
41
|
+
});
|
|
42
|
+
const issues = uniqueLiveIssues([mcp, languageServers, hooks, subagents]);
|
|
43
|
+
if (restartRequired.length === 0 && !general.source && issues.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
...(restartRequired.length > 0 ? { restartRequired } : {}),
|
|
47
|
+
...(general.source ? { source: general.source } : {}),
|
|
48
|
+
...(issues.length > 0 ? { issues } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function uniqueLiveIssues(domains) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const issues = [];
|
|
54
|
+
for (const domain of domains) {
|
|
55
|
+
for (const diagnostic of domain.diagnostics) {
|
|
56
|
+
if (diagnostic.severity !== "error")
|
|
57
|
+
continue;
|
|
58
|
+
const issue = liveIssue(domain.domain, diagnostic);
|
|
59
|
+
const key = `${issue.domain}\0${issue.code}\0${issue.source}\0${issue.message}\0${issue.usingLastKnownGood}`;
|
|
60
|
+
if (seen.has(key))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
issues.push(issue);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return issues.sort((left, right) => left.domain.localeCompare(right.domain) || left.source.localeCompare(right.source) || left.code.localeCompare(right.code));
|
|
67
|
+
}
|
|
68
|
+
function liveIssue(domain, diagnostic) {
|
|
69
|
+
return {
|
|
70
|
+
domain,
|
|
71
|
+
severity: "error",
|
|
72
|
+
code: diagnostic.code,
|
|
73
|
+
source: diagnostic.source.location ?? diagnostic.source.id,
|
|
74
|
+
usingLastKnownGood: diagnostic.usingLastKnownGood === true,
|
|
75
|
+
message: diagnostic.message,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { generalConfigDefinition } from "../definition/general-config.js";
|
|
2
|
+
import { resolveConfigDomain } from "./resolver.js";
|
|
3
|
+
export function resolveGeneralConfig(input = {}) {
|
|
4
|
+
const env = input.env ?? process.env;
|
|
5
|
+
const sources = [];
|
|
6
|
+
appendFileSource(sources, {
|
|
7
|
+
expectedScope: "user",
|
|
8
|
+
prepared: input.userSource,
|
|
9
|
+
value: input.user,
|
|
10
|
+
sourcePath: input.userSourcePath,
|
|
11
|
+
});
|
|
12
|
+
appendFileSource(sources, {
|
|
13
|
+
expectedScope: "project",
|
|
14
|
+
prepared: input.projectSource,
|
|
15
|
+
value: input.project,
|
|
16
|
+
sourcePath: input.projectSourcePath,
|
|
17
|
+
});
|
|
18
|
+
appendFileSource(sources, {
|
|
19
|
+
expectedScope: "project-local",
|
|
20
|
+
prepared: input.projectLocalSource,
|
|
21
|
+
value: input.projectLocal,
|
|
22
|
+
sourcePath: input.projectLocalSourcePath,
|
|
23
|
+
});
|
|
24
|
+
sources.push(environmentSource(env));
|
|
25
|
+
if (input.cli !== undefined) {
|
|
26
|
+
sources.push({
|
|
27
|
+
id: "runtime:cli",
|
|
28
|
+
scope: "runtime",
|
|
29
|
+
kind: "cli",
|
|
30
|
+
priority: 20,
|
|
31
|
+
value: input.cli,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return resolveConfigDomain({
|
|
35
|
+
definition: generalConfigDefinition,
|
|
36
|
+
sources,
|
|
37
|
+
environment: env,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function appendFileSource(sources, input) {
|
|
41
|
+
if (input.prepared !== undefined && input.value !== undefined) {
|
|
42
|
+
throw new Error(`General configuration received duplicate ${input.expectedScope} sources.`);
|
|
43
|
+
}
|
|
44
|
+
if (input.prepared !== undefined) {
|
|
45
|
+
if (input.prepared.scope !== input.expectedScope || input.prepared.kind !== "file") {
|
|
46
|
+
throw new Error(`General configuration ${input.expectedScope} source must be a file source at the matching scope.`);
|
|
47
|
+
}
|
|
48
|
+
sources.push(input.prepared);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (input.value === undefined)
|
|
52
|
+
return;
|
|
53
|
+
sources.push({
|
|
54
|
+
id: `${input.expectedScope}:config`,
|
|
55
|
+
scope: input.expectedScope,
|
|
56
|
+
kind: "file",
|
|
57
|
+
location: input.sourcePath,
|
|
58
|
+
priority: 0,
|
|
59
|
+
value: input.value,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function environmentSource(env) {
|
|
63
|
+
const values = {};
|
|
64
|
+
try {
|
|
65
|
+
for (const [name, field] of Object.entries(generalConfigDefinition.fields)) {
|
|
66
|
+
const readEnv = field.runtimeOverride?.readEnv;
|
|
67
|
+
if (!readEnv)
|
|
68
|
+
continue;
|
|
69
|
+
const value = readEnv(env);
|
|
70
|
+
if (value !== undefined)
|
|
71
|
+
values[name] = value;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
id: "runtime:environment",
|
|
75
|
+
scope: "runtime",
|
|
76
|
+
kind: "environment",
|
|
77
|
+
priority: 10,
|
|
78
|
+
value: values,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
id: "runtime:environment",
|
|
84
|
+
scope: "runtime",
|
|
85
|
+
kind: "environment",
|
|
86
|
+
priority: 10,
|
|
87
|
+
error: {
|
|
88
|
+
code: "invalid_source",
|
|
89
|
+
message: error instanceof Error ? error.message : "Runtime environment configuration is invalid.",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { hooksConfigDefinition, normalizeLegacyHookEntries, } from "../../../mcp/hooks/config.js";
|
|
3
|
+
import { ConfigSourceRuntime } from "../runtime/source-refresh.js";
|
|
4
|
+
import { refreshLegacyUserConfigField } from "./project-sources.js";
|
|
5
|
+
import { resolveConfigDomain } from "./resolver.js";
|
|
6
|
+
const CANONICAL_PRIORITY = 100;
|
|
7
|
+
const LEGACY_PRIORITY = 0;
|
|
8
|
+
const LEGACY_DEPRECATION = {
|
|
9
|
+
since: "1.2.0",
|
|
10
|
+
removeIn: "1.4.0",
|
|
11
|
+
replacement: "hooks/",
|
|
12
|
+
};
|
|
13
|
+
export async function resolveHooksConfig(input) {
|
|
14
|
+
const sources = [];
|
|
15
|
+
const refreshDiagnostics = [];
|
|
16
|
+
const sourceRuntime = input.sourceRuntime ?? new ConfigSourceRuntime();
|
|
17
|
+
if (input.legacyUser && isRecord(input.legacyUser) && Object.keys(input.legacyUser).length > 0) {
|
|
18
|
+
sources.push({
|
|
19
|
+
id: "legacy:user:hooks",
|
|
20
|
+
scope: "user",
|
|
21
|
+
kind: "file",
|
|
22
|
+
priority: LEGACY_PRIORITY,
|
|
23
|
+
...(input.configDir ? { location: join(input.configDir, "config.json/hooks") } : {}),
|
|
24
|
+
normalized: true,
|
|
25
|
+
deprecation: LEGACY_DEPRECATION,
|
|
26
|
+
value: { hooks: normalizeLegacyHookEntries(input.legacyUser) },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (input.configDir) {
|
|
30
|
+
const legacyInline = readLegacyInlineHooks(sourceRuntime, input.configDir);
|
|
31
|
+
if (legacyInline.source)
|
|
32
|
+
sources.push(legacyInline.source);
|
|
33
|
+
if (legacyInline.diagnostic)
|
|
34
|
+
refreshDiagnostics.push(legacyInline.diagnostic);
|
|
35
|
+
const legacyUserFile = readLegacyHookAggregate(sourceRuntime, "legacy:user:hooks.json", "user", join(input.configDir, "hooks.json"));
|
|
36
|
+
if (legacyUserFile.source)
|
|
37
|
+
sources.push(legacyUserFile.source);
|
|
38
|
+
if (legacyUserFile.diagnostic)
|
|
39
|
+
refreshDiagnostics.push(legacyUserFile.diagnostic);
|
|
40
|
+
appendHookDirectory(sources, refreshDiagnostics, sourceRuntime, "user", join(input.configDir, "hooks"));
|
|
41
|
+
}
|
|
42
|
+
const sharedConfigDir = input.project?.sharedConfigDir ?? input.projectSharedConfigDir;
|
|
43
|
+
if (sharedConfigDir) {
|
|
44
|
+
const legacyProject = readLegacyHookAggregate(sourceRuntime, "legacy:project:hooks.json", "project", join(sharedConfigDir, "hooks.json"));
|
|
45
|
+
if (legacyProject.source)
|
|
46
|
+
sources.push(legacyProject.source);
|
|
47
|
+
if (legacyProject.diagnostic)
|
|
48
|
+
refreshDiagnostics.push(legacyProject.diagnostic);
|
|
49
|
+
appendHookDirectory(sources, refreshDiagnostics, sourceRuntime, "project", join(sharedConfigDir, "hooks"));
|
|
50
|
+
}
|
|
51
|
+
if (input.project) {
|
|
52
|
+
appendHookDirectory(sources, refreshDiagnostics, sourceRuntime, "project-local", join(input.project.localConfigDir, "hooks"));
|
|
53
|
+
}
|
|
54
|
+
const resolution = resolveConfigDomain({
|
|
55
|
+
definition: hooksConfigDefinition,
|
|
56
|
+
sources,
|
|
57
|
+
});
|
|
58
|
+
resolution.diagnostics = mergeDiagnostics(resolution.diagnostics, refreshDiagnostics);
|
|
59
|
+
return resolution;
|
|
60
|
+
}
|
|
61
|
+
export function effectiveHookConfigEntries(resolution) {
|
|
62
|
+
const hooks = isRecord(resolution.values.hooks) ? resolution.values.hooks : {};
|
|
63
|
+
const entries = Object.entries(hooks).flatMap(([name, value]) => {
|
|
64
|
+
const provenance = resolution.entries[`hooks.${name}`];
|
|
65
|
+
if (!provenance || !Array.isArray(value))
|
|
66
|
+
return [];
|
|
67
|
+
const scope = provenance.effective.source.scope;
|
|
68
|
+
if (scope !== "user" && scope !== "project" && scope !== "project-local")
|
|
69
|
+
return [];
|
|
70
|
+
return [{
|
|
71
|
+
name,
|
|
72
|
+
scope,
|
|
73
|
+
sourcePriority: provenance.effective.source.priority,
|
|
74
|
+
...(provenance.effective.source.location ? { sourceLocation: provenance.effective.source.location } : {}),
|
|
75
|
+
entries: value,
|
|
76
|
+
}];
|
|
77
|
+
});
|
|
78
|
+
return entries.sort((left, right) => {
|
|
79
|
+
const scope = hookScopeRank(left.scope) - hookScopeRank(right.scope);
|
|
80
|
+
if (scope !== 0)
|
|
81
|
+
return scope;
|
|
82
|
+
const priority = left.sourcePriority - right.sourcePriority;
|
|
83
|
+
if (priority !== 0)
|
|
84
|
+
return priority;
|
|
85
|
+
const leftOrder = left.entries[0]?.order ?? Number.MAX_SAFE_INTEGER;
|
|
86
|
+
const rightOrder = right.entries[0]?.order ?? Number.MAX_SAFE_INTEGER;
|
|
87
|
+
if (leftOrder !== rightOrder)
|
|
88
|
+
return leftOrder - rightOrder;
|
|
89
|
+
const location = (left.sourceLocation ?? "").localeCompare(right.sourceLocation ?? "");
|
|
90
|
+
return location !== 0 ? location : left.name.localeCompare(right.name);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function hookScopeRank(scope) {
|
|
94
|
+
return scope === "user" ? 0 : scope === "project" ? 1 : 2;
|
|
95
|
+
}
|
|
96
|
+
function appendHookDirectory(sources, diagnostics, sourceRuntime, scope, directory) {
|
|
97
|
+
const refreshed = sourceRuntime.refreshDirectory({
|
|
98
|
+
key: `hooks:${scope}:${directory}`,
|
|
99
|
+
directory,
|
|
100
|
+
include: (name) => name.endsWith(".json"),
|
|
101
|
+
parse: (_name, content) => JSON.parse(content),
|
|
102
|
+
parseIssue: {
|
|
103
|
+
code: "invalid_source",
|
|
104
|
+
message: "Hook configuration file is not valid JSON.",
|
|
105
|
+
},
|
|
106
|
+
readIssue: {
|
|
107
|
+
code: "invalid_source",
|
|
108
|
+
message: "Hook configuration file could not be read.",
|
|
109
|
+
},
|
|
110
|
+
validate: (value, name, location) => {
|
|
111
|
+
const entryKey = name.slice(0, -5);
|
|
112
|
+
const validation = resolveConfigDomain({
|
|
113
|
+
definition: hooksConfigDefinition,
|
|
114
|
+
sources: [canonicalHookSource(scope, location, entryKey, value)],
|
|
115
|
+
});
|
|
116
|
+
const error = validation.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
117
|
+
if (!error)
|
|
118
|
+
return undefined;
|
|
119
|
+
return {
|
|
120
|
+
code: error.code === "missing_environment" ? "missing_environment" : "invalid_source",
|
|
121
|
+
message: error.message,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (refreshed.state === "invalid" && refreshed.issue) {
|
|
126
|
+
diagnostics.push({
|
|
127
|
+
severity: "error",
|
|
128
|
+
code: refreshed.issue.code,
|
|
129
|
+
source: {
|
|
130
|
+
id: `canonical:${scope}:hooks-directory`,
|
|
131
|
+
scope,
|
|
132
|
+
kind: "file",
|
|
133
|
+
location: refreshed.directory,
|
|
134
|
+
priority: CANONICAL_PRIORITY,
|
|
135
|
+
},
|
|
136
|
+
message: refreshed.issue.message,
|
|
137
|
+
diagnosticChanged: refreshed.diagnosticChanged,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const unit of refreshed.units) {
|
|
141
|
+
const location = unit.status.path;
|
|
142
|
+
const entryKey = unit.name.slice(0, -5);
|
|
143
|
+
if (unit.value !== undefined) {
|
|
144
|
+
const source = canonicalHookSource(scope, location, entryKey, unit.value);
|
|
145
|
+
sources.push(source);
|
|
146
|
+
if (unit.status.state === "invalid" && unit.issue) {
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
severity: "error",
|
|
149
|
+
code: unit.issue.code,
|
|
150
|
+
source: sourceReference(source),
|
|
151
|
+
message: unit.issue.message,
|
|
152
|
+
usingLastKnownGood: true,
|
|
153
|
+
diagnosticChanged: unit.status.diagnosticChanged,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (unit.status.state === "invalid") {
|
|
159
|
+
sources.push(invalidHookSource(scope, location, entryKey, unit.issue?.message ?? "Hook configuration file is invalid.", unit.issue?.code));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function canonicalHookSource(scope, location, entryKey, value) {
|
|
164
|
+
return {
|
|
165
|
+
id: `canonical:${scope}:hook:${entryKey}`,
|
|
166
|
+
scope,
|
|
167
|
+
kind: "file",
|
|
168
|
+
location,
|
|
169
|
+
priority: CANONICAL_PRIORITY,
|
|
170
|
+
entryKey,
|
|
171
|
+
shadowsLowerPriorityKeys: [`hooks.${entryKey}`],
|
|
172
|
+
value,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function invalidHookSource(scope, location, entryKey, message, code = "invalid_source") {
|
|
176
|
+
return {
|
|
177
|
+
id: `canonical:${scope}:hook:${entryKey}`,
|
|
178
|
+
scope,
|
|
179
|
+
kind: "file",
|
|
180
|
+
location,
|
|
181
|
+
priority: CANONICAL_PRIORITY,
|
|
182
|
+
entryKey,
|
|
183
|
+
shadowsLowerPriorityKeys: [`hooks.${entryKey}`],
|
|
184
|
+
error: { code, message },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function readLegacyInlineHooks(sourceRuntime, configDir) {
|
|
188
|
+
const location = join(configDir, "config.json");
|
|
189
|
+
const refreshed = refreshLegacyUserConfigField({
|
|
190
|
+
sourceRuntime,
|
|
191
|
+
configDir,
|
|
192
|
+
field: "hooks",
|
|
193
|
+
parse: (value) => value === undefined ? undefined : normalizeLegacyHookEntries(value),
|
|
194
|
+
invalidMessage: "Legacy inline Hook configuration is invalid.",
|
|
195
|
+
});
|
|
196
|
+
if (refreshed.status.state === "missing")
|
|
197
|
+
return {};
|
|
198
|
+
const source = refreshed.value === undefined
|
|
199
|
+
? undefined
|
|
200
|
+
: legacyHookSource("legacy:user:config-hooks", "user", location, refreshed.value);
|
|
201
|
+
if (refreshed.status.state !== "invalid" || !refreshed.issue)
|
|
202
|
+
return source ? { source } : {};
|
|
203
|
+
if (refreshed.issueScope === "container")
|
|
204
|
+
return source ? { source } : {};
|
|
205
|
+
if (source) {
|
|
206
|
+
return {
|
|
207
|
+
source,
|
|
208
|
+
diagnostic: {
|
|
209
|
+
severity: "error",
|
|
210
|
+
code: refreshed.issue.code,
|
|
211
|
+
source: sourceReference(source),
|
|
212
|
+
message: refreshed.issue.message,
|
|
213
|
+
usingLastKnownGood: refreshed.status.usingLastKnownGood,
|
|
214
|
+
diagnosticChanged: refreshed.status.diagnosticChanged,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
source: {
|
|
220
|
+
id: "legacy:user:config-hooks",
|
|
221
|
+
scope: "user",
|
|
222
|
+
kind: "file",
|
|
223
|
+
location,
|
|
224
|
+
priority: LEGACY_PRIORITY,
|
|
225
|
+
deprecation: LEGACY_DEPRECATION,
|
|
226
|
+
error: {
|
|
227
|
+
code: refreshed.issue.code,
|
|
228
|
+
message: refreshed.issue.message,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function readLegacyHookAggregate(sourceRuntime, id, scope, location) {
|
|
234
|
+
const refreshed = sourceRuntime.refreshFile({
|
|
235
|
+
key: `hooks:${id}:${location}`,
|
|
236
|
+
path: location,
|
|
237
|
+
parse: (raw) => normalizeLegacyHookEntries(JSON.parse(raw)),
|
|
238
|
+
parseIssue: { code: "invalid_source", message: "Legacy Hook configuration is invalid." },
|
|
239
|
+
readIssue: { code: "invalid_source", message: "Legacy Hook configuration could not be read." },
|
|
240
|
+
validate: (value) => {
|
|
241
|
+
const validation = resolveConfigDomain({
|
|
242
|
+
definition: hooksConfigDefinition,
|
|
243
|
+
sources: [legacyHookSource(id, scope, location, value)],
|
|
244
|
+
});
|
|
245
|
+
const error = validation.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
246
|
+
return error ? { code: "invalid_source", message: error.message } : undefined;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
if (refreshed.status.state === "missing")
|
|
250
|
+
return {};
|
|
251
|
+
if (refreshed.value !== undefined) {
|
|
252
|
+
const source = legacyHookSource(id, scope, location, refreshed.value);
|
|
253
|
+
if (refreshed.status.state !== "invalid" || !refreshed.issue)
|
|
254
|
+
return { source };
|
|
255
|
+
return {
|
|
256
|
+
source,
|
|
257
|
+
diagnostic: {
|
|
258
|
+
severity: "error",
|
|
259
|
+
code: refreshed.issue.code,
|
|
260
|
+
source: sourceReference(source),
|
|
261
|
+
message: refreshed.issue.message,
|
|
262
|
+
usingLastKnownGood: true,
|
|
263
|
+
diagnosticChanged: refreshed.status.diagnosticChanged,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
source: {
|
|
269
|
+
id,
|
|
270
|
+
scope,
|
|
271
|
+
kind: "file",
|
|
272
|
+
location,
|
|
273
|
+
priority: LEGACY_PRIORITY,
|
|
274
|
+
deprecation: LEGACY_DEPRECATION,
|
|
275
|
+
error: {
|
|
276
|
+
code: refreshed.issue?.code ?? "invalid_source",
|
|
277
|
+
message: refreshed.issue?.message ?? "Legacy Hook configuration is invalid.",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function legacyHookSource(id, scope, location, value) {
|
|
283
|
+
return {
|
|
284
|
+
id,
|
|
285
|
+
scope,
|
|
286
|
+
kind: "file",
|
|
287
|
+
location,
|
|
288
|
+
priority: LEGACY_PRIORITY,
|
|
289
|
+
normalized: true,
|
|
290
|
+
deprecation: LEGACY_DEPRECATION,
|
|
291
|
+
value: { hooks: value },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function sourceReference(source) {
|
|
295
|
+
return {
|
|
296
|
+
id: source.id,
|
|
297
|
+
scope: source.scope,
|
|
298
|
+
kind: source.kind,
|
|
299
|
+
...(source.location ? { location: source.location } : {}),
|
|
300
|
+
priority: source.priority,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function mergeDiagnostics(left, right) {
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
return [...left, ...right].filter((diagnostic) => {
|
|
306
|
+
const key = `${diagnostic.source.id}\0${diagnostic.code}\0${diagnostic.message}\0${diagnostic.usingLastKnownGood === true}`;
|
|
307
|
+
if (seen.has(key))
|
|
308
|
+
return false;
|
|
309
|
+
seen.add(key);
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function isRecord(value) {
|
|
314
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
315
|
+
}
|