@domesystems/templates 0.3.6 → 0.3.8

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/README.md CHANGED
@@ -30,11 +30,11 @@ templates logout [--server <URL>|--registry <URL>]
30
30
  templates doctor <pull|up|publish> [--dir <directory>] [--server <URL>|--registry <URL>] [--json]
31
31
  templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]
32
32
  templates generate [--dir <directory>] [--output <file>] [--force]
33
- templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--verbose]
33
+ templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--debug] [--verbose]
34
34
  templates publish [--dry-run] [--server <URL>|--registry <URL>]
35
35
  ```
36
36
 
37
- `pull` only accepts scoped package names, verifies the Registry SHA-256 before extraction, accepts only the archive's `template/` root, writes `.dome-template-lock.yaml`, and never overwrites a non-empty directory. `generate` reads `catalog` in `template.yaml` and writes a `dome.tf` with the matching Gateway, connections, model pool, agent, Rules, Guards, quotas, and runtime key. Generated model pools are non-default so templates can be installed into workspaces that already have a default pool. It never replaces an existing file unless you pass `--force`; review the generated Terraform before importing it. `up` is local-only and never downloads or updates a release. When it provisions an unconfigured runtime, it reveals the import's one-time agent token, resolves the workspace default Gateway, and writes only `DOME_TOKEN` and `DOME_GATEWAY_URL` to the ignored `.env` before starting Docker. `publish` reads the scoped name and SemVer version from `template.yaml`, requires a clean Git checkout, and cannot replace a published version.
37
+ `pull` only accepts scoped package names, verifies the Registry SHA-256 before extraction, accepts only the archive's `template/` root, writes `.dome-template-lock.yaml`, and never overwrites a non-empty directory. `generate` reads `catalog` in `template.yaml` and writes a `dome.tf` with the matching Gateway, connections, model pool, agent, Rules, Guards, quotas, and runtime key. Generated model pools are non-default so templates can be installed into workspaces that already have a default pool. It never replaces an existing file unless you pass `--force`; review the generated Terraform before importing it. `up` is local-only and never downloads or updates a release. When it provisions an unconfigured runtime, it reveals the import's one-time agent token, resolves the workspace default Gateway, and writes only `DOME_TOKEN` and `DOME_GATEWAY_URL` to the ignored `.env` before refreshing the configured runtime image and starting Docker. Pass `--debug` to enable safe runtime diagnostics for that Compose run; it logs outbound Dome endpoint, status/outcome, and timing but not secrets or request content. `publish` reads the scoped name and SemVer version from `template.yaml`, requires a clean Git checkout, and cannot replace a published version.
38
38
 
39
39
  ## Publishing from a GitHub organization
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domesystems/templates",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Pull, publish, and run versioned Dome templates.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.mjs CHANGED
@@ -10,16 +10,16 @@ import { DEFAULT_REGISTRY, clearSelectedTarget, deleteProfile, directRegistry, d
10
10
 
11
11
  function ansi(code, value, options) { return options.color && process.stdout.isTTY ? `\u001B[${code}m${value}\u001B[0m` : value; }
12
12
  function line(kind, value, options) { const styles = { info: ["•", "36"], success: ["✓", "32"], warning: ["!", "33"], error: ["✗", "31"] }; const [mark, code] = styles[kind]; return `${ansi(code, mark, options)} ${value}`; }
13
- function usage() { return "Usage:\n templates login [--server <Dome application URL>|--registry <Registry URL>] [--github]\n templates logout [--server <Dome application URL>|--registry <Registry URL>]\n templates doctor <pull|up|publish> [package] [--dir <directory>] [--server <URL>|--registry <URL>] [--json]\n templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]\n templates generate [--dir <directory>] [--output <file>] [--force]\n templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--verbose]\n templates publish [--dir <directory>] [--dry-run] [--server <Dome application URL>|--registry <Registry URL>]\n"; }
13
+ function usage() { return "Usage:\n templates login [--server <Dome application URL>|--registry <Registry URL>] [--github]\n templates logout [--server <Dome application URL>|--registry <Registry URL>]\n templates doctor <pull|up|publish> [package] [--dir <directory>] [--server <URL>|--registry <URL>] [--json]\n templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]\n templates generate [--dir <directory>] [--output <file>] [--force]\n templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--debug] [--verbose]\n templates publish [--dir <directory>] [--dry-run] [--server <Dome application URL>|--registry <Registry URL>]\n"; }
14
14
  function parse(argv) {
15
15
  if (!argv.length || argv.includes("--help") || argv.includes("-h")) return { help: true };
16
16
  const [command, ...rest] = argv;
17
- const options = { server: undefined, directory: undefined, output: "dome.tf", force: false, port: 3000, dryRun: false, json: false, github: false, importJob: undefined, provision: true, forceProvision: false, start: true, detach: false, verbose: false, color: !process.env.NO_COLOR };
17
+ const options = { server: undefined, directory: undefined, output: "dome.tf", force: false, port: 3000, dryRun: false, json: false, github: false, importJob: undefined, provision: true, forceProvision: false, start: true, detach: false, debug: false, verbose: false, color: !process.env.NO_COLOR };
18
18
  const values = [];
19
19
  for (let i = 0; i < rest.length; i += 1) {
20
20
  const arg = rest[i];
21
21
  if (arg === "--server" || arg === "--registry" || arg === "--dir" || arg === "--output" || arg === "--port" || arg === "--color" || arg === "--import-job") { const value = rest[++i]; if (!value) throw new Error(`${arg} requires a value.`); if (arg === "--server") options.server = normalizeServer(value); else if (arg === "--registry") options.registry = normalizeServer(value); else if (arg === "--dir") options.directory = value; else if (arg === "--output") options.output = value; else if (arg === "--import-job") options.importJob = value; else if (arg === "--color") { if (!["auto", "always", "never"].includes(value)) throw new Error("--color must be auto, always, or never."); options.color = value === "always" || (value === "auto" && !process.env.NO_COLOR); } else { options.port = Number(value); if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new Error("--port requires an integer from 1 to 65535."); } }
22
- else if (arg === "--github") options.github = true; else if (arg === "--dry-run") options.dryRun = true; else if (arg === "--json") options.json = true; else if (arg === "--force") options.force = true; else if (arg === "--skip-provision") options.provision = false; else if (arg === "--provision") options.forceProvision = true; else if (arg === "--no-start") options.start = false; else if (arg === "--detach") options.detach = true; else if (arg === "--verbose") options.verbose = true; else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); else values.push(arg);
22
+ else if (arg === "--github") options.github = true; else if (arg === "--dry-run") options.dryRun = true; else if (arg === "--json") options.json = true; else if (arg === "--force") options.force = true; else if (arg === "--skip-provision") options.provision = false; else if (arg === "--provision") options.forceProvision = true; else if (arg === "--no-start") options.start = false; else if (arg === "--detach") options.detach = true; else if (arg === "--debug") options.debug = true; else if (arg === "--verbose") options.verbose = true; else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); else values.push(arg);
23
23
  }
24
24
  if (options.server && options.registry) throw new Error("Use either --server or --registry, not both.");
25
25
  if (!["login", "logout", "doctor", "pull", "generate", "up", "publish"].includes(command)) throw new Error(`Unknown command: ${command}`);
@@ -60,6 +60,13 @@ function generate(options, output) {
60
60
  output(line("success", `Generated ${file}`, options));
61
61
  }
62
62
  function run(command, args, cwd) { const result = spawnSync(command, args, { cwd, stdio: "inherit" }); if (result.error || result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed.`); }
63
+ export function composeCommands({ detach = false } = {}) {
64
+ const prefix = ["compose", "-f", ".dome-compose.yaml"];
65
+ return {
66
+ pull: [...prefix, "pull"],
67
+ up: [...prefix, "up", ...(detach ? ["--detach"] : [])],
68
+ };
69
+ }
63
70
  function json(text, command) { try { return JSON.parse(text.slice(text.indexOf("{"))); } catch { throw new Error(`${command} did not return valid JSON.`); } }
64
71
  function runJSON(command, args, cwd, { reveal = false } = {}) {
65
72
  return new Promise((resolve, reject) => {
@@ -88,7 +95,7 @@ export function importMadeNoChanges(job) { const summary = job?.plan_summary ??
88
95
  export function importHasPendingOutputs(job) { return (job?.outputs_state ?? job?.outputsState) === "pending"; }
89
96
  export function importCreatedResources(job) { const summary = job?.plan_summary ?? job?.planSummary; return Number(summary?.create_count ?? summary?.createCount ?? 0) > 0; }
90
97
  async function configureRuntime(target, jobID, options, output) { const gateways = await runJSON("dome", ["gateways", "list", "--json"], target); const gatewayURL = gateways?.data?.gateways?.find((candidate) => candidate.is_default)?.endpoints?.gateway_url; if (!gatewayURL) throw new Error("The import did not configure a default Gateway for the runtime."); const revealed = await runJSON("dome", ["import", "outputs", jobID, "--json"], target); const tokens = Object.entries(revealed?.data?.outputs ?? {}).filter(([key, value]) => key.endsWith("_token") && typeof value === "string"); if (tokens.length !== 1) throw new Error("The import did not produce exactly one runtime agent token."); writeRuntimeEnvironment(target, { DOME_TOKEN: tokens[0][1], DOME_GATEWAY_URL: gatewayURL }); output(line("success", "Configured the runtime with the imported agent credentials.", options)); }
91
- async function up(options, output) { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readManifest(target); const needsCredentials = !configured(target); output(line("info", `Checking ${target}`, options)); fs.writeFileSync(path.join(target, ".dome-compose.yaml"), renderCompose(manifest.app.runtime, options.port)); if (options.importJob) { output(line("info", "Saving credentials from the selected Dome import", options)); await configureRuntime(target, options.importJob, options, output); } else if (options.provision && (needsCredentials || options.forceProvision)) { output(line("info", "Setting up Dome resources", options)); let imported; try { imported = await runJSON("dome", ["import", "dome.tf", "--json"], target); } catch (caught) { const dashboardURL = importDashboardURL(target); const details = dashboardURL ? ` View the import details: ${dashboardURL}` : ""; throw new Error(`${caught.message}${details}`); } const job = imported?.data?.job; if (!job?.id) throw new Error("Dome import did not return a job ID."); if (importCreatedResources(job) && importHasPendingOutputs(job)) await configureRuntime(target, job.id, options, output); else if (needsCredentials) throw new Error(`Dome matched existing resources and did not create a new runtime token. Recover a pending import with templates up --dir ${options.directory ?? "."} --import-job <job-id>, or rotate the runtime key and set DOME_TOKEN and DOME_GATEWAY_URL in .env.`); else output(line("success", "Dome resources are already up to date; keeping the existing runtime credentials.", options)); } if (options.start) { output(line("info", `Starting app at http://localhost:${options.port}`, options)); run("docker", ["compose", "-f", ".dome-compose.yaml", "up", ...(options.detach ? ["--detach"] : [])], target); } }
98
+ async function up(options, output) { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readManifest(target); const needsCredentials = !configured(target); output(line("info", `Checking ${target}`, options)); fs.writeFileSync(path.join(target, ".dome-compose.yaml"), renderCompose(manifest.app.runtime, options.port, { debug: options.debug })); if (options.importJob) { output(line("info", "Saving credentials from the selected Dome import", options)); await configureRuntime(target, options.importJob, options, output); } else if (options.provision && (needsCredentials || options.forceProvision)) { output(line("info", "Setting up Dome resources", options)); let imported; try { imported = await runJSON("dome", ["import", "dome.tf", "--json"], target); } catch (caught) { const dashboardURL = importDashboardURL(target); const details = dashboardURL ? ` View the import details: ${dashboardURL}` : ""; throw new Error(`${caught.message}${details}`); } const job = imported?.data?.job; if (!job?.id) throw new Error("Dome import did not return a job ID."); if (importCreatedResources(job) && importHasPendingOutputs(job)) await configureRuntime(target, job.id, options, output); else if (needsCredentials) throw new Error(`Dome matched existing resources and did not create a new runtime token. Recover a pending import with templates up --dir ${options.directory ?? "."} --import-job <job-id>, or rotate the runtime key and set DOME_TOKEN and DOME_GATEWAY_URL in .env.`); else output(line("success", "Dome resources are already up to date; keeping the existing runtime credentials.", options)); } if (options.start) { const compose = composeCommands(options); output(line("info", "Refreshing runtime image", options)); run("docker", compose.pull, target); output(line("info", `Starting app at http://localhost:${options.port}`, options)); run("docker", compose.up, target); } }
92
99
  async function publish(options, output, fetchImplementation) { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readPublishManifest(target); const source = publishSource(target); const name = manifest.metadata.name; const [scope, slug] = name.split("/"); const archive = options.dryRun ? null : createArchive(source); output(line("info", `Checking ${name}@${manifest.metadata.version}`, options)); const payload = { name, version: manifest.metadata.version, title: manifest.metadata.title, summary: manifest.metadata.summary ?? "", sha256: archive?.sha256, size_bytes: archive?.buffer.length, source: { repository: manifest.metadata.source.repository, path: manifest.metadata.source.path, commit: source.commit, remote: source.remote, files: source.files } };
93
100
  const remote = remoteTarget(options); let registry; let attempt;
94
101
  try { ({ target: registry, body: attempt } = await registryRequest(remote, "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation })); } catch (caught) {
package/src/runtime.mjs CHANGED
@@ -16,7 +16,8 @@ export function imageForRuntime(runtime) {
16
16
  return `${image}:${runtime.image_tag ?? "latest"}`;
17
17
  }
18
18
 
19
- export function renderCompose(runtime, hostPort = 3000) {
19
+ export function renderCompose(runtime, hostPort = 3000, { debug = false } = {}) {
20
20
  const image = imageForRuntime(runtime);
21
- return `# Generated by @domesystems/templates. Do not edit; update template.yaml instead.\nservices:\n app:\n image: ${image}\n env_file: .env\n volumes:\n - ./app:/config:ro\n - ./template.yaml:/template.yaml:ro\n ports:\n - \"${hostPort}:3000\"\n healthcheck:\n test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:3000/health', timeout=2)\"]\n interval: 30s\n timeout: 3s\n start_period: 10s\n retries: 3\n`;
21
+ const diagnostics = debug ? " environment:\n DOME_DEBUG: \"true\"\n" : "";
22
+ return `# Generated by @domesystems/templates. Do not edit; update template.yaml instead.\nservices:\n app:\n image: ${image}\n env_file: .env\n${diagnostics} volumes:\n - ./app:/config:ro\n - ./template.yaml:/template.yaml:ro\n ports:\n - \"${hostPort}:3000\"\n healthcheck:\n test: [\"CMD\", \"python\", \"-c\", \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:3000/health', timeout=2)\"]\n interval: 30s\n timeout: 3s\n start_period: 10s\n retries: 3\n`;
22
23
  }