File size: 3,733 Bytes
c0af099
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env node
/**
 * Generate examples/acp-docker/.env from the single source of truth.
 *
 * Reads the version pins in config/defaults.json and writes the
 * `AGENT_SERVER_IMAGE=` line into examples/acp-docker/.env, pinning the
 * example to the exact `versions.agentServer` release. This keeps the
 * reproducible quickstart path on the SoT version instead of a hardcoded
 * tag that silently drifts below the Canvas compatibility floor
 * (compatibility.minimumAgentServer) and renders "Disconnected".
 *
 * The no-config `docker compose up` path uses the compose fallback
 * (`latest-python`, always >= the floor); running this script first pins the
 * example to the reproducible SoT version instead.
 *
 * Idempotent: re-running upserts the AGENT_SERVER_IMAGE line and leaves any
 * other lines in an existing .env untouched.
 *
 * Usage:
 *   node scripts/gen-acp-docker-env.mjs        # or: npm run example:acp-docker:env
 */
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname, join } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, "..");

/**
 * @param {{ images: { agentServer: string }, versions: { agentServer: string } }} config
 * @returns {string} e.g. "ghcr.io/openhands/agent-server:1.28.1-python"
 */
export function computeAgentServerImage(config) {
  return `${config.images.agentServer}:${config.versions.agentServer}-python`;
}

/**
 * @param {{ images: { agentServer: string }, versions: { agentServer: string } }} config
 * @returns {string} the `AGENT_SERVER_IMAGE=<image>` line
 */
export function renderEnvLine(config) {
  return `AGENT_SERVER_IMAGE=${computeAgentServerImage(config)}`;
}

/**
 * Upsert the AGENT_SERVER_IMAGE line into an existing .env body, preserving
 * every other line. Appends the line if absent.
 * @param {string} existing prior .env contents ("" if the file is absent)
 * @param {string} line the `AGENT_SERVER_IMAGE=...` line to set
 * @returns {string} the updated .env contents
 */
export function upsertEnvLine(existing, line) {
  const eq = line.indexOf("=");
  if (eq <= 0) {
    // A keyless line ("", "novalue", "=value") would make `key` empty and
    // match every line — refuse rather than silently rewrite the whole file.
    throw new Error(
      `upsertEnvLine: expected a "KEY=value" line, got "${line}"`,
    );
  }
  const key = line.slice(0, eq + 1); // "AGENT_SERVER_IMAGE="
  const lines = existing.length ? existing.replace(/\n+$/, "").split("\n") : [];
  let replaced = false;
  const next = lines.map((l) => {
    if (l.startsWith(key)) {
      replaced = true;
      return line;
    }
    return l;
  });
  if (!replaced) next.push(line);
  return next.join("\n") + "\n";
}

function loadConfig() {
  return JSON.parse(
    readFileSync(join(projectRoot, "config", "defaults.json"), "utf-8"),
  );
}

function main() {
  const config = loadConfig();
  const line = renderEnvLine(config);
  const envPath = join(projectRoot, "examples", "acp-docker", ".env");

  let existing = "";
  try {
    existing = readFileSync(envPath, "utf-8");
  } catch {
    // no .env yet — create it
  }

  const updated = upsertEnvLine(existing, line);
  writeFileSync(envPath, updated);
  console.log(`Wrote ${line} to examples/acp-docker/.env`);
}

// Run main() only when invoked as a CLI. process.argv[1] is undefined in some
// ESM contexts (e.g. `node --input-type=module -e "import(...)"`), so guard it
// before pathToFileURL — otherwise importing this module for its exports throws
// ERR_INVALID_ARG_TYPE.
if (
  process.argv[1] &&
  import.meta.url === pathToFileURL(process.argv[1]).href
) {
  main();
}