@domesystems/templates 0.3.4 → 0.3.5

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
@@ -34,7 +34,11 @@ templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry
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. 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 starting Docker. `publish` reads the scoped name and SemVer version from `template.yaml`, requires a clean Git checkout, and cannot replace a published version.
38
+
39
+ ## Publishing from a GitHub organization
40
+
41
+ Run `templates publish` from the directory containing `template.yaml`. The package scope must be the repository owner's exact GitHub handle: a template in `https://github.com/acme/templates` can publish as `acme/example`, not a personal handle. Before publishing, link GitHub and authorize the Dome Templates Registry App for the source repository. For organization repositories, the linked GitHub user must be a repository administrator. Use `templates doctor publish` to check both Registry sign-in and GitHub publishing access before uploading an artifact.
38
42
 
39
43
  Use `--verbose` with a command to include its diagnostic stack trace when it fails. When publishing requires a GitHub connection, the CLI opens the authorization page automatically; complete it, then run `publish` again.
40
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domesystems/templates",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Pull, publish, and run versioned Dome templates.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@ 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 [--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] [--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;
@@ -70,12 +70,25 @@ function runJSON(command, args, cwd, { reveal = false } = {}) {
70
70
  child.on("close", (status) => status === 0 ? resolve(json(stdout, command)) : reject(new Error(`${command} ${args.join(" ")} failed.`)));
71
71
  });
72
72
  }
73
+ export function importExportURL(settingsURL) {
74
+ try {
75
+ const url = new URL(settingsURL.trim());
76
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/import-export`;
77
+ return url.toString();
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+ function importDashboardURL(cwd) {
83
+ const result = spawnSync("dome", ["open", "settings", "--quiet"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
84
+ return result.status === 0 ? importExportURL(result.stdout) : null;
85
+ }
73
86
  function configured(target) { const env = path.join(target, ".env"); return fs.existsSync(env) && ["DOME_TOKEN", "DOME_GATEWAY_URL"].every((key) => new RegExp(`^${key}=(?!\\s*$|undefined|\"\")`, "m").test(fs.readFileSync(env, "utf8"))); }
74
87
  export function importMadeNoChanges(job) { const summary = job?.plan_summary ?? job?.planSummary; if (!summary) return false; return ["create_count", "update_count", "import_count"].every((key) => Number(summary[key] ?? summary[key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())] ?? 0) === 0); }
75
88
  export function importHasPendingOutputs(job) { return (job?.outputs_state ?? job?.outputsState) === "pending"; }
76
89
  export function importCreatedResources(job) { const summary = job?.plan_summary ?? job?.planSummary; return Number(summary?.create_count ?? summary?.createCount ?? 0) > 0; }
77
90
  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)); }
78
- 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)); const imported = await runJSON("dome", ["import", "dome.tf", "--json"], target); 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); } }
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); } }
79
92
  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 } };
80
93
  const remote = remoteTarget(options); let registry; let attempt;
81
94
  try { ({ target: registry, body: attempt } = await registryRequest(remote, "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation })); } catch (caught) {
@@ -85,7 +98,7 @@ async function publish(options, output, fetchImplementation) { const target = pa
85
98
  if (options.dryRun) { output(line("success", `Publish plan is valid for ${name}@${manifest.metadata.version}`, options)); return; }
86
99
  try { output(line("info", "Uploading release artifact", options)); const upload = await fetchImplementation(attempt.upload_url, { method: "PUT", headers: { "content-type": "application/zip" }, body: archive.buffer }); if (!upload.ok) throw new Error(`Artifact upload failed: ${upload.status} ${upload.statusText}.`); const { body: published } = await registryRequest(registry, `/v1/publish-attempts/${encodeURIComponent(attempt.id)}/complete`, { method: "POST", body: { sha256: archive.sha256, size_bytes: archive.buffer.length }, authenticated: true, fetchImplementation }); output(line("success", `Published ${published.name ?? name}@${published.version ?? manifest.metadata.version}`, options)); if (published.url) output(` ${published.url}`); } finally { archive.cleanup(); }
87
100
  }
88
- async function doctor(subject, options, output, fetchImplementation) { const checks = []; const add = (name, passed, remediation) => checks.push({ name, passed, remediation }); const remote = remoteTarget(options); try { if (subject === "pull") { if (typeof remote === "string") await discover(remote, fetchImplementation); add("Registry reachability", true); } else if (subject === "up") { const target = path.resolve(process.cwd(), options.directory ?? "."); readManifest(target); add("template.yaml", true); add("Docker Compose", spawnSync("docker", ["compose", "version"], { stdio: "ignore" }).status === 0, "Install Docker Desktop or Docker Engine with the Compose plugin."); add("Dome CLI", spawnSync("dome", ["--version"], { stdio: "ignore" }).status === 0, "Install the Dome CLI."); } else if (subject === "publish") { readPublishManifest(path.resolve(process.cwd(), options.directory ?? ".")); publishSource(path.resolve(process.cwd(), options.directory ?? ".")); const profile = readProfile(remote.server ?? remote); add("Registry sign-in", Boolean(profile?.token), `Run npx @domesystems/templates login ${remoteLocation(options)}`); } else throw new Error("doctor expects pull, up, or publish."); } catch (error) { add("Preflight", false, error.message); }
101
+ async function doctor(subject, options, output, fetchImplementation) { const checks = []; const add = (name, passed, remediation) => checks.push({ name, passed, remediation }); const remote = remoteTarget(options); try { if (subject === "pull") { if (typeof remote === "string") await discover(remote, fetchImplementation); add("Registry reachability", true); } else if (subject === "up") { const target = path.resolve(process.cwd(), options.directory ?? "."); readManifest(target); add("template.yaml", true); add("Docker Compose", spawnSync("docker", ["compose", "version"], { stdio: "ignore" }).status === 0, "Install Docker Desktop or Docker Engine with the Compose plugin."); add("Dome CLI", spawnSync("dome", ["--version"], { stdio: "ignore" }).status === 0, "Install the Dome CLI."); } else if (subject === "publish") { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readPublishManifest(target); const source = publishSource(target); const profile = readProfile(remote.server ?? remote); add("Registry sign-in", Boolean(profile?.token), `Run npx @domesystems/templates login ${remoteLocation(options)}`); if (profile?.token) { const { body } = await registryRequest(remote, "/v1/github/access", { method: "POST", body: { repository: manifest.metadata.source.repository, path: manifest.metadata.source.path, commit: source.commit }, authenticated: true, fetchImplementation }); add("GitHub publishing access", body.repository_authorized === true, body.message ?? "Link GitHub, authorize the Dome Templates Registry App for the source repository, and ensure you are a repository administrator."); } } else throw new Error("doctor expects pull, up, or publish."); } catch (error) { add("Preflight", false, error.message); }
89
102
  if (options.json) output(JSON.stringify({ server: remote.registry ?? remote, checks })); else checks.forEach((check) => output(line(check.passed ? "success" : "error", check.passed ? check.name : `${check.name}: ${check.remediation}`, options))); if (checks.some((check) => !check.passed)) throw new Error(`Cannot continue until ${subject} checks pass.`);
90
103
  }
91
104
  export async function main(argv, { output = console.log, error = console.error, fetchImplementation = globalThis.fetch, sleep, exit = (code) => { process.exitCode = code; }, open = openBrowser } = {}) { let options; try { const parsed = parse(argv); if (parsed.help) return output(usage()); const { command, values } = parsed; options = parsed.options; const remote = remoteTarget(options); if (options.json) options.color = false; if (command === "login") { const destination = options.registry ?? options.server ?? (typeof remote === "string" ? remote : remote.registry); output(line("info", `Opening sign-in for ${destination}`, options)); const session = await login(remote, { github: options.github, fetchImplementation, sleep, onStart: (started) => { open(started.verification_uri); output(options.verbose ? `Opening ${started.verification_uri}.` : `If your browser didn't open, open ${started.verification_uri} and enter ${started.user_code ?? "the displayed code"}.`); } }); writeSelectedTarget(remote); output(line("success", `Signed in as ${session.session.email}`, options)); return; } if (command === "logout") { const server = remote.server ?? remote; const profile = readProfile(server); if (profile?.token) await registryRequest(remote, "/v1/auth/logout", { method: "POST", authenticated: true, fetchImplementation }); deleteProfile(server); clearSelectedTarget(remote); output(line("success", `Signed out of ${server}`, options)); return; } if (command === "pull") return await pull(values[0], options, output, fetchImplementation); if (command === "generate") return generate(options, output); if (command === "up") return await up(options, output); if (command === "publish") return await publish(options, output, fetchImplementation); return await doctor(values[0], options, output, fetchImplementation); } catch (caught) { const detail = options?.verbose && caught?.stack ? caught.stack : caught.message; error(line("info", `Error: ${detail}`, { color: false })); exit(1); } }
package/src/generate.mjs CHANGED
@@ -69,15 +69,27 @@ function modelBlocks(model, pool, gatewayLabel, variables) {
69
69
  ];
70
70
  if (secretVariable(secret)) {
71
71
  const variableName = secretVariable(secret);
72
+ // Dome shared API-key credentials are stored against the provider's
73
+ // managed header. Its default is x-api-key; Authorization is not a
74
+ // substitute and is rejected by the Management API.
75
+ const secretHeader = model.secret_header ?? "x-api-key";
72
76
  variables.add(variableName);
73
- modelLines.push(attr("auth_method", q("api-key")), attr("credential_type", q("shared")), attr("secret_values", `{ ${q(model.secret_header ?? "Authorization")} = var.${variableName} }`));
77
+ modelLines.push(
78
+ attr("auth_method", q("api-key")),
79
+ attr("credential_type", q("shared")),
80
+ attr("managed_header_name", q(secretHeader)),
81
+ attr("secret_values", `{ ${q(secretHeader)} = var.${variableName} }`),
82
+ );
74
83
  }
75
84
  return [
76
85
  block("dome_llm_connection", modelLabel, modelLines),
77
86
  block("dome_llm_pool", poolLabel, [
78
87
  attr("workspace_id", "var.workspace_id"),
79
88
  attr("name", q(pool.name)),
80
- attr("is_default", String(Boolean(pool.default))),
89
+ // Generated templates must coexist with any pool already installed in
90
+ // the target workspace. A workspace permits only one default pool, so
91
+ // never make a reusable template's pool the default during provisioning.
92
+ attr("is_default", "false"),
81
93
  ]),
82
94
  block("dome_llm_pool_member", modelLabel, [
83
95
  attr("workspace_id", "var.workspace_id"),