@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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/adapters/cache/noop-cache.d.ts +7 -0
  4. package/dist/adapters/cache/noop-cache.js +12 -0
  5. package/dist/adapters/credentials/composite.d.ts +30 -0
  6. package/dist/adapters/credentials/composite.js +79 -0
  7. package/dist/adapters/credentials/process-env.d.ts +13 -0
  8. package/dist/adapters/credentials/process-env.js +17 -0
  9. package/dist/adapters/credentials/secret-store.d.ts +69 -0
  10. package/dist/adapters/credentials/secret-store.js +259 -0
  11. package/dist/adapters/credentials/windows-user-env.d.ts +20 -0
  12. package/dist/adapters/credentials/windows-user-env.js +51 -0
  13. package/dist/adapters/jira-cloud/adf-to-text.d.ts +12 -0
  14. package/dist/adapters/jira-cloud/adf-to-text.js +151 -0
  15. package/dist/adapters/jira-cloud/jira-client.d.ts +32 -0
  16. package/dist/adapters/jira-cloud/jira-client.js +137 -0
  17. package/dist/adapters/jira-cloud/jira-read.adapter.d.ts +13 -0
  18. package/dist/adapters/jira-cloud/jira-read.adapter.js +97 -0
  19. package/dist/adapters/jira-cloud/jira-write.adapter.d.ts +12 -0
  20. package/dist/adapters/jira-cloud/jira-write.adapter.js +17 -0
  21. package/dist/adapters/jira-cloud/mapper.d.ts +34 -0
  22. package/dist/adapters/jira-cloud/mapper.js +143 -0
  23. package/dist/adapters/telemetry/console-telemetry.d.ts +13 -0
  24. package/dist/adapters/telemetry/console-telemetry.js +31 -0
  25. package/dist/application/get-full-issue-context.d.ts +18 -0
  26. package/dist/application/get-full-issue-context.js +120 -0
  27. package/dist/application/get-issue-context.d.ts +19 -0
  28. package/dist/application/get-issue-context.js +79 -0
  29. package/dist/application/search-issues.d.ts +22 -0
  30. package/dist/application/search-issues.js +93 -0
  31. package/dist/bootstrap/boot-health-gate.d.ts +25 -0
  32. package/dist/bootstrap/boot-health-gate.js +137 -0
  33. package/dist/bootstrap/bootstrap-orchestrator.d.ts +15 -0
  34. package/dist/bootstrap/bootstrap-orchestrator.js +14 -0
  35. package/dist/bootstrap/host-mcp.d.ts +54 -0
  36. package/dist/bootstrap/host-mcp.js +98 -0
  37. package/dist/bootstrap/jira-projects.d.ts +16 -0
  38. package/dist/bootstrap/jira-projects.js +24 -0
  39. package/dist/bootstrap/mcp-config-merger.d.ts +58 -0
  40. package/dist/bootstrap/mcp-config-merger.js +119 -0
  41. package/dist/bootstrap/migration-target.d.ts +60 -0
  42. package/dist/bootstrap/migration-target.js +80 -0
  43. package/dist/bootstrap/project-bindings.d.ts +48 -0
  44. package/dist/bootstrap/project-bindings.js +112 -0
  45. package/dist/bootstrap/project-config-bootstrapper.d.ts +37 -0
  46. package/dist/bootstrap/project-config-bootstrapper.js +69 -0
  47. package/dist/bootstrap/project-config-resolver.d.ts +53 -0
  48. package/dist/bootstrap/project-config-resolver.js +59 -0
  49. package/dist/bootstrap/project-root-resolver.d.ts +22 -0
  50. package/dist/bootstrap/project-root-resolver.js +43 -0
  51. package/dist/bootstrap/setup-apply.d.ts +31 -0
  52. package/dist/bootstrap/setup-apply.js +61 -0
  53. package/dist/bootstrap/setup-plan.d.ts +102 -0
  54. package/dist/bootstrap/setup-plan.js +224 -0
  55. package/dist/bootstrap/setup-state.d.ts +73 -0
  56. package/dist/bootstrap/setup-state.js +89 -0
  57. package/dist/bootstrap/workspace-identity.d.ts +52 -0
  58. package/dist/bootstrap/workspace-identity.js +114 -0
  59. package/dist/cli/agent-api.d.ts +63 -0
  60. package/dist/cli/agent-api.js +149 -0
  61. package/dist/cli/auth.d.ts +39 -0
  62. package/dist/cli/auth.js +201 -0
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.js +31 -0
  65. package/dist/cli/runtime.d.ts +16 -0
  66. package/dist/cli/runtime.js +97 -0
  67. package/dist/cli/serve.d.ts +7 -0
  68. package/dist/cli/serve.js +23 -0
  69. package/dist/cli/setup-wizard.d.ts +37 -0
  70. package/dist/cli/setup-wizard.js +338 -0
  71. package/dist/cli/setup.d.ts +26 -0
  72. package/dist/cli/setup.js +214 -0
  73. package/dist/cli/ui.d.ts +118 -0
  74. package/dist/cli/ui.js +338 -0
  75. package/dist/cli-entry.d.ts +7 -0
  76. package/dist/cli-entry.js +124 -0
  77. package/dist/config/load-config.d.ts +18 -0
  78. package/dist/config/load-config.js +48 -0
  79. package/dist/config/schema.d.ts +36 -0
  80. package/dist/config/schema.js +74 -0
  81. package/dist/deps.d.ts +50 -0
  82. package/dist/deps.js +39 -0
  83. package/dist/domain/completeness.d.ts +63 -0
  84. package/dist/domain/completeness.js +20 -0
  85. package/dist/domain/context.d.ts +38 -0
  86. package/dist/domain/context.js +1 -0
  87. package/dist/domain/errors.d.ts +27 -0
  88. package/dist/domain/errors.js +49 -0
  89. package/dist/domain/issue.d.ts +35 -0
  90. package/dist/domain/issue.js +1 -0
  91. package/dist/index.d.ts +2 -0
  92. package/dist/index.js +17 -0
  93. package/dist/mcp/create-server.d.ts +9 -0
  94. package/dist/mcp/create-server.js +32 -0
  95. package/dist/mcp/tool-result.d.ts +10 -0
  96. package/dist/mcp/tool-result.js +33 -0
  97. package/dist/mcp/tools/jira-context.tool.d.ts +3 -0
  98. package/dist/mcp/tools/jira-context.tool.js +25 -0
  99. package/dist/mcp/tools/jira-full.tool.d.ts +3 -0
  100. package/dist/mcp/tools/jira-full.tool.js +27 -0
  101. package/dist/mcp/tools/jira-search.tool.d.ts +3 -0
  102. package/dist/mcp/tools/jira-search.tool.js +31 -0
  103. package/dist/policy/completeness-policy.d.ts +19 -0
  104. package/dist/policy/completeness-policy.js +34 -0
  105. package/dist/policy/consistency-policy.d.ts +13 -0
  106. package/dist/policy/consistency-policy.js +6 -0
  107. package/dist/policy/field-policy.d.ts +11 -0
  108. package/dist/policy/field-policy.js +26 -0
  109. package/dist/policy/output-budget-policy.d.ts +23 -0
  110. package/dist/policy/output-budget-policy.js +96 -0
  111. package/dist/policy/pagination-policy.d.ts +15 -0
  112. package/dist/policy/pagination-policy.js +13 -0
  113. package/dist/ports/cache.port.d.ts +5 -0
  114. package/dist/ports/cache.port.js +1 -0
  115. package/dist/ports/credentials.port.d.ts +27 -0
  116. package/dist/ports/credentials.port.js +1 -0
  117. package/dist/ports/jira-read.port.d.ts +73 -0
  118. package/dist/ports/jira-read.port.js +1 -0
  119. package/dist/ports/jira-write.port.d.ts +12 -0
  120. package/dist/ports/jira-write.port.js +1 -0
  121. package/dist/ports/telemetry.port.d.ts +13 -0
  122. package/dist/ports/telemetry.port.js +1 -0
  123. package/package.json +69 -0
@@ -0,0 +1,73 @@
1
+ import { type RuntimeMode } from "@jam-mcp/launcher";
2
+ import type { CredentialPort, CredentialSource } from "../ports/credentials.port.js";
3
+ import { type HostRunner, type HostState } from "./host-mcp.js";
4
+ import { type McpInspection } from "./mcp-config-merger.js";
5
+ import { type ProjectBinding } from "./project-bindings.js";
6
+ import { type GitRemoteFn } from "./workspace-identity.js";
7
+ export type RuntimeState = {
8
+ configured: boolean;
9
+ mode?: RuntimeMode;
10
+ source?: string;
11
+ version?: string;
12
+ /** Set when a runtime is configured but cannot currently be resolved. */
13
+ error?: string;
14
+ };
15
+ export type CredentialState = {
16
+ present: boolean;
17
+ source: CredentialSource;
18
+ baseUrl?: string;
19
+ email?: string;
20
+ };
21
+ export type ProjectState = {
22
+ root: string;
23
+ hasConfig: boolean;
24
+ configPath?: string;
25
+ key?: string;
26
+ /** Set when a config file exists but could not be parsed. */
27
+ error?: string;
28
+ /** What this user has bound this workspace to, if anything. */
29
+ binding?: ProjectBinding;
30
+ };
31
+ export type SetupState = {
32
+ cwd: string;
33
+ /** Stable id for this workspace - what a personal binding is recorded against. */
34
+ workspaceId: string;
35
+ /**
36
+ * True when `~/.jam/projects.yaml` exists but could not be read. Recorded
37
+ * rather than thrown so planning can refuse to rewrite it, instead of
38
+ * discovering the problem halfway through applying.
39
+ */
40
+ bindingsUnreadable: boolean;
41
+ runtime: RuntimeState;
42
+ credentials: CredentialState;
43
+ project: ProjectState;
44
+ mcp: McpInspection & {
45
+ jamEntryIsLegacy: boolean;
46
+ };
47
+ /**
48
+ * The coding agents on this machine and whether each already knows about
49
+ * jam. Empty unless probing was asked for: it costs a process launch per
50
+ * host, which `jam doctor` has no reason to pay.
51
+ */
52
+ hosts: HostState[];
53
+ };
54
+ export type DetectOptions = {
55
+ cwd?: string;
56
+ /** Injected by tests to isolate ~/.jam. */
57
+ home?: string;
58
+ credentials?: CredentialPort;
59
+ /** Injected by tests so identity never depends on the checkout under test. */
60
+ git?: GitRemoteFn;
61
+ /** Probe the host CLIs. Only the setup paths need this. */
62
+ probeHosts?: boolean;
63
+ /** Injected by tests so no test ever reaches a real host CLI. */
64
+ runHost?: HostRunner;
65
+ };
66
+ /**
67
+ * Snapshot everything setup needs to decide, without changing any of it.
68
+ *
69
+ * Strictly read-only: no file is created, and no config is bootstrapped. That
70
+ * separation is what makes `setup plan` safe to run speculatively, and what
71
+ * lets an agent inspect a machine before proposing anything.
72
+ */
73
+ export declare function detectSetupState(options?: DetectOptions): SetupState;
@@ -0,0 +1,89 @@
1
+ import { readRuntimeConfig, resolveRuntime, } from "@jam-mcp/launcher";
2
+ import { CompositeCredentialProvider } from "../adapters/credentials/composite.js";
3
+ import { loadConfig } from "../config/load-config.js";
4
+ import { detectHosts } from "./host-mcp.js";
5
+ import { inspectMcpConfig, isLegacyJamEntry } from "./mcp-config-merger.js";
6
+ import { inspectProjectBindings } from "./project-bindings.js";
7
+ import { resolveProjectRoot } from "./project-root-resolver.js";
8
+ import { workspaceIdentity } from "./workspace-identity.js";
9
+ /**
10
+ * Snapshot everything setup needs to decide, without changing any of it.
11
+ *
12
+ * Strictly read-only: no file is created, and no config is bootstrapped. That
13
+ * separation is what makes `setup plan` safe to run speculatively, and what
14
+ * lets an agent inspect a machine before proposing anything.
15
+ */
16
+ export function detectSetupState(options = {}) {
17
+ const cwd = options.cwd ?? process.cwd();
18
+ const located = resolveProjectRoot(cwd);
19
+ const workspaceId = workspaceIdentity(located.root, {
20
+ ...(located.gitRoot ? { gitRoot: located.gitRoot } : {}),
21
+ ...(options.git ? { git: options.git } : {}),
22
+ });
23
+ const bindings = inspectProjectBindings(options.home);
24
+ const binding = bindings.bindings.find((b) => b.workspace === workspaceId);
25
+ return {
26
+ cwd,
27
+ workspaceId,
28
+ bindingsUnreadable: bindings.status === "unreadable",
29
+ runtime: detectRuntime(options.home),
30
+ credentials: detectCredentials(options.credentials ?? new CompositeCredentialProvider()),
31
+ project: { ...detectProject(located), ...(binding ? { binding } : {}) },
32
+ mcp: detectMcp(located.root),
33
+ hosts: options.probeHosts ? detectHosts(options.runHost) : [],
34
+ };
35
+ }
36
+ function detectRuntime(home) {
37
+ const config = readRuntimeConfig(home);
38
+ if (!config)
39
+ return { configured: false };
40
+ const state = { configured: true, mode: config.runtime.mode };
41
+ if (config.runtime.mode === "development")
42
+ state.source = config.runtime.source;
43
+ try {
44
+ state.version = resolveRuntime(config).version;
45
+ }
46
+ catch (err) {
47
+ state.error = err instanceof Error ? err.message : String(err);
48
+ }
49
+ return state;
50
+ }
51
+ function detectCredentials(credentials) {
52
+ const described = credentials.describe();
53
+ const state = {
54
+ present: Boolean(described.baseUrl && described.email && described.hasToken),
55
+ source: described.source,
56
+ };
57
+ // Presence and origin only - the token value never enters this snapshot.
58
+ if (described.baseUrl)
59
+ state.baseUrl = described.baseUrl;
60
+ if (described.email)
61
+ state.email = described.email;
62
+ return state;
63
+ }
64
+ function detectProject(located) {
65
+ const { root, hasConfig } = located;
66
+ if (!hasConfig)
67
+ return { root, hasConfig: false };
68
+ try {
69
+ const loaded = loadConfig(root);
70
+ const state = { root, hasConfig: true };
71
+ if (loaded.path)
72
+ state.configPath = loaded.path;
73
+ if (loaded.config.project.key)
74
+ state.key = loaded.config.project.key;
75
+ return state;
76
+ }
77
+ catch (err) {
78
+ return {
79
+ root,
80
+ hasConfig: true,
81
+ error: err instanceof Error ? err.message : String(err),
82
+ };
83
+ }
84
+ }
85
+ /** Inspection plus whether the existing jam entry predates launcher-based wiring. */
86
+ function detectMcp(root) {
87
+ const inspection = inspectMcpConfig(root);
88
+ return { ...inspection, jamEntryIsLegacy: isLegacyJamEntry(inspection.jamEntry) };
89
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reads a repository's `origin` remote. Injected by tests so no test depends
3
+ * on the checkout it happens to run in.
4
+ */
5
+ export type GitRemoteFn = (root: string) => string | undefined;
6
+ /**
7
+ * Ask git for `origin`, and treat every failure as "no remote".
8
+ *
9
+ * git missing, not a repository, no `origin`, a stalled filesystem - all of
10
+ * them mean the same thing to a caller, and none of them is worth an error or
11
+ * a log line. `shell: false` unlike the npm probe in `migration-target.ts`:
12
+ * git is a real executable, so routing it through a shell would only add
13
+ * quoting hazards around the user's path.
14
+ */
15
+ export declare const readGitRemote: GitRemoteFn;
16
+ /**
17
+ * One repository, one identity, whichever URL form was cloned.
18
+ *
19
+ * `git@host:org/repo.git` and `https://host/org/repo` are the same repository
20
+ * and must produce the same string, or a binding made from one clone would be
21
+ * invisible from another.
22
+ *
23
+ * Two rules are load-bearing rather than cosmetic:
24
+ *
25
+ * - **Any userinfo is dropped.** A remote can carry `user:token@`, and a token
26
+ * must never reach `~/.jam/projects.yaml`, a log line or telemetry.
27
+ * - **An explicit non-default port is kept.** `git.example.com:8443/org/repo`
28
+ * and `git.example.com/org/repo` may be different services - self-hosted
29
+ * GitLab makes that ordinary - so only a port that is the scheme's own
30
+ * default is dropped.
31
+ */
32
+ export declare function canonicalRemote(url: string): string | undefined;
33
+ export type WorkspaceIdentityOptions = {
34
+ /** The repository root, when `root` is inside one. */
35
+ gitRoot?: string;
36
+ /** Injected by tests. */
37
+ git?: GitRemoteFn;
38
+ };
39
+ /**
40
+ * A stable id for "this workspace", used to bind it to a Jira project.
41
+ *
42
+ * Prefers the canonical remote plus the project's offset inside the
43
+ * repository, so two clones of one repo share a binding and two packages in a
44
+ * monorepo do not. Falls back to the normalised absolute path when there is no
45
+ * remote to ask about - which is honest about its ceiling: a folder that moves
46
+ * loses its binding.
47
+ *
48
+ * No realpath: the preset matcher already compares non-realpathed absolute
49
+ * paths, and resolving symlinks only here would create a case where a preset
50
+ * matches and a binding does not.
51
+ */
52
+ export declare function workspaceIdentity(root: string, options?: WorkspaceIdentityOptions): string;
@@ -0,0 +1,114 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { relative } from "node:path";
4
+ import { normalizePath } from "./project-config-bootstrapper.js";
5
+ /** Local file read, not a network call - a second is already generous. */
6
+ const GIT_TIMEOUT_MS = 2_000;
7
+ /**
8
+ * Ask git for `origin`, and treat every failure as "no remote".
9
+ *
10
+ * git missing, not a repository, no `origin`, a stalled filesystem - all of
11
+ * them mean the same thing to a caller, and none of them is worth an error or
12
+ * a log line. `shell: false` unlike the npm probe in `migration-target.ts`:
13
+ * git is a real executable, so routing it through a shell would only add
14
+ * quoting hazards around the user's path.
15
+ */
16
+ export const readGitRemote = (root) => {
17
+ const result = spawnSync("git", ["config", "--get", "remote.origin.url"], {
18
+ cwd: root,
19
+ encoding: "utf8",
20
+ timeout: GIT_TIMEOUT_MS,
21
+ shell: false,
22
+ });
23
+ if (result.error || result.status !== 0)
24
+ return undefined;
25
+ const url = result.stdout?.trim();
26
+ return url ? url : undefined;
27
+ };
28
+ const DEFAULT_PORTS = {
29
+ http: "80",
30
+ https: "443",
31
+ ssh: "22",
32
+ git: "9418",
33
+ };
34
+ /**
35
+ * One repository, one identity, whichever URL form was cloned.
36
+ *
37
+ * `git@host:org/repo.git` and `https://host/org/repo` are the same repository
38
+ * and must produce the same string, or a binding made from one clone would be
39
+ * invisible from another.
40
+ *
41
+ * Two rules are load-bearing rather than cosmetic:
42
+ *
43
+ * - **Any userinfo is dropped.** A remote can carry `user:token@`, and a token
44
+ * must never reach `~/.jam/projects.yaml`, a log line or telemetry.
45
+ * - **An explicit non-default port is kept.** `git.example.com:8443/org/repo`
46
+ * and `git.example.com/org/repo` may be different services - self-hosted
47
+ * GitLab makes that ordinary - so only a port that is the scheme's own
48
+ * default is dropped.
49
+ */
50
+ export function canonicalRemote(url) {
51
+ const trimmed = url.trim();
52
+ if (!trimmed)
53
+ return undefined;
54
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed)) {
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(trimmed);
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ const host = parsed.hostname.toLowerCase();
63
+ if (!host)
64
+ return undefined;
65
+ const scheme = parsed.protocol.replace(":", "").toLowerCase();
66
+ // WHATWG already drops http/https default ports; this covers ssh and git.
67
+ const port = parsed.port && parsed.port !== DEFAULT_PORTS[scheme] ? `:${parsed.port}` : "";
68
+ const path = cleanPath(parsed.pathname);
69
+ return path ? `${host}${port}/${path}` : undefined;
70
+ }
71
+ // scp-like: [user@]host:path. The text after the colon is a path, never a
72
+ // port - reading it as one would turn org/repo into a port number.
73
+ const scp = /^(?:[^@/]+@)?([^:/]+):(.+)$/.exec(trimmed);
74
+ if (scp) {
75
+ const host = scp[1].toLowerCase();
76
+ const path = cleanPath(scp[2]);
77
+ return path ? `${host}/${path}` : undefined;
78
+ }
79
+ return undefined;
80
+ }
81
+ function cleanPath(raw) {
82
+ return raw
83
+ .replace(/^\/+/, "")
84
+ .replace(/\/+$/, "")
85
+ .replace(/\.git$/i, "")
86
+ .toLowerCase();
87
+ }
88
+ /**
89
+ * A stable id for "this workspace", used to bind it to a Jira project.
90
+ *
91
+ * Prefers the canonical remote plus the project's offset inside the
92
+ * repository, so two clones of one repo share a binding and two packages in a
93
+ * monorepo do not. Falls back to the normalised absolute path when there is no
94
+ * remote to ask about - which is honest about its ceiling: a folder that moves
95
+ * loses its binding.
96
+ *
97
+ * No realpath: the preset matcher already compares non-realpathed absolute
98
+ * paths, and resolving symlinks only here would create a case where a preset
99
+ * matches and a binding does not.
100
+ */
101
+ export function workspaceIdentity(root, options = {}) {
102
+ const git = options.git ?? readGitRemote;
103
+ const gitRoot = options.gitRoot;
104
+ const remote = gitRoot ? canonicalRemote(git(gitRoot) ?? "") : undefined;
105
+ if (remote) {
106
+ const offset = toPosix(relative(gitRoot, root));
107
+ return offset ? `git:${remote}#${offset}` : `git:${remote}`;
108
+ }
109
+ return `path:${toPosix(normalizePath(root))}`;
110
+ }
111
+ function toPosix(p) {
112
+ const forward = p.split("\\").join("/");
113
+ return platform() === "win32" ? forward.toLowerCase() : forward;
114
+ }
@@ -0,0 +1,63 @@
1
+ import { type MigrationTarget } from "../bootstrap/migration-target.js";
2
+ import type { CredentialPort } from "../ports/credentials.port.js";
3
+ import type { HostRunner } from "../bootstrap/host-mcp.js";
4
+ import type { GitRemoteFn } from "../bootstrap/workspace-identity.js";
5
+ /**
6
+ * The machine-readable half of setup.
7
+ *
8
+ * Everything here shares the detect/plan/apply core with the human wizard -
9
+ * only the interface differs. An agent gets structured status codes instead of
10
+ * prose so it never has to infer intent from an error message, and never has
11
+ * to assemble JAM's config files itself.
12
+ *
13
+ * Contract, enforced by tests:
14
+ * stdout - valid JSON and nothing else, no ANSI, no prompts
15
+ * stderr - diagnostics only
16
+ */
17
+ export type AgentOptions = {
18
+ cwd?: string;
19
+ home?: string;
20
+ explicitKey?: string;
21
+ /** `--shared`: adopt JAM for the team, writing into the repository. */
22
+ shared?: boolean;
23
+ migrate?: boolean;
24
+ /** Injected by tests so a plan never depends on the machine's JIRA_* env. */
25
+ credentials?: CredentialPort;
26
+ /** Injected by tests so a plan never depends on the machine's JAM_PROJECT_KEY. */
27
+ env?: NodeJS.ProcessEnv;
28
+ /** Injected by tests so a plan never shells out to npm to verify a migration target. */
29
+ migrationTarget?: MigrationTarget;
30
+ /** Injected by tests so identity never depends on the checkout under test. */
31
+ git?: GitRemoteFn;
32
+ /** Injected by tests so no test ever registers JAM with a real host. */
33
+ runHost?: HostRunner;
34
+ };
35
+ export declare function emitJson(payload: unknown): void;
36
+ /**
37
+ * `jam setup plan --json` - what setup would do, having done nothing.
38
+ */
39
+ export declare function setupPlanCommand(options?: AgentOptions): Promise<number>;
40
+ /**
41
+ * `jam setup apply --non-interactive --json` - execute a freshly computed plan.
42
+ *
43
+ * Applies whatever the plan safely can even when a human step remains, and
44
+ * says so via `changesApplied`. A missing credential should not leave a
45
+ * project half-wired.
46
+ */
47
+ export declare function setupApplyCommand(options?: AgentOptions): Promise<number>;
48
+ /**
49
+ * `jam setup --agent` - one shot: detect, plan, apply what is safe, verify.
50
+ *
51
+ * Stops only where a person is genuinely required (choosing a Jira project,
52
+ * authenticating), and reports exactly how far it got.
53
+ */
54
+ export declare function setupAgentCommand(options?: AgentOptions): Promise<number>;
55
+ /** `jam doctor --json`. */
56
+ export declare function doctorJsonCommand(options?: AgentOptions): Promise<number>;
57
+ /**
58
+ * `jam auth status --json` - presence and origin only.
59
+ *
60
+ * Never returns the credential itself. An agent needs to know whether
61
+ * authentication is configured, not what it is.
62
+ */
63
+ export declare function authStatusCommand(options?: AgentOptions): number;
@@ -0,0 +1,149 @@
1
+ import { runHealthGate } from "../bootstrap/boot-health-gate.js";
2
+ import { listVisibleProjects } from "../bootstrap/jira-projects.js";
3
+ import { computeSetupPlanWithPreflight, } from "../bootstrap/migration-target.js";
4
+ import { applySetupPlan } from "../bootstrap/setup-apply.js";
5
+ import { detectSetupState } from "../bootstrap/setup-state.js";
6
+ import { buildDeps } from "../deps.js";
7
+ import { toJamError } from "../domain/errors.js";
8
+ export function emitJson(payload) {
9
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
10
+ }
11
+ function detect(options) {
12
+ return detectSetupState({
13
+ ...(options.cwd ? { cwd: options.cwd } : {}),
14
+ ...(options.home ? { home: options.home } : {}),
15
+ ...(options.credentials ? { credentials: options.credentials } : {}),
16
+ ...(options.git ? { git: options.git } : {}),
17
+ probeHosts: !options.shared,
18
+ ...(options.runHost ? { runHost: options.runHost } : {}),
19
+ });
20
+ }
21
+ function planFrom(state, options) {
22
+ return computeSetupPlanWithPreflight(state, {
23
+ ...(options.shared ? { shared: options.shared } : {}),
24
+ ...(options.explicitKey ? { explicitKey: options.explicitKey } : {}),
25
+ ...(options.migrate ? { migrate: options.migrate } : {}),
26
+ ...(options.env ? { env: options.env } : {}),
27
+ ...(options.migrationTarget ? { migrationTarget: options.migrationTarget } : {}),
28
+ });
29
+ }
30
+ /** Enrich a selection-required plan with the projects the account can see. */
31
+ async function withProjects(plan, options) {
32
+ if (plan.code !== "JAM_PROJECT_SELECTION_REQUIRED")
33
+ return plan;
34
+ const { projects } = await listVisibleProjects(options.credentials);
35
+ return projects.length > 0 ? { ...plan, projects } : plan;
36
+ }
37
+ /**
38
+ * `jam setup plan --json` - what setup would do, having done nothing.
39
+ */
40
+ export async function setupPlanCommand(options = {}) {
41
+ const plan = await withProjects(planFrom(detect(options), options), options);
42
+ emitJson({ ...plan, changesApplied: false });
43
+ return plan.requiresUserAction ? 1 : 0;
44
+ }
45
+ /**
46
+ * `jam setup apply --non-interactive --json` - execute a freshly computed plan.
47
+ *
48
+ * Applies whatever the plan safely can even when a human step remains, and
49
+ * says so via `changesApplied`. A missing credential should not leave a
50
+ * project half-wired.
51
+ */
52
+ export async function setupApplyCommand(options = {}) {
53
+ const state = detect(options);
54
+ const plan = planFrom(state, options);
55
+ if (plan.code === "JAM_PROJECT_SELECTION_REQUIRED") {
56
+ emitJson({ ...(await withProjects(plan, options)), changesApplied: false });
57
+ return 1;
58
+ }
59
+ if (plan.code === "JAM_PROJECT_CONFIG_INVALID" ||
60
+ plan.code === "JAM_MCP_CONFIG_UNREADABLE" ||
61
+ plan.code === "JAM_BINDINGS_UNREADABLE") {
62
+ emitJson({ ...plan, changesApplied: false });
63
+ return 1;
64
+ }
65
+ const result = applySetupPlan(plan, {
66
+ ...(options.home ? { home: options.home } : {}),
67
+ ...(options.runHost ? { runHost: options.runHost } : {}),
68
+ });
69
+ emitJson({ ...plan, status: applyStatus(plan), changesApplied: result.changesApplied });
70
+ return plan.requiresUserAction ? 1 : 0;
71
+ }
72
+ function applyStatus(plan) {
73
+ return plan.requiresUserAction ? "user_action_required" : "already_configured";
74
+ }
75
+ /**
76
+ * `jam setup --agent` - one shot: detect, plan, apply what is safe, verify.
77
+ *
78
+ * Stops only where a person is genuinely required (choosing a Jira project,
79
+ * authenticating), and reports exactly how far it got.
80
+ */
81
+ export async function setupAgentCommand(options = {}) {
82
+ const state = detect(options);
83
+ const plan = planFrom(state, options);
84
+ if (plan.code === "JAM_PROJECT_SELECTION_REQUIRED") {
85
+ emitJson({ ...(await withProjects(plan, options)), changesApplied: false });
86
+ return 1;
87
+ }
88
+ if (plan.code === "JAM_PROJECT_CONFIG_INVALID" ||
89
+ plan.code === "JAM_MCP_CONFIG_UNREADABLE" ||
90
+ plan.code === "JAM_BINDINGS_UNREADABLE") {
91
+ emitJson({ ...plan, changesApplied: false });
92
+ return 1;
93
+ }
94
+ const { changesApplied } = applySetupPlan(plan, {
95
+ ...(options.home ? { home: options.home } : {}),
96
+ ...(options.runHost ? { runHost: options.runHost } : {}),
97
+ });
98
+ if (plan.requiresUserAction) {
99
+ emitJson({ ...plan, changesApplied });
100
+ return 1;
101
+ }
102
+ const health = await gateResult(state.project.root);
103
+ emitJson({
104
+ status: health.passed ? "ready" : "verification_failed",
105
+ changesApplied,
106
+ project: plan.project,
107
+ checks: health.checks,
108
+ });
109
+ return health.passed ? 0 : 1;
110
+ }
111
+ /** `jam doctor --json`. */
112
+ export async function doctorJsonCommand(options = {}) {
113
+ const state = detect(options);
114
+ const health = await gateResult(state.project.root);
115
+ emitJson({
116
+ status: health.passed ? "ready" : "failed",
117
+ ...(health.error ? { error: health.error } : {}),
118
+ project: { root: state.project.root, ...(state.project.key ? { key: state.project.key } : {}) },
119
+ checks: health.checks,
120
+ });
121
+ return health.passed ? 0 : 1;
122
+ }
123
+ /**
124
+ * `jam auth status --json` - presence and origin only.
125
+ *
126
+ * Never returns the credential itself. An agent needs to know whether
127
+ * authentication is configured, not what it is.
128
+ */
129
+ export function authStatusCommand(options = {}) {
130
+ const { credentials } = detect(options);
131
+ emitJson({
132
+ status: credentials.present ? "configured" : "not_configured",
133
+ ...(credentials.present ? {} : { code: "JAM_AUTH_REQUIRED" }),
134
+ source: credentials.source,
135
+ ...(credentials.email ? { email: credentials.email } : {}),
136
+ ...(credentials.baseUrl ? { baseUrl: credentials.baseUrl } : {}),
137
+ });
138
+ return credentials.present ? 0 : 1;
139
+ }
140
+ async function gateResult(root) {
141
+ try {
142
+ const deps = await buildDeps({ cwd: root, keyFallback: "optional" });
143
+ const gate = await runHealthGate(deps, "full");
144
+ return { passed: gate.passed, checks: gate.checks };
145
+ }
146
+ catch (err) {
147
+ return { passed: false, checks: [], error: toJamError(err).message };
148
+ }
149
+ }
@@ -0,0 +1,39 @@
1
+ import { type SecretStore, type StoredCredentials } from "../adapters/credentials/secret-store.js";
2
+ import type { CredentialPort } from "../ports/credentials.port.js";
3
+ import { Ui } from "./ui.js";
4
+ /**
5
+ * `jam auth login` / `jam auth logout`.
6
+ *
7
+ * Human-facing on purpose: these are the one step of setup an agent must not
8
+ * perform. An agent that asks a person for a token, or stores one, has done
9
+ * something wrong regardless of intent - so this path prompts, and the agent
10
+ * path stops at JAM_AUTH_REQUIRED and hands the user this command.
11
+ *
12
+ * `jam auth status` stays in the agent API: it is presence and origin only,
13
+ * and its JSON shape is a contract.
14
+ */
15
+ export type AuthOptions = {
16
+ ui?: Ui;
17
+ /** Injected by tests so the suite never touches a real keychain. */
18
+ store?: SecretStore | undefined;
19
+ /** Reads back what is now in effect. A fresh port each call - see below. */
20
+ readBack?: () => CredentialPort;
21
+ /**
22
+ * Injected by tests. Real verification talks to Jira, which no unit test may
23
+ * do - and the guarantee under test (nothing is stored unless Jira accepted
24
+ * it) is about the ordering, not about the network.
25
+ */
26
+ verify?: (values: StoredCredentials) => Promise<string | undefined>;
27
+ };
28
+ export declare function authLoginCommand(options?: AuthOptions): Promise<number>;
29
+ export declare function authLogoutCommand(options?: AuthOptions): number;
30
+ /**
31
+ * The origin of any URL from the user's Jira site, or undefined.
32
+ *
33
+ * Parsed rather than trimmed, because a pathname is the failure that actually
34
+ * happens: someone copies the project page URL, and JiraClient resolves each
35
+ * REST path *relative* to the base, so `/jira/software/c/projects/KEY/summary`
36
+ * gets the API path appended to it. Atlassian answers that with its HTML shell
37
+ * at 200, and the only symptom is "Jira returned a non-JSON response".
38
+ */
39
+ export declare function toJiraOrigin(input: string): string | undefined;