Skip to content

Commit ae5d009

Browse files
authored
feat(cli): add unified setup wizard consolidating init/install/docker flows (#132)
* feat(cli): add unified setup wizard consolidating init/install/docker flows (#129) Create `gitlab-mcp setup` command that unifies the three separate wizards (init, install, docker init) into a single entry point with discovery, mode selection, tool configuration, and multiple execution flows. - Add src/cli/setup/ module with types, presets, discovery, wizard, and flows - Add local-setup, server-setup, configure-existing, and tool-selection flows - Define 18 tool categories and 6 preset definitions (developer, senior-dev, devops, code-reviewer, full-access, readonly) - Add missing preset YAML files: code-reviewer.yaml, full-access.yaml - Route `init` as alias to `setup --mode=local` - Route `docker init` as alias to `setup --mode=server` - Update cli-utils.ts with setup command and --mode flag parsing - Add 43 unit tests for presets, discovery, and wizard modules * refactor(setup): align env vars with config and add category application - Rename USE_ISSUES/USE_WIKI/etc to USE_WORKITEMS/USE_GITLAB_WIKI/etc - Rename GITLAB_SCOPE_* to GITLAB_PROJECT_ID/GITLAB_ALLOWED_PROJECT_IDS - Rename GITLAB_MCP_PRESET to GITLAB_PROFILE - Rename GITLAB_URL to GITLAB_API_URL in local-setup - Apply toolConfig env to Docker config in server-setup - Add applyManualCategories() for manual mode category disabling - Add failure reporting in configure-existing updateClients - Replace process.exit(0) with null return in wizard cancel path - Remove unused skipGitlab option from wizard - Use manage_repository:fork in code-reviewer profile - Add unit tests for setup flow modules * refactor(setup): remove commits mapping, rename scope option, persist env to Docker - Remove "commits" from CATEGORY_ENV_MAP (browse_commits is always-on core) - Rename "namespace" scope option to "allowlist" with clearer prompt - Add environment field to DockerConfig type - Persist tool config env vars into docker-compose via generateDockerCompose - Make SetupResult.mode optional for cancel-before-selection path - Add generateDockerCompose environment tests * fix(discovery): add docker-compose v1 fallback for compose detection detectDocker now checks both `docker compose version` (v2) and `docker-compose --version` (v1) for consistency with docker-utils. * feat(docker): add native Podman support with container runtime detection Add container-runtime module that detects Docker or Podman automatically. When Docker is unavailable, falls back to podman/podman-compose for all container operations. Centralizes runtime detection with process-level caching, eliminating duplicated logic in discovery.ts. * fix(setup): use runtime-aware messages and propagate deployment type - Pass DEPLOYMENT_TYPE to Docker environment config - Replace hardcoded "Docker" error messages with runtime label - Add error field to configure-existing container operation failure * fix(docker): propagate OAuth secrets to compose and throw on missing compose - Use config.oauthSessionSecret and config.databaseUrl in compose env - Throw controlled error in tailLogs when no compose tool detected * fix(setup): implement deployment types, secure secrets, filter ungated categories - Exit non-zero from main when setup wizard fails - Add compose-bundle deployment with bundled postgres service - Store OAuth secret in .env file (0600) instead of plaintext in compose - Filter always-on categories from manual tool selection UI - Add deploymentType field to DockerConfig interface * fix(docker): require OAuth for postgres, use random password, fix exit code - docker init now exits non-zero on wizard failure (like init/setup) - compose-bundle only adds postgres service when oauthEnabled is true - POSTGRES_PASSWORD uses randomBytes(24) instead of weak fixed default - Add unit tests for setup subcommand parsing in cli-utils - Add tests for docker init exit codes in main.entry * fix(presets): correct tool names for webhooks/integrations, add return guard - Rename list_webhooks → browse_webhooks in webhooks category - Rename list_integrations → browse_integrations in integrations category - Add return after process.exit in docker init to prevent fall-through
1 parent d1d5487 commit ae5d009

28 files changed

+4880
-247
lines changed

src/cli-utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ export interface CliArgs {
2121
dryRun: boolean;
2222
/** Git remote name to use (default: origin) */
2323
remoteName?: string;
24-
/** Run init wizard */
24+
/** Run unified setup wizard */
25+
setup: boolean;
26+
/** Setup wizard mode override */
27+
setupMode?: "local" | "server" | "configure-existing";
28+
/** Run init wizard (alias for setup --mode=local) */
2529
init: boolean;
2630
/** Run install command */
2731
install: boolean;
@@ -45,6 +49,7 @@ export function parseCliArgs(argv: string[] = process.argv): CliArgs {
4549
showProjectConfig: false,
4650
auto: false,
4751
dryRun: false,
52+
setup: false,
4853
init: false,
4954
install: false,
5055
installArgs: [],
@@ -55,6 +60,15 @@ export function parseCliArgs(argv: string[] = process.argv): CliArgs {
5560
// Check for subcommands (first positional arg)
5661
if (args.length > 0) {
5762
switch (args[0]) {
63+
case "setup":
64+
result.setup = true;
65+
// Parse setup-specific flags
66+
for (const arg of args.slice(1)) {
67+
if (arg === "--mode=local") result.setupMode = "local";
68+
else if (arg === "--mode=server") result.setupMode = "server";
69+
else if (arg === "--mode=configure-existing") result.setupMode = "configure-existing";
70+
}
71+
return result;
5872
case "init":
5973
result.init = true;
6074
return result;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* Container runtime detection module.
3+
* Detects Docker or Podman and their compose variants, caching the result per process.
4+
*/
5+
6+
import { spawnSync } from "child_process";
7+
import { ContainerRuntime, ContainerRuntimeInfo } from "./types";
8+
9+
/** Module-level cached runtime info */
10+
let cachedRuntime: ContainerRuntimeInfo | null = null;
11+
12+
/**
13+
* Try running a command and return true if it exits 0.
14+
*/
15+
function commandSucceeds(cmd: string, args: string[]): boolean {
16+
try {
17+
const result = spawnSync(cmd, args, {
18+
stdio: "pipe",
19+
encoding: "utf8",
20+
});
21+
return result.status === 0;
22+
} catch {
23+
return false;
24+
}
25+
}
26+
27+
/**
28+
* Try running a command and return its stdout if it exits 0, otherwise undefined.
29+
*/
30+
function commandOutput(cmd: string, args: string[]): string | undefined {
31+
try {
32+
const result = spawnSync(cmd, args, {
33+
stdio: "pipe",
34+
encoding: "utf8",
35+
});
36+
if (result.status === 0 && result.stdout) {
37+
return result.stdout.trim();
38+
}
39+
return undefined;
40+
} catch {
41+
return undefined;
42+
}
43+
}
44+
45+
/**
46+
* Extract version string from runtime --version output.
47+
* e.g. "Docker version 24.0.7, build afdd53b" → "24.0.7"
48+
* "podman version 4.9.3" → "4.9.3"
49+
*/
50+
function parseVersion(output: string): string | undefined {
51+
const match = output.match(/(\d+\.\d+\.\d+)/);
52+
return match?.[1];
53+
}
54+
55+
/**
56+
* Detect the compose command for a given runtime.
57+
* Priority for docker: docker compose → docker-compose
58+
* Priority for podman: podman compose → podman-compose → docker-compose (fallback)
59+
*/
60+
function detectComposeCmd(runtime: ContainerRuntime): string[] | null {
61+
const runtimeCmd = runtime;
62+
63+
// Try "<runtime> compose version" (compose v2 plugin)
64+
if (commandSucceeds(runtimeCmd, ["compose", "version"])) {
65+
return [runtimeCmd, "compose"];
66+
}
67+
68+
// Try "<runtime>-compose --version" (standalone compose)
69+
const standaloneCompose = `${runtimeCmd}-compose`;
70+
if (commandSucceeds(standaloneCompose, ["--version"])) {
71+
return [standaloneCompose];
72+
}
73+
74+
// Cross-runtime fallback: try docker-compose as last resort
75+
if (commandSucceeds("docker-compose", ["--version"])) {
76+
return ["docker-compose"];
77+
}
78+
79+
return null;
80+
}
81+
82+
/**
83+
* Perform full container runtime detection.
84+
* Priority: docker > podman.
85+
* Checks runtime availability, daemon status, and compose command.
86+
*/
87+
export function detectContainerRuntime(): ContainerRuntimeInfo {
88+
const runtimes: ContainerRuntime[] = ["docker", "podman"];
89+
90+
for (const runtime of runtimes) {
91+
const versionOutput = commandOutput(runtime, ["--version"]);
92+
if (versionOutput) {
93+
// Runtime binary exists, check if daemon is accessible
94+
const runtimeAvailable = commandSucceeds(runtime, ["info"]);
95+
const composeCmd = detectComposeCmd(runtime);
96+
const runtimeVersion = parseVersion(versionOutput);
97+
98+
return {
99+
runtime,
100+
runtimeCmd: runtime,
101+
runtimeAvailable,
102+
composeCmd,
103+
runtimeVersion,
104+
};
105+
}
106+
}
107+
108+
// No runtime found at all
109+
return {
110+
runtime: "docker",
111+
runtimeCmd: "docker",
112+
runtimeAvailable: false,
113+
composeCmd: null,
114+
runtimeVersion: undefined,
115+
};
116+
}
117+
118+
/**
119+
* Get cached container runtime info.
120+
* Detects once per process and caches the result.
121+
*/
122+
export function getContainerRuntime(): ContainerRuntimeInfo {
123+
cachedRuntime ??= detectContainerRuntime();
124+
return cachedRuntime;
125+
}
126+
127+
/**
128+
* Reset the runtime cache. Used in tests to allow re-detection.
129+
*/
130+
export function resetRuntimeCache(): void {
131+
cachedRuntime = null;
132+
}

0 commit comments

Comments
 (0)