context101-cli 0.1.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/src/checks.js ADDED
@@ -0,0 +1,165 @@
1
+ import { SMOOTH_REGION } from "./defaults.js";
2
+ import { commandExists } from "./exec.js";
3
+ import { ensureDockerDaemon } from "./docker.js";
4
+
5
+ /** Amplify CreateApp calls GitHub list-repository-webhooks with this token. */
6
+ export function classifyGithubToken(token) {
7
+ const value = String(token ?? "").trim();
8
+ if (!value) return "missing";
9
+ if (value.startsWith("ghp_")) return "classic_pat";
10
+ if (value.startsWith("github_pat_")) return "fine_grained_pat";
11
+ if (value.startsWith("ghs_")) return "installation";
12
+ if (value.startsWith("gho_")) return "oauth";
13
+ if (value.startsWith("ghu_")) return "user_to_server";
14
+ return "unknown";
15
+ }
16
+
17
+ export function githubTokenWorksForAmplify(kind) {
18
+ return kind === "classic_pat" || kind === "fine_grained_pat";
19
+ }
20
+
21
+ function versionOf(exec, command, args, pattern) {
22
+ const result = exec({ command, args });
23
+ if (!result.ok) return null;
24
+ const text = `${result.stdout}\n${result.stderr}`;
25
+ const match = text.match(pattern);
26
+ return match ? match[1] : text.split("\n")[0];
27
+ }
28
+
29
+ export function runChecks({
30
+ exec,
31
+ env = {},
32
+ region = SMOOTH_REGION,
33
+ platform,
34
+ dryRun = false,
35
+ wait,
36
+ } = {}) {
37
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
38
+ const nodeOk = nodeMajor >= 20;
39
+ const npmVersion = versionOf(exec, "npm", ["-v"], /^(\d+\.\d+\.\d+)/m);
40
+ const awsVersion = versionOf(exec, "aws", ["--version"], /aws-cli\/(\S+)/);
41
+ const docker = ensureDockerDaemon({ exec, platform, dryRun, wait });
42
+ const ghOk = commandExists(exec, "gh");
43
+
44
+ let awsIdentity = null;
45
+ if (awsVersion) {
46
+ const identity = exec({
47
+ command: "aws",
48
+ args: ["sts", "get-caller-identity", "--output", "json"],
49
+ env,
50
+ });
51
+ if (identity.ok) {
52
+ try {
53
+ const parsed = JSON.parse(identity.stdout);
54
+ awsIdentity = {
55
+ account: parsed.Account ?? "",
56
+ arn: parsed.Arn ?? "",
57
+ };
58
+ } catch {
59
+ awsIdentity = { account: "", arn: "" };
60
+ }
61
+ }
62
+ }
63
+
64
+ let bootstrapped = null;
65
+ if (awsIdentity) {
66
+ const stack = exec({
67
+ command: "aws",
68
+ args: [
69
+ "cloudformation",
70
+ "describe-stacks",
71
+ "--stack-name",
72
+ "CDKToolkit",
73
+ "--region",
74
+ region,
75
+ "--query",
76
+ "Stacks[0].StackStatus",
77
+ "--output",
78
+ "text",
79
+ ],
80
+ env,
81
+ });
82
+ bootstrapped = stack.ok && /CREATE_COMPLETE|UPDATE_COMPLETE/.test(stack.stdout);
83
+ }
84
+
85
+ let ghLoggedIn = false;
86
+ let ghTokenKind = "missing";
87
+ if (ghOk) {
88
+ const token = exec({ command: "gh", args: ["auth", "token"] });
89
+ ghLoggedIn = token.ok && token.stdout.trim().length > 0;
90
+ if (ghLoggedIn) ghTokenKind = classifyGithubToken(token.stdout);
91
+ }
92
+
93
+ return {
94
+ node: { ok: nodeOk, version: process.versions.node },
95
+ npm: { ok: Boolean(npmVersion), version: npmVersion },
96
+ aws: { ok: Boolean(awsVersion), version: awsVersion, identity: awsIdentity },
97
+ docker,
98
+ gh: {
99
+ ok: ghOk,
100
+ loggedIn: ghLoggedIn,
101
+ tokenKind: ghTokenKind,
102
+ amplifyOk: githubTokenWorksForAmplify(ghTokenKind),
103
+ },
104
+ bootstrap: { ok: bootstrapped, region },
105
+ };
106
+ }
107
+
108
+ export function printChecks(checks, io) {
109
+ const { ok, warn, dim } = io;
110
+ const nodeLine = checks.node.ok
111
+ ? `node ${checks.node.version}`
112
+ : `node ${checks.node.version} (need 20+)`;
113
+ (checks.node.ok ? ok : warn)(nodeLine);
114
+ (checks.npm.ok ? ok : warn)(checks.npm.ok ? `npm ${checks.npm.version}` : "npm not found");
115
+ if (checks.aws.ok && checks.aws.identity) {
116
+ ok(`aws ${checks.aws.version} account ${checks.aws.identity.account}`);
117
+ } else if (checks.aws.ok) {
118
+ warn(`aws ${checks.aws.version} — sts get-caller-identity failed`);
119
+ } else {
120
+ warn("aws cli not found");
121
+ }
122
+ if (checks.awsProfile) {
123
+ ok(`AWS profile ${checks.awsProfile}`);
124
+ } else if (checks.hasAwsKeys) {
125
+ ok("AWS access keys (no profile)");
126
+ } else if (checks.awsProfiles?.length > 1) {
127
+ warn(
128
+ `${checks.awsProfiles.length} AWS profiles (${checks.awsProfiles.join(", ")}) — pick one`
129
+ );
130
+ } else if (checks.awsProfiles?.length === 0) {
131
+ warn("no AWS profiles — will ask for access key and secret");
132
+ }
133
+ printDockerCheck(checks.docker, { ok, warn });
134
+ if (checks.gh.ok && checks.gh.amplifyOk) {
135
+ ok("gh (logged in with a PAT — usable if you watch a repo with Amplify)");
136
+ } else if (checks.gh.ok && checks.gh.loggedIn) {
137
+ warn(
138
+ `gh token is ${checks.gh.tokenKind} — if Amplify watches a repo, set CTX_GH_TOKEN=ghp_…`
139
+ );
140
+ } else if (checks.gh.ok) {
141
+ warn("gh found but not logged in — set CTX_GH_TOKEN only if Amplify watches a repo");
142
+ } else {
143
+ dim("gh optional — Amplify is skipped unless you pass --repo");
144
+ }
145
+ }
146
+
147
+ function printDockerCheck(docker, { ok, warn }) {
148
+ if (!docker) {
149
+ warn("docker not found (needed for CDK image assets)");
150
+ return;
151
+ }
152
+ if (!docker.installed) {
153
+ warn("docker not found (needed for CDK image assets)");
154
+ return;
155
+ }
156
+ if (docker.daemon && docker.started) {
157
+ ok(`docker (started ${docker.starter})`);
158
+ return;
159
+ }
160
+ if (docker.daemon) {
161
+ ok("docker");
162
+ return;
163
+ }
164
+ warn("docker daemon is not running (needed for CDK image assets)");
165
+ }
package/src/clone.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_AMPLIFY_REPO } from "./defaults.js";
4
+ import { findRepoRoot } from "./repo.js";
5
+
6
+ export const CLONE_URL = DEFAULT_AMPLIFY_REPO;
7
+ export const DEFAULT_CLONE_DIR = "context101";
8
+
9
+ export function resolveCloneDir(cwd, dir) {
10
+ return path.resolve(cwd, dir || DEFAULT_CLONE_DIR);
11
+ }
12
+
13
+ export function ensureRepoRoot({
14
+ cwd,
15
+ dir,
16
+ exec,
17
+ io,
18
+ dryRun = false,
19
+ exists = existsSync,
20
+ } = {}) {
21
+ const existing = findRepoRoot(cwd, exists);
22
+ if (existing) return { repoRoot: existing, cloned: false };
23
+
24
+ const target = resolveCloneDir(cwd, dir);
25
+ const already = findRepoRoot(target, exists);
26
+ if (already) return { repoRoot: already, cloned: false };
27
+
28
+ if (dryRun) {
29
+ io?.write?.(`Would clone ${CLONE_URL} into ${path.relative(cwd, target) || target}`);
30
+ return { repoRoot: target, cloned: false, wouldClone: true };
31
+ }
32
+
33
+ if (!exec) {
34
+ return { repoRoot: null, cloned: false, error: "git is required to clone Context101" };
35
+ }
36
+
37
+ io?.write?.(`Cloning ${CLONE_URL}…`);
38
+ if (!exists(path.dirname(target))) {
39
+ mkdirSync(path.dirname(target), { recursive: true });
40
+ }
41
+ const result = exec({
42
+ command: "git",
43
+ args: ["clone", "--depth", "1", CLONE_URL, target],
44
+ timeout: 120_000,
45
+ });
46
+ if (!result.ok) {
47
+ return {
48
+ repoRoot: null,
49
+ cloned: false,
50
+ error: result.stderr || result.stdout || "git clone failed",
51
+ };
52
+ }
53
+ const cloned = findRepoRoot(target, exists);
54
+ if (!cloned) {
55
+ return {
56
+ repoRoot: null,
57
+ cloned: false,
58
+ error: `cloned ${target} but it is not a Context101 checkout (needs cdk/ and web/)`,
59
+ };
60
+ }
61
+ io?.ok?.(`cloned into ${path.relative(cwd, cloned) || cloned}`);
62
+ return { repoRoot: cloned, cloned: true };
63
+ }
package/src/config.js ADDED
@@ -0,0 +1,102 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { SECRET_KEYS } from "./defaults.js";
5
+ import { findDeployEnvPath, parseEnvFile } from "./deploy-env-load.js";
6
+ import { quoteShell } from "./env-file.js";
7
+ import { isHostedContext101Url } from "./hosted-url.js";
8
+ import { mask } from "./redact.js";
9
+ import { findRepoRoot } from "./repo.js";
10
+ import { banner, writers } from "./style.js";
11
+
12
+ const SECRET_NAME = /TOKEN|SECRET|PASSWORD|PEPPER|KEY/i;
13
+ const HOSTED_KEYS = new Set([
14
+ "BETTER_AUTH_URL",
15
+ "APP_URL",
16
+ "MARKETING_URL",
17
+ "MCP_PUBLIC_HOST",
18
+ ]);
19
+
20
+ export function isSecretConfigKey(key) {
21
+ return SECRET_KEYS.includes(key) || SECRET_NAME.test(key);
22
+ }
23
+
24
+ export function formatConfig(values) {
25
+ const keys = Object.keys(values).sort();
26
+ if (keys.length === 0) return "No keys in the env file.";
27
+ return keys
28
+ .map((key) => {
29
+ const value = values[key] ?? "";
30
+ const shown = isSecretConfigKey(key) ? mask(value) : value;
31
+ return `${key}=${shown}`;
32
+ })
33
+ .join("\n");
34
+ }
35
+
36
+ export function upsertEnvLine(text, key, value) {
37
+ const line = `${key}=${quoteShell(value)}`;
38
+ const re = new RegExp(`^(\\s*(?:export\\s+)?${key}=).*`, "m");
39
+ if (re.test(text || "")) return (text || "").replace(re, line);
40
+ const base = !text ? "# Written by `context101 config set`. chmod 600.\n\n" : text;
41
+ const trimmed = base.endsWith("\n") ? base : `${base}\n`;
42
+ return `${trimmed}${line}\n`;
43
+ }
44
+
45
+ export async function runConfig(opts, ctx) {
46
+ const io = writers(ctx);
47
+ banner(ctx);
48
+
49
+ const repoRoot = findRepoRoot(ctx.cwd);
50
+ const filePath = findDeployEnvPath({
51
+ repoRoot,
52
+ envFile: opts.envFile,
53
+ home: opts.home,
54
+ cwd: ctx.cwd,
55
+ });
56
+
57
+ if (opts.configAction === "set") {
58
+ return writeConfig(opts, { io, filePath });
59
+ }
60
+
61
+ if (!filePath || !existsSync(filePath)) {
62
+ io.write("No deploy-env file yet.");
63
+ io.write("Next: context101 init");
64
+ return 0;
65
+ }
66
+
67
+ const text = await readFile(filePath, "utf8");
68
+ const { values } = parseEnvFile(text);
69
+ io.write(formatConfig(values));
70
+ io.write("");
71
+ return 0;
72
+ }
73
+
74
+ async function writeConfig(opts, { io, filePath }) {
75
+ const key = opts.configKey;
76
+ const value = opts.configValue;
77
+ if (!key) {
78
+ io.err("usage: context101 config set KEY=value");
79
+ return 1;
80
+ }
81
+ if (HOSTED_KEYS.has(key) && isHostedContext101Url(value)) {
82
+ io.err(
83
+ `${key} is the hosted Context101 product, not a self-host URL. Set a domain you own, or omit it.`
84
+ );
85
+ return 1;
86
+ }
87
+ if (!filePath) {
88
+ io.err("no deploy-env path. Re-run from a checkout or pass --deploy-env.");
89
+ return 1;
90
+ }
91
+
92
+ let text = "";
93
+ if (existsSync(filePath)) {
94
+ text = await readFile(filePath, "utf8");
95
+ }
96
+ const next = upsertEnvLine(text, key, value);
97
+ await mkdir(dirname(filePath), { recursive: true });
98
+ await writeFile(filePath, next, { encoding: "utf8", mode: 0o600 });
99
+ await chmod(filePath, 0o600);
100
+ io.ok(`set ${key}`);
101
+ return 0;
102
+ }
@@ -0,0 +1,28 @@
1
+ export const SMOOTH_REGION = "us-east-1"; // pragma: allowlist secret
2
+ export const DEFAULT_AMPLIFY_REPO = "https://github.com/jginorio/context101";
3
+ export const TITAN_EMBED_MODEL = "amazon.titan-embed-text-v2:0";
4
+ export const CLAUDE_IMPROVE_MODEL = "us.anthropic.claude-opus-4-7";
5
+ export const DRIVER_NEON = "neon-http"; // pragma: allowlist secret
6
+ export const DRIVER_POSTGRES = "postgres-js";
7
+ export const APP_MODE = "self_hosted";
8
+ export const ALLOW_PUBLIC_SIGNUP = "false";
9
+ export const BILLING_ENABLED = "false";
10
+ export const EXAMPLE_ENV_REL = "cdk/.deploy-env.example";
11
+ export const REPO_ENV_REL = "cdk/.deploy-env";
12
+ export const HOME_ENV_REL = ".context101/deploy-env";
13
+ export const DEPLOY_CLI = "context101 deploy";
14
+ export const LIST_CLI = "context101 list";
15
+ export const DESTROY_CLI = "context101 destroy";
16
+ export const NPX_CLI = "npx context101-cli";
17
+ export const STACK_NAME = "Context101Stack";
18
+
19
+ export const SECRET_KEYS = [
20
+ "CTX_TOKEN",
21
+ "CTX_GH_TOKEN",
22
+ "BETTER_AUTH_SECRET",
23
+ "MCP_TOKEN_PEPPER",
24
+ "DATABASE_URL",
25
+ "AWS_ACCESS_KEY_ID",
26
+ "AWS_SECRET_ACCESS_KEY",
27
+ "AWS_SESSION_TOKEN",
28
+ ];
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { HOME_ENV_REL, REPO_ENV_REL } from "./defaults.js";
5
+
6
+ export function unquoteEnvValue(raw) {
7
+ const trimmed = String(raw ?? "").trim();
8
+ if (
9
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
10
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
11
+ ) {
12
+ return trimmed.slice(1, -1).replace(/\\([\\"$`])/g, "$1");
13
+ }
14
+ return trimmed;
15
+ }
16
+
17
+ export function parseEnvFile(text) {
18
+ const values = {};
19
+ const declared = new Set();
20
+ const lines = String(text ?? "").split(/\r?\n/);
21
+ for (const line of lines) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed || trimmed.startsWith("#")) continue;
24
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
25
+ if (!match) continue;
26
+ declared.add(match[1]);
27
+ values[match[1]] = unquoteEnvValue(match[2]);
28
+ }
29
+ return { values, declared };
30
+ }
31
+
32
+ export function envFileDeclares(declared, key) {
33
+ return Boolean(declared && declared.has(key));
34
+ }
35
+
36
+ export function findDeployEnvPath({
37
+ repoRoot,
38
+ envFile,
39
+ home = false,
40
+ cwd,
41
+ exists = existsSync,
42
+ } = {}) {
43
+ if (envFile) {
44
+ return path.isAbsolute(envFile)
45
+ ? envFile
46
+ : path.resolve(cwd ?? repoRoot ?? process.cwd(), envFile);
47
+ }
48
+ if (home) {
49
+ const homePath = path.join(homedir(), HOME_ENV_REL);
50
+ return exists(homePath) ? homePath : homePath;
51
+ }
52
+ if (repoRoot) {
53
+ const repoPath = path.join(repoRoot, ...REPO_ENV_REL.split("/"));
54
+ if (exists(repoPath)) return repoPath;
55
+ }
56
+ const homePath = path.join(homedir(), HOME_ENV_REL);
57
+ if (exists(homePath)) return homePath;
58
+ return repoRoot ? path.join(repoRoot, ...REPO_ENV_REL.split("/")) : null;
59
+ }
60
+
61
+ export function readDeployEnvFile(filePath, { readFile = readFileSync, exists = existsSync } = {}) {
62
+ if (!filePath || !exists(filePath)) {
63
+ return { path: filePath, values: {}, declared: new Set(), text: "", exists: false };
64
+ }
65
+ const text = readFile(filePath, "utf8");
66
+ const parsed = parseEnvFile(text);
67
+ return {
68
+ path: filePath,
69
+ values: parsed.values,
70
+ declared: parsed.declared,
71
+ text,
72
+ exists: true,
73
+ };
74
+ }
package/src/deploy.js ADDED
@@ -0,0 +1,138 @@
1
+ import { SMOOTH_REGION } from "./defaults.js";
2
+ import {
3
+ assertDeployTokens,
4
+ buildCdkArgs,
5
+ formatCdkPreview,
6
+ resolveDeployContext,
7
+ runCdk,
8
+ } from "./cdk-invoke.js";
9
+ import { printChecks, runChecks } from "./checks.js";
10
+ import { createExec } from "./exec.js";
11
+ import { deployCommand } from "./plan.js";
12
+ import { findRepoRoot } from "./repo.js";
13
+ import { banner, writers } from "./style.js";
14
+
15
+ export async function runDeploy(opts, ctx) {
16
+ return runCdkCommand(opts, ctx, opts.command || "deploy");
17
+ }
18
+
19
+ async function runCdkCommand(opts, ctx, action) {
20
+ const io = writers(ctx);
21
+ const exec = ctx.exec ?? createExec(ctx.env);
22
+
23
+ banner(ctx);
24
+ if (opts.dryRun) {
25
+ io.dim(`dry-run — ${action} nothing`);
26
+ io.write("");
27
+ }
28
+
29
+ const repoRoot = findRepoRoot(ctx.cwd);
30
+ if (!repoRoot) {
31
+ io.err("run this from a Context101 checkout (needs cdk/ and web/).");
32
+ return 1;
33
+ }
34
+
35
+ const env = ctx.env ?? {};
36
+ const checks = runChecks({
37
+ exec,
38
+ env,
39
+ region: SMOOTH_REGION,
40
+ dryRun: opts.dryRun,
41
+ });
42
+ printChecks(checks, io);
43
+ if (checks.docker?.hint && !checks.docker.daemon) {
44
+ io.write(checks.docker.hint);
45
+ }
46
+ io.write("");
47
+
48
+ let context;
49
+ let args;
50
+ try {
51
+ context = resolveDeployContext({
52
+ repoRoot,
53
+ env,
54
+ home: opts.home,
55
+ envFile: opts.envFile,
56
+ cwd: ctx.cwd,
57
+ exec,
58
+ });
59
+ assertDeployTokens(context, { action });
60
+ args = buildCdkArgs({
61
+ action,
62
+ seed: opts.seed,
63
+ context,
64
+ env,
65
+ });
66
+ } catch (error) {
67
+ if (error && error.code === "USAGE") {
68
+ io.err(error.message);
69
+ return 1;
70
+ }
71
+ throw error;
72
+ }
73
+
74
+ io.write(formatCdkPreview({ action, context, args, seed: opts.seed }));
75
+ io.write("");
76
+
77
+ if (opts.dryRun) {
78
+ io.write(`Would invoke: ${deployCommand(opts.seed)}`);
79
+ return 0;
80
+ }
81
+
82
+ if (action === "deploy") {
83
+ return startDeploy({
84
+ io,
85
+ ctx,
86
+ repoRoot,
87
+ seed: opts.seed,
88
+ env,
89
+ home: opts.home,
90
+ envFile: opts.envFile,
91
+ dockerDaemon: Boolean(checks.docker?.daemon),
92
+ dockerHint: checks.docker?.hint,
93
+ });
94
+ }
95
+
96
+ return (ctx.runDeploy ?? runCdk)({
97
+ repoRoot,
98
+ action,
99
+ seed: opts.seed,
100
+ env,
101
+ home: opts.home,
102
+ envFile: opts.envFile,
103
+ cwd: ctx.cwd,
104
+ exec,
105
+ });
106
+ }
107
+
108
+ export async function startDeploy({
109
+ io,
110
+ ctx,
111
+ repoRoot,
112
+ seed,
113
+ env,
114
+ home,
115
+ envFile,
116
+ dockerDaemon,
117
+ dockerHint,
118
+ }) {
119
+ if (!dockerDaemon) {
120
+ io.err(
121
+ "not deploying: Docker daemon is not running. Start it, then run context101 deploy."
122
+ );
123
+ if (dockerHint) io.write(dockerHint);
124
+ return 1;
125
+ }
126
+
127
+ io.write("Deploying the stack…");
128
+ return (ctx.runDeploy ?? runCdk)({
129
+ repoRoot,
130
+ action: "deploy",
131
+ seed,
132
+ env,
133
+ home,
134
+ envFile,
135
+ cwd: ctx.cwd,
136
+ exec: ctx.exec,
137
+ });
138
+ }
package/src/docker.js ADDED
@@ -0,0 +1,120 @@
1
+ import { commandExists } from "./exec.js";
2
+
3
+ export function dockerDaemonRunning(exec) {
4
+ const result = exec({
5
+ command: "docker",
6
+ args: ["info"],
7
+ timeout: 15_000,
8
+ });
9
+ return Boolean(result.ok);
10
+ }
11
+
12
+ export function dockerStartHint(platform = process.platform) {
13
+ const lines = [
14
+ "Start the Docker daemon, then run context101 deploy:",
15
+ " · Docker Desktop — open the app and wait until it is running",
16
+ " · Colima — colima start",
17
+ ];
18
+ if (platform === "linux") {
19
+ lines.push(" · Linux — sudo systemctl start docker");
20
+ }
21
+ return lines.join("\n");
22
+ }
23
+
24
+ export function listDockerStarters({ exec, platform = process.platform } = {}) {
25
+ const starters = [];
26
+ if (commandExists(exec, "colima")) {
27
+ starters.push({
28
+ name: "colima",
29
+ command: "colima",
30
+ args: ["start"],
31
+ timeout: 120_000,
32
+ });
33
+ }
34
+ if (platform === "darwin") {
35
+ starters.push({
36
+ name: "Docker Desktop",
37
+ command: "open",
38
+ args: ["-a", "Docker"],
39
+ timeout: 20_000,
40
+ });
41
+ }
42
+ if (platform === "linux" && commandExists(exec, "systemctl")) {
43
+ starters.push({
44
+ name: "systemctl",
45
+ command: "systemctl",
46
+ args: ["start", "docker"],
47
+ timeout: 20_000,
48
+ });
49
+ }
50
+ return starters;
51
+ }
52
+
53
+ export function ensureDockerDaemon({
54
+ exec,
55
+ platform = process.platform,
56
+ dryRun = false,
57
+ wait = defaultWait,
58
+ attempts = 8,
59
+ } = {}) {
60
+ if (!commandExists(exec, "docker")) {
61
+ return {
62
+ installed: false,
63
+ daemon: false,
64
+ started: false,
65
+ starter: null,
66
+ hint: "Install Docker (or Colima) so CDK can build the wiki-gen and MCP images.",
67
+ };
68
+ }
69
+ if (dockerDaemonRunning(exec)) {
70
+ return {
71
+ installed: true,
72
+ daemon: true,
73
+ started: false,
74
+ starter: null,
75
+ hint: null,
76
+ };
77
+ }
78
+ if (dryRun) {
79
+ return {
80
+ installed: true,
81
+ daemon: false,
82
+ started: false,
83
+ starter: null,
84
+ hint: dockerStartHint(platform),
85
+ };
86
+ }
87
+
88
+ for (const starter of listDockerStarters({ exec, platform })) {
89
+ const launched = exec({
90
+ command: starter.command,
91
+ args: starter.args,
92
+ timeout: starter.timeout,
93
+ });
94
+ if (!launched.ok) continue;
95
+ for (let i = 0; i < attempts; i++) {
96
+ if (dockerDaemonRunning(exec)) {
97
+ return {
98
+ installed: true,
99
+ daemon: true,
100
+ started: true,
101
+ starter: starter.name,
102
+ hint: null,
103
+ };
104
+ }
105
+ wait(500);
106
+ }
107
+ }
108
+
109
+ return {
110
+ installed: true,
111
+ daemon: false,
112
+ started: false,
113
+ starter: null,
114
+ hint: dockerStartHint(platform),
115
+ };
116
+ }
117
+
118
+ function defaultWait(ms) {
119
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
120
+ }