@jam-mcp/server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/adapters/cache/noop-cache.d.ts +7 -0
- package/dist/adapters/cache/noop-cache.js +12 -0
- package/dist/adapters/credentials/composite.d.ts +30 -0
- package/dist/adapters/credentials/composite.js +79 -0
- package/dist/adapters/credentials/process-env.d.ts +13 -0
- package/dist/adapters/credentials/process-env.js +17 -0
- package/dist/adapters/credentials/secret-store.d.ts +69 -0
- package/dist/adapters/credentials/secret-store.js +259 -0
- package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
- package/dist/adapters/credentials/windows-user-env.js +51 -0
- package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
- package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
- package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
- package/dist/adapters/jira-cloud/jira-client.js +137 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
- package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
- package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
- package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
- package/dist/adapters/jira-cloud/mapper.js +143 -0
- package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
- package/dist/adapters/telemetry/console-telemetry.js +31 -0
- package/dist/application/get-full-issue-context.d.ts +18 -0
- package/dist/application/get-full-issue-context.js +120 -0
- package/dist/application/get-issue-context.d.ts +19 -0
- package/dist/application/get-issue-context.js +79 -0
- package/dist/application/search-issues.d.ts +22 -0
- package/dist/application/search-issues.js +93 -0
- package/dist/bootstrap/boot-health-gate.d.ts +25 -0
- package/dist/bootstrap/boot-health-gate.js +137 -0
- package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
- package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
- package/dist/bootstrap/host-mcp.d.ts +54 -0
- package/dist/bootstrap/host-mcp.js +98 -0
- package/dist/bootstrap/jira-projects.d.ts +16 -0
- package/dist/bootstrap/jira-projects.js +24 -0
- package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
- package/dist/bootstrap/mcp-config-merger.js +119 -0
- package/dist/bootstrap/migration-target.d.ts +60 -0
- package/dist/bootstrap/migration-target.js +80 -0
- package/dist/bootstrap/project-bindings.d.ts +48 -0
- package/dist/bootstrap/project-bindings.js +112 -0
- package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
- package/dist/bootstrap/project-config-bootstrapper.js +69 -0
- package/dist/bootstrap/project-config-resolver.d.ts +53 -0
- package/dist/bootstrap/project-config-resolver.js +59 -0
- package/dist/bootstrap/project-root-resolver.d.ts +22 -0
- package/dist/bootstrap/project-root-resolver.js +43 -0
- package/dist/bootstrap/setup-apply.d.ts +31 -0
- package/dist/bootstrap/setup-apply.js +61 -0
- package/dist/bootstrap/setup-plan.d.ts +102 -0
- package/dist/bootstrap/setup-plan.js +224 -0
- package/dist/bootstrap/setup-state.d.ts +73 -0
- package/dist/bootstrap/setup-state.js +89 -0
- package/dist/bootstrap/workspace-identity.d.ts +52 -0
- package/dist/bootstrap/workspace-identity.js +114 -0
- package/dist/cli/agent-api.d.ts +63 -0
- package/dist/cli/agent-api.js +149 -0
- package/dist/cli/auth.d.ts +39 -0
- package/dist/cli/auth.js +201 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +31 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +97 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +23 -0
- package/dist/cli/setup-wizard.d.ts +37 -0
- package/dist/cli/setup-wizard.js +338 -0
- package/dist/cli/setup.d.ts +26 -0
- package/dist/cli/setup.js +214 -0
- package/dist/cli/ui.d.ts +118 -0
- package/dist/cli/ui.js +338 -0
- package/dist/cli-entry.d.ts +7 -0
- package/dist/cli-entry.js +124 -0
- package/dist/config/load-config.d.ts +18 -0
- package/dist/config/load-config.js +48 -0
- package/dist/config/schema.d.ts +36 -0
- package/dist/config/schema.js +74 -0
- package/dist/deps.d.ts +50 -0
- package/dist/deps.js +39 -0
- package/dist/domain/completeness.d.ts +63 -0
- package/dist/domain/completeness.js +20 -0
- package/dist/domain/context.d.ts +38 -0
- package/dist/domain/context.js +1 -0
- package/dist/domain/errors.d.ts +27 -0
- package/dist/domain/errors.js +49 -0
- package/dist/domain/issue.d.ts +35 -0
- package/dist/domain/issue.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/mcp/create-server.d.ts +9 -0
- package/dist/mcp/create-server.js +32 -0
- package/dist/mcp/tool-result.d.ts +10 -0
- package/dist/mcp/tool-result.js +33 -0
- package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-context.tool.js +25 -0
- package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-full.tool.js +27 -0
- package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
- package/dist/mcp/tools/jira-search.tool.js +31 -0
- package/dist/policy/completeness-policy.d.ts +19 -0
- package/dist/policy/completeness-policy.js +34 -0
- package/dist/policy/consistency-policy.d.ts +13 -0
- package/dist/policy/consistency-policy.js +6 -0
- package/dist/policy/field-policy.d.ts +11 -0
- package/dist/policy/field-policy.js +26 -0
- package/dist/policy/output-budget-policy.d.ts +23 -0
- package/dist/policy/output-budget-policy.js +96 -0
- package/dist/policy/pagination-policy.d.ts +15 -0
- package/dist/policy/pagination-policy.js +13 -0
- package/dist/ports/cache.port.d.ts +5 -0
- package/dist/ports/cache.port.js +1 -0
- package/dist/ports/credentials.port.d.ts +27 -0
- package/dist/ports/credentials.port.js +1 -0
- package/dist/ports/jira-read.port.d.ts +73 -0
- package/dist/ports/jira-read.port.js +1 -0
- package/dist/ports/jira-write.port.d.ts +12 -0
- package/dist/ports/jira-write.port.js +1 -0
- package/dist/ports/telemetry.port.d.ts +13 -0
- package/dist/ports/telemetry.port.js +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { writeRuntimeConfig } from "@jam-mcp/launcher";
|
|
2
|
+
import { runHealthGate } from "../bootstrap/boot-health-gate.js";
|
|
3
|
+
import { authLoginCommand } from "./auth.js";
|
|
4
|
+
import { listVisibleProjects } from "../bootstrap/jira-projects.js";
|
|
5
|
+
import { checkMigrationTarget, computeSetupPlanWithPreflight, } from "../bootstrap/migration-target.js";
|
|
6
|
+
import { LAUNCHER_PACKAGE_SPEC } from "../bootstrap/mcp-config-merger.js";
|
|
7
|
+
import { applySetupPlan } from "../bootstrap/setup-apply.js";
|
|
8
|
+
import { detectSetupState } from "../bootstrap/setup-state.js";
|
|
9
|
+
import { buildDeps } from "../deps.js";
|
|
10
|
+
import { toJamError } from "../domain/errors.js";
|
|
11
|
+
import { CancelledError, reportPromptError, Ui } from "./ui.js";
|
|
12
|
+
/**
|
|
13
|
+
* `jam setup`, the human path.
|
|
14
|
+
*
|
|
15
|
+
* Runs the same detect -> plan -> apply -> verify core the agent API uses; the
|
|
16
|
+
* only difference is presentation and the ability to ask. Anything that can be
|
|
17
|
+
* determined is determined - the wizard never asks a question it already knows
|
|
18
|
+
* the answer to, and never re-walks steps that are already done.
|
|
19
|
+
*/
|
|
20
|
+
export async function runSetupWizard(options = {}) {
|
|
21
|
+
const ui = options.ui ?? new Ui();
|
|
22
|
+
const cwd = options.cwd ?? process.cwd();
|
|
23
|
+
try {
|
|
24
|
+
let state = detectSetupState(detectOptions(options, cwd));
|
|
25
|
+
// Everything already in place: show status and offer actions rather than
|
|
26
|
+
// marching through a wizard the user has completed before.
|
|
27
|
+
if (isFullyConfigured(state)) {
|
|
28
|
+
return await runStatusMenu(ui, state, options);
|
|
29
|
+
}
|
|
30
|
+
ui.line();
|
|
31
|
+
ui.line(`${"◆"} JAM`);
|
|
32
|
+
ui.line(" Jira Agent MCP");
|
|
33
|
+
ui.line();
|
|
34
|
+
ui.line(" Configure JAM for this machine.");
|
|
35
|
+
state = await ensureRuntime(ui, state, options);
|
|
36
|
+
state = await ensureCredentials(ui, state, options);
|
|
37
|
+
const outcome = await wireProject(ui, state, options);
|
|
38
|
+
if (outcome !== 0)
|
|
39
|
+
return outcome;
|
|
40
|
+
return await verify(ui, state.project.root, options);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// Not a JAM or Jira fault - there is simply nobody to answer, or the person
|
|
44
|
+
// changed their mind. Shared with `jam auth login` so both say the same
|
|
45
|
+
// thing.
|
|
46
|
+
const code = reportPromptError(err, ui);
|
|
47
|
+
if (code === undefined)
|
|
48
|
+
throw err;
|
|
49
|
+
return code;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* "Nothing left to do" depends on the scope this workspace is actually set up
|
|
54
|
+
* in: a personal user has a binding and no repository files, and telling them
|
|
55
|
+
* their setup is incomplete because `.mcp.json` is missing would be wrong.
|
|
56
|
+
*/
|
|
57
|
+
function isFullyConfigured(state) {
|
|
58
|
+
const wired = state.mcp.hasJamEntry || Boolean(state.project.binding);
|
|
59
|
+
return (state.runtime.configured &&
|
|
60
|
+
!state.runtime.error &&
|
|
61
|
+
state.credentials.present &&
|
|
62
|
+
Boolean(state.project.key || state.project.binding) &&
|
|
63
|
+
wired);
|
|
64
|
+
}
|
|
65
|
+
async function runStatusMenu(ui, state, options) {
|
|
66
|
+
ui.line();
|
|
67
|
+
ui.line("◆ JAM");
|
|
68
|
+
ui.line();
|
|
69
|
+
ui.success("Runtime", describeRuntime(state));
|
|
70
|
+
ui.success("Authentication", `configured · ${state.credentials.source}`);
|
|
71
|
+
ui.success("Project", state.project.key ?? "");
|
|
72
|
+
ui.success("MCP", "ready");
|
|
73
|
+
ui.line();
|
|
74
|
+
ui.line("Everything is configured.");
|
|
75
|
+
if (!ui.interactive) {
|
|
76
|
+
ui.next("Run `jam doctor` to verify Jira connectivity.");
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
ui.line();
|
|
80
|
+
const action = await ui.select("What do you want to do?", [
|
|
81
|
+
{ value: "health", label: "Run health check", hint: "Verify Jira connectivity now." },
|
|
82
|
+
{ value: "runtime", label: "Change runtime", hint: "Switch between the package and a local checkout." },
|
|
83
|
+
{ value: "auth", label: "Re-authenticate", hint: "Replace the stored credentials." },
|
|
84
|
+
{ value: "repair", label: "Repair project setup", hint: "Re-apply project.yaml and .mcp.json wiring." },
|
|
85
|
+
{ value: "exit", label: "Exit", hint: "" },
|
|
86
|
+
], "Run: jam setup plan --json");
|
|
87
|
+
switch (action) {
|
|
88
|
+
case "health":
|
|
89
|
+
return verify(ui, state.project.root, options);
|
|
90
|
+
case "runtime":
|
|
91
|
+
await chooseRuntime(ui, options);
|
|
92
|
+
return 0;
|
|
93
|
+
case "auth":
|
|
94
|
+
ui.line();
|
|
95
|
+
return await authLoginCommand({ ui, ...options.auth });
|
|
96
|
+
case "repair": {
|
|
97
|
+
const plan = computeSetupPlanWithPreflight(state, planOptions(options), probe(ui));
|
|
98
|
+
const applied = applySetupPlan(plan, {
|
|
99
|
+
...(options.home ? { home: options.home } : {}),
|
|
100
|
+
...(options.runHost ? { runHost: options.runHost } : {}),
|
|
101
|
+
});
|
|
102
|
+
ui.line();
|
|
103
|
+
if (applied.changesApplied)
|
|
104
|
+
ui.success("Project wiring repaired");
|
|
105
|
+
else
|
|
106
|
+
ui.success("Nothing to repair");
|
|
107
|
+
if (reportMigrationRefused(ui, plan))
|
|
108
|
+
return 1;
|
|
109
|
+
return verify(ui, state.project.root, options);
|
|
110
|
+
}
|
|
111
|
+
case "exit":
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function ensureRuntime(ui, state, options) {
|
|
116
|
+
if (state.runtime.configured && !state.runtime.error) {
|
|
117
|
+
ui.section("Runtime");
|
|
118
|
+
ui.success("Runtime configured", describeRuntime(state));
|
|
119
|
+
return state;
|
|
120
|
+
}
|
|
121
|
+
ui.section("Runtime");
|
|
122
|
+
if (state.runtime.error) {
|
|
123
|
+
ui.warn("The configured runtime is not usable", state.runtime.error);
|
|
124
|
+
}
|
|
125
|
+
await chooseRuntime(ui, options);
|
|
126
|
+
return detectSetupState(detectOptions(options));
|
|
127
|
+
}
|
|
128
|
+
async function chooseRuntime(ui, options) {
|
|
129
|
+
// Wording is behavioural on purpose. "Package" and "development" are JAM's
|
|
130
|
+
// internal vocabulary; what a user knows is whether they are using JAM or
|
|
131
|
+
// working on it.
|
|
132
|
+
const mode = await ui.select("How will you use JAM?", [
|
|
133
|
+
{
|
|
134
|
+
value: "package",
|
|
135
|
+
label: "Use JAM",
|
|
136
|
+
hint: "Run the project-pinned package. Recommended for most users.",
|
|
137
|
+
},
|
|
138
|
+
{ value: "development", label: "Develop JAM", hint: "Run a local source checkout." },
|
|
139
|
+
], "Run: jam runtime use package");
|
|
140
|
+
if (mode === "package") {
|
|
141
|
+
writeRuntimeConfig({ version: 1, runtime: { mode: "package" } }, options.home);
|
|
142
|
+
ui.line();
|
|
143
|
+
ui.success("Runtime configured", "package");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
ui.line();
|
|
147
|
+
ui.next("Point JAM at your checkout: jam runtime use development <path>");
|
|
148
|
+
throw new CancelledError();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Report the credentials, or offer to store some - and then look again.
|
|
152
|
+
*
|
|
153
|
+
* A step, not a printout: `jam auth login` changes the machine, so continuing
|
|
154
|
+
* with the snapshot taken before it would carry `credentials.present = false`
|
|
155
|
+
* into the plan and the health gate, and the wizard would report a missing
|
|
156
|
+
* credential it had just watched the user supply.
|
|
157
|
+
*/
|
|
158
|
+
async function ensureCredentials(ui, state, options) {
|
|
159
|
+
ui.section("Authentication");
|
|
160
|
+
if (state.credentials.present) {
|
|
161
|
+
// Found and usable - stating it is enough. Asking "use these?" would be a
|
|
162
|
+
// question with one sensible answer.
|
|
163
|
+
ui.success("Jira credentials found", `${state.credentials.email} · ${state.credentials.baseUrl} (${state.credentials.source})`);
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
if (!ui.interactive) {
|
|
167
|
+
// Nobody to ask. Setup still wires the project and stops at the human step.
|
|
168
|
+
ui.warn("Jira credentials are not configured");
|
|
169
|
+
ui.line(" Run `jam auth login`, or set JIRA_BASE_URL, JIRA_EMAIL and JIRA_API_TOKEN.");
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
ui.warn("Jira credentials are not configured");
|
|
173
|
+
if ((await authLoginCommand({ ui, ...options.auth })) !== 0)
|
|
174
|
+
return state;
|
|
175
|
+
// Look again. `auth login` changed the machine, and every later step reads
|
|
176
|
+
// this snapshot - carrying the stale one forward would plan and verify
|
|
177
|
+
// against a credential the user just watched themselves supply.
|
|
178
|
+
const fresh = detectSetupState(detectOptions(options));
|
|
179
|
+
if (fresh.credentials.present) {
|
|
180
|
+
// Reported from the refreshed state on purpose: this line is what says the
|
|
181
|
+
// rest of setup is working from the new credential, and where it resolves
|
|
182
|
+
// from - which is not always the store, if an export is shadowing it.
|
|
183
|
+
ui.success("Jira credentials found", `${fresh.credentials.email} · ${fresh.credentials.baseUrl} (${fresh.credentials.source})`);
|
|
184
|
+
}
|
|
185
|
+
return fresh;
|
|
186
|
+
}
|
|
187
|
+
async function wireProject(ui, state, options) {
|
|
188
|
+
ui.section("Project");
|
|
189
|
+
const plan = computeSetupPlanWithPreflight(state, planOptions(options), probe(ui));
|
|
190
|
+
if (plan.code === "JAM_PROJECT_CONFIG_INVALID" ||
|
|
191
|
+
plan.code === "JAM_MCP_CONFIG_UNREADABLE" ||
|
|
192
|
+
plan.code === "JAM_BINDINGS_UNREADABLE") {
|
|
193
|
+
ui.failure(plan.code === "JAM_PROJECT_CONFIG_INVALID"
|
|
194
|
+
? "The project's .jira-agent/project.yaml could not be parsed"
|
|
195
|
+
: plan.code === "JAM_BINDINGS_UNREADABLE"
|
|
196
|
+
? "Your ~/.jam/projects.yaml could not be read"
|
|
197
|
+
: "The project's .mcp.json is not valid JSON");
|
|
198
|
+
ui.line(" Fix it and re-run - JAM will not overwrite it.");
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
if (plan.code === "JAM_PROJECT_SELECTION_REQUIRED") {
|
|
202
|
+
return reportSelectionRequired(ui, state, options);
|
|
203
|
+
}
|
|
204
|
+
if (plan.changes.length === 0) {
|
|
205
|
+
ui.success("Project already wired", state.project.key ?? "");
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
for (const change of applySetupPlan(plan, {
|
|
209
|
+
...(options.home ? { home: options.home } : {}),
|
|
210
|
+
...(options.runHost ? { runHost: options.runHost } : {}),
|
|
211
|
+
})
|
|
212
|
+
.applied) {
|
|
213
|
+
if (change.target === "personal-binding") {
|
|
214
|
+
ui.success(change.previousKey ? "Workspace re-bound" : "Workspace bound", change.previousKey ? `${change.previousKey} → ${change.key}` : change.key);
|
|
215
|
+
ui.line(" Recorded for you only - the repository was not touched.");
|
|
216
|
+
}
|
|
217
|
+
else if (change.target === "host-mcp") {
|
|
218
|
+
ui.success("Registered with", change.host);
|
|
219
|
+
}
|
|
220
|
+
else if (change.target === "project-config") {
|
|
221
|
+
ui.success("Project configured", `${change.key} · from ${change.keySource}`);
|
|
222
|
+
}
|
|
223
|
+
else if (change.type === "merge") {
|
|
224
|
+
ui.success("MCP entry added", change.preserveExisting.length > 0
|
|
225
|
+
? `${change.preserveExisting.length} other server(s) preserved`
|
|
226
|
+
: "");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
ui.success("MCP config written");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (reportMigrationRefused(ui, plan))
|
|
234
|
+
return 1;
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The registry probe blocks, so it gets a pending line rather than a spinner -
|
|
239
|
+
* a spinner wrapped around synchronous work prints a frame that never animates.
|
|
240
|
+
* Building it into the check means the line appears only when a probe actually
|
|
241
|
+
* happens, never on the paths that skip it.
|
|
242
|
+
*/
|
|
243
|
+
function probe(ui) {
|
|
244
|
+
return () => {
|
|
245
|
+
ui.pending(`Checking ${LAUNCHER_PACKAGE_SPEC} on npm...`);
|
|
246
|
+
return checkMigrationTarget();
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** True when a requested migration was refused, so the caller stops here. */
|
|
250
|
+
function reportMigrationRefused(ui, plan) {
|
|
251
|
+
if (plan.code !== "JAM_MIGRATION_TARGET_UNAVAILABLE")
|
|
252
|
+
return false;
|
|
253
|
+
ui.failure("Migration target is not available from the configured npm registry");
|
|
254
|
+
if (plan.migrationTarget?.detail)
|
|
255
|
+
ui.line(` ${plan.migrationTarget.detail}`);
|
|
256
|
+
ui.line(" Existing .mcp.json was left unchanged.");
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
async function reportSelectionRequired(ui, state, options) {
|
|
260
|
+
ui.failure("No Jira project key could be determined safely");
|
|
261
|
+
ui.line(` Nothing under ${state.project.root} says which Jira project this is,`);
|
|
262
|
+
ui.line(" and JAM does not guess one from a directory or repository name.");
|
|
263
|
+
// Network call, so this one earns a spinner.
|
|
264
|
+
const { projects, truncated, error } = await ui.spin("Listing Jira projects...", () => listVisibleProjects(options.credentials));
|
|
265
|
+
ui.line();
|
|
266
|
+
if (error) {
|
|
267
|
+
ui.line(` ${error}`);
|
|
268
|
+
}
|
|
269
|
+
else if (projects.length === 0) {
|
|
270
|
+
ui.line(" This Jira account cannot see any projects.");
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
ui.line(" Projects visible to this account:");
|
|
274
|
+
for (const p of projects)
|
|
275
|
+
ui.line(` ${p.key.padEnd(14)} ${p.name}`);
|
|
276
|
+
if (truncated)
|
|
277
|
+
ui.line(" ...(more not shown)");
|
|
278
|
+
}
|
|
279
|
+
ui.next("Re-run: jam setup --project <KEY>");
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
async function verify(ui, root, options) {
|
|
283
|
+
ui.section("Verify");
|
|
284
|
+
let deps;
|
|
285
|
+
try {
|
|
286
|
+
deps = await buildDeps({
|
|
287
|
+
cwd: root,
|
|
288
|
+
keyFallback: "optional",
|
|
289
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
290
|
+
...(options.jira ? { jira: options.jira } : {}),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
ui.failure("Project config", toJamError(err).message);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
const gate = await ui.spin("Checking Jira access...", () => runHealthGate(deps, "full"));
|
|
298
|
+
for (const check of gate.checks) {
|
|
299
|
+
if (check.ok)
|
|
300
|
+
ui.success(check.name, check.detail);
|
|
301
|
+
else if (check.fatal)
|
|
302
|
+
ui.failure(check.name, check.detail);
|
|
303
|
+
else
|
|
304
|
+
ui.warn(check.name, check.detail);
|
|
305
|
+
}
|
|
306
|
+
ui.line();
|
|
307
|
+
if (gate.passed) {
|
|
308
|
+
ui.success("JAM ready");
|
|
309
|
+
ui.next("Start Claude Code or Codex and use JAM.");
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
ui.failure("Setup is incomplete");
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
function describeRuntime(state) {
|
|
316
|
+
const version = state.runtime.version ? ` · ${state.runtime.version}` : "";
|
|
317
|
+
return state.runtime.mode === "development"
|
|
318
|
+
? `development · ${state.runtime.source}`
|
|
319
|
+
: `package${version}`;
|
|
320
|
+
}
|
|
321
|
+
function planOptions(options) {
|
|
322
|
+
return {
|
|
323
|
+
...(options.shared ? { shared: options.shared } : {}),
|
|
324
|
+
...(options.explicitKey ? { explicitKey: options.explicitKey } : {}),
|
|
325
|
+
...(options.migrate ? { migrate: options.migrate } : {}),
|
|
326
|
+
...(options.env ? { env: options.env } : {}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Detect options, with the test seams threaded through. */
|
|
330
|
+
function detectOptions(options, cwd) {
|
|
331
|
+
return {
|
|
332
|
+
cwd: cwd ?? options.cwd ?? process.cwd(),
|
|
333
|
+
...(options.home ? { home: options.home } : {}),
|
|
334
|
+
probeHosts: !options.shared,
|
|
335
|
+
...(options.runHost ? { runHost: options.runHost } : {}),
|
|
336
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HostRunner } from "../bootstrap/host-mcp.js";
|
|
2
|
+
export type SetupOptions = {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
/** `--project KEY` */
|
|
5
|
+
explicitKey?: string;
|
|
6
|
+
/**
|
|
7
|
+
* `--shared`: adopt JAM for the team, writing `.jira-agent/project.yaml` and
|
|
8
|
+
* `.mcp.json`. Without it setup records the binding for this user only and
|
|
9
|
+
* leaves the repository untouched.
|
|
10
|
+
*/
|
|
11
|
+
shared?: boolean;
|
|
12
|
+
/** `--migrate`: rewrite a legacy jam entry in .mcp.json. */
|
|
13
|
+
migrate?: boolean;
|
|
14
|
+
/** Injected by tests to isolate ~/.jam. */
|
|
15
|
+
home?: string;
|
|
16
|
+
/** Injected by tests so no test ever registers JAM with a real host. */
|
|
17
|
+
runHost?: HostRunner;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* `jam setup`: the one command a teammate should ever need to run by hand.
|
|
21
|
+
*
|
|
22
|
+
* Runs the same detect -> plan -> apply -> verify core the agent API uses, so
|
|
23
|
+
* a human and an agent cannot end up with different notions of what setup does
|
|
24
|
+
* or what it is allowed to touch.
|
|
25
|
+
*/
|
|
26
|
+
export declare function setup(options?: SetupOptions): Promise<number>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runHealthGate } from "../bootstrap/boot-health-gate.js";
|
|
5
|
+
import { describeHostCommand, hostRegistration, } from "../bootstrap/host-mcp.js";
|
|
6
|
+
import { listVisibleProjects } from "../bootstrap/jira-projects.js";
|
|
7
|
+
import { computeSetupPlanWithPreflight } from "../bootstrap/migration-target.js";
|
|
8
|
+
import { applySetupPlan } from "../bootstrap/setup-apply.js";
|
|
9
|
+
import { detectSetupState } from "../bootstrap/setup-state.js";
|
|
10
|
+
import { toJamError } from "../domain/errors.js";
|
|
11
|
+
import { buildDeps } from "../deps.js";
|
|
12
|
+
const line = (text) => process.stdout.write(`${text}\n`);
|
|
13
|
+
/**
|
|
14
|
+
* `jam setup`: the one command a teammate should ever need to run by hand.
|
|
15
|
+
*
|
|
16
|
+
* Runs the same detect -> plan -> apply -> verify core the agent API uses, so
|
|
17
|
+
* a human and an agent cannot end up with different notions of what setup does
|
|
18
|
+
* or what it is allowed to touch.
|
|
19
|
+
*/
|
|
20
|
+
export async function setup(options = {}) {
|
|
21
|
+
const cwd = options.cwd ?? process.cwd();
|
|
22
|
+
if (isJamCheckout(cwd)) {
|
|
23
|
+
const installed = await installAndBuild(cwd);
|
|
24
|
+
if (installed !== 0)
|
|
25
|
+
return installed;
|
|
26
|
+
}
|
|
27
|
+
const state = detectSetupState({
|
|
28
|
+
cwd,
|
|
29
|
+
...(options.home ? { home: options.home } : {}),
|
|
30
|
+
// Personal setup registers JAM with this machine's coding agents, so it
|
|
31
|
+
// has to know which of them exist. Doctor has no such need and does not
|
|
32
|
+
// pay for the probe.
|
|
33
|
+
probeHosts: !options.shared,
|
|
34
|
+
...(options.runHost ? { runHost: options.runHost } : {}),
|
|
35
|
+
});
|
|
36
|
+
const plan = computeSetupPlanWithPreflight(state, {
|
|
37
|
+
...(options.shared ? { shared: options.shared } : {}),
|
|
38
|
+
...(options.explicitKey ? { explicitKey: options.explicitKey } : {}),
|
|
39
|
+
...(options.migrate ? { migrate: options.migrate } : {}),
|
|
40
|
+
});
|
|
41
|
+
if (plan.code === "JAM_PROJECT_SELECTION_REQUIRED") {
|
|
42
|
+
return reportProjectSelectionRequired(state.project.root);
|
|
43
|
+
}
|
|
44
|
+
if (plan.code === "JAM_PROJECT_CONFIG_INVALID" ||
|
|
45
|
+
plan.code === "JAM_MCP_CONFIG_UNREADABLE" ||
|
|
46
|
+
plan.code === "JAM_BINDINGS_UNREADABLE") {
|
|
47
|
+
line(`[FAIL] ${describeBlockingCode(plan)}`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
reportApplied(applySetupPlan(plan, {
|
|
51
|
+
...(options.home ? { home: options.home } : {}),
|
|
52
|
+
...(options.runHost ? { runHost: options.runHost } : {}),
|
|
53
|
+
}).applied, plan);
|
|
54
|
+
reportBindingDisagreement(state);
|
|
55
|
+
reportUnreachableHosts(state);
|
|
56
|
+
// The rewrite the user asked for is the thing that failed, so it is reported
|
|
57
|
+
// before anything else - and reported as a stop, not a warning: continuing to
|
|
58
|
+
// the health gate would imply the migration happened.
|
|
59
|
+
if (plan.code === "JAM_MIGRATION_TARGET_UNAVAILABLE") {
|
|
60
|
+
line("");
|
|
61
|
+
line("[FAIL] Migration target is not available from the configured npm registry.");
|
|
62
|
+
line(` ${plan.migrationTarget?.detail ?? "The target could not be verified."}`);
|
|
63
|
+
line(" Existing .mcp.json was left unchanged.");
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (plan.code === "JAM_AUTH_REQUIRED") {
|
|
67
|
+
line("");
|
|
68
|
+
line("[FAIL] Jira authentication is not configured for this user.");
|
|
69
|
+
line(" Run `jam auth login`, then re-run `jam doctor`.");
|
|
70
|
+
line(" Or set JIRA_BASE_URL, JIRA_EMAIL and JIRA_API_TOKEN to override.");
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
if (plan.code === "JAM_RUNTIME_CONFIG_MISSING") {
|
|
74
|
+
line("");
|
|
75
|
+
line("[WARN] No JAM runtime is configured for this user yet.");
|
|
76
|
+
line(" Run: jam runtime use package");
|
|
77
|
+
}
|
|
78
|
+
line("\n> jam doctor\n");
|
|
79
|
+
return verify(state.project.root);
|
|
80
|
+
}
|
|
81
|
+
async function verify(root) {
|
|
82
|
+
let deps;
|
|
83
|
+
try {
|
|
84
|
+
deps = await buildDeps({ cwd: root, keyFallback: "optional" });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
line(`[FAIL] Project config - ${toJamError(err).message}`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const gate = await runHealthGate(deps, "full");
|
|
91
|
+
for (const check of gate.checks) {
|
|
92
|
+
line(`${check.ok ? "[OK] " : "[FAIL]"} ${check.name}${check.detail ? ` - ${check.detail}` : ""}`);
|
|
93
|
+
}
|
|
94
|
+
return gate.passed ? 0 : 1;
|
|
95
|
+
}
|
|
96
|
+
function reportApplied(applied, plan) {
|
|
97
|
+
if (applied.length === 0) {
|
|
98
|
+
line("[OK] Project already wired - nothing to change");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const change of applied) {
|
|
102
|
+
if (change.target === "personal-binding") {
|
|
103
|
+
const what = change.previousKey
|
|
104
|
+
? `re-bound from ${change.previousKey} to ${change.key}`
|
|
105
|
+
: `bound to ${change.key} (from ${change.keySource})`;
|
|
106
|
+
line(`[OK] ${change.path} - this workspace ${what}`);
|
|
107
|
+
line(" Nothing was written to the repository. Use --shared to adopt JAM for the team.");
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (change.target === "host-mcp") {
|
|
111
|
+
line(`[OK] ${change.host} - jam registered for this user`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (change.target === "project-config") {
|
|
115
|
+
line(`[OK] ${change.path} created - project.key = ${change.key} (from ${change.keySource})`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (change.type === "create") {
|
|
119
|
+
line(`[OK] ${change.path} - created`);
|
|
120
|
+
}
|
|
121
|
+
else if (change.type === "merge") {
|
|
122
|
+
const preserved = change.preserveExisting.length;
|
|
123
|
+
line(`[OK] ${change.path} - added jam entry` +
|
|
124
|
+
(preserved > 0 ? ` (${preserved} other MCP server(s) preserved)` : ""));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
line(`[OK] ${change.path} - migrated jam entry`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
void plan;
|
|
131
|
+
}
|
|
132
|
+
function describeBlockingCode(plan) {
|
|
133
|
+
if (plan.code === "JAM_PROJECT_CONFIG_INVALID") {
|
|
134
|
+
return "The project's .jira-agent/project.yaml could not be parsed. Fix it and re-run.";
|
|
135
|
+
}
|
|
136
|
+
if (plan.code === "JAM_BINDINGS_UNREADABLE") {
|
|
137
|
+
return "Your ~/.jam/projects.yaml could not be read. Fix or remove it and re-run - JAM will not overwrite it.";
|
|
138
|
+
}
|
|
139
|
+
return "The project's .mcp.json is not valid JSON. Fix it and re-run - JAM will not overwrite it.";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A host JAM could not reach is reported, never guessed at: the command is
|
|
143
|
+
* printed so the person can run it, and nothing is claimed to have happened.
|
|
144
|
+
*/
|
|
145
|
+
function reportUnreachableHosts(state) {
|
|
146
|
+
for (const host of state.hosts) {
|
|
147
|
+
if (host.cliAvailable)
|
|
148
|
+
continue;
|
|
149
|
+
const registration = hostRegistration(host.id);
|
|
150
|
+
if (!registration)
|
|
151
|
+
continue;
|
|
152
|
+
line(`[WARN] ${host.id} was not reachable, so JAM was not registered with it.`);
|
|
153
|
+
line(` If you use it, run: ${describeHostCommand(registration)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A committed project key and a personal binding can disagree - normally
|
|
158
|
+
* because the team adopted JAM after someone bound the repo. The file wins,
|
|
159
|
+
* and saying so is better than silently ignoring one of them or deleting the
|
|
160
|
+
* other on the user's behalf.
|
|
161
|
+
*/
|
|
162
|
+
function reportBindingDisagreement(state) {
|
|
163
|
+
const bound = state.project.binding?.key;
|
|
164
|
+
if (!bound || !state.project.key || bound === state.project.key)
|
|
165
|
+
return;
|
|
166
|
+
line(`[WARN] Personal binding says ${bound}; ${state.project.configPath ?? "the project config"} says ${state.project.key} - the project file wins.`);
|
|
167
|
+
}
|
|
168
|
+
/** The JAM monorepo checkout itself, where setup should also install and build. */
|
|
169
|
+
function isJamCheckout(cwd) {
|
|
170
|
+
return (existsSync(join(cwd, "package.json")) &&
|
|
171
|
+
existsSync(join(cwd, "packages", "server", "src", "index.ts")));
|
|
172
|
+
}
|
|
173
|
+
async function installAndBuild(root) {
|
|
174
|
+
for (const step of [
|
|
175
|
+
{ name: "Install dependencies", args: ["ci"] },
|
|
176
|
+
{ name: "Build", args: ["run", "build"] },
|
|
177
|
+
]) {
|
|
178
|
+
line(`\n> npm ${step.args.join(" ")}`);
|
|
179
|
+
const res = spawnSync("npm", step.args, {
|
|
180
|
+
cwd: root,
|
|
181
|
+
stdio: "inherit",
|
|
182
|
+
shell: process.platform === "win32",
|
|
183
|
+
});
|
|
184
|
+
if (res.status !== 0) {
|
|
185
|
+
line(`[FAIL] ${step.name}`);
|
|
186
|
+
return res.status ?? 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* No config, and no explicit/env/preset source to decide one from. JAM never
|
|
193
|
+
* guesses a Jira project from a repo or folder name - it shows the operator
|
|
194
|
+
* their options and asks them to say which one, once.
|
|
195
|
+
*/
|
|
196
|
+
async function reportProjectSelectionRequired(root) {
|
|
197
|
+
line(`[FAIL] No .jira-agent/project.yaml under ${root}, and no project key could be determined safely.`);
|
|
198
|
+
const { projects, truncated, error } = await listVisibleProjects();
|
|
199
|
+
if (error) {
|
|
200
|
+
line(` ${error}`);
|
|
201
|
+
}
|
|
202
|
+
else if (projects.length === 0) {
|
|
203
|
+
line(" This Jira account cannot see any projects.");
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
line(" Projects visible to this account:");
|
|
207
|
+
for (const p of projects)
|
|
208
|
+
line(` ${p.key} ${p.name}`);
|
|
209
|
+
if (truncated)
|
|
210
|
+
line(" ...(more not shown)");
|
|
211
|
+
}
|
|
212
|
+
line(" Re-run: jam setup --project <KEY>");
|
|
213
|
+
return 1;
|
|
214
|
+
}
|