@domesystems/templates 0.3.1 → 0.3.3

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
@@ -8,7 +8,7 @@ npx @domesystems/templates pull dome-systems/ai-agent-for-documentation --regist
8
8
  npx @domesystems/templates up --dir ai-agent-for-documentation
9
9
  ```
10
10
 
11
- Every remote command takes either `--server <Dome application URL>` or `--registry <Registry URL>`. `--server` discovers the Registry endpoint through the app server; `--registry` connects directly to an explicitly supplied Registry. Credentials are isolated per selected endpoint.
11
+ Remote commands use `https://templates-registry.domesystems.ai` by default. Pass either `--server <Dome application URL>` to discover a different Registry endpoint through an app server, or `--registry <Registry URL>` to connect directly. A successful login makes that endpoint the default for later remote commands; either flag overrides it. Credentials are isolated per selected endpoint.
12
12
 
13
13
  Use direct Registry endpoints when the app-server discovery document is unavailable:
14
14
 
@@ -28,10 +28,12 @@ templates login [--server <URL>|--registry <URL>] [--github]
28
28
  templates logout [--server <URL>|--registry <URL>]
29
29
  templates doctor <pull|up|publish> [--dir <directory>] [--server <URL>|--registry <URL>] [--json]
30
30
  templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]
31
- templates up [--dir <directory>] [--port <host-port>] [--skip-provision|--provision] [--no-start] [--detach]
31
+ templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach]
32
32
  templates publish [--dry-run] [--server <URL>|--registry <URL>]
33
33
  ```
34
34
 
35
- `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. `up` is local-only and never downloads or updates a release. `publish` reads the scoped name and SemVer version from `template.yaml`, requires a clean Git checkout, and cannot replace a published version.
35
+ `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. `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.
36
+
37
+ 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.
36
38
 
37
39
  The CLI holds no authority over package ownership or artifact access. The Registry revalidates every publish and creates short-lived artifact URLs; it does not expose raw S3 keys.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domesystems/templates",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Pull, publish, and run versioned Dome templates.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.mjs CHANGED
@@ -1,31 +1,44 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
- import readline from "node:readline/promises";
5
4
  import { readManifest, readPublishManifest } from "./manifest.mjs";
6
5
  import { imageForRuntime, renderCompose } from "./runtime.mjs";
7
- import { extractRelease, initializeEnvironment } from "./template-files.mjs";
6
+ import { extractRelease, initializeEnvironment, writeRuntimeEnvironment } from "./template-files.mjs";
8
7
  import { createArchive, publishSource } from "./publish-files.mjs";
9
- import { DEFAULT_SERVER, deleteProfile, directRegistry, discover, login, normalizeServer, readProfile, registryRequest } from "./registry.mjs";
8
+ import { DEFAULT_REGISTRY, clearSelectedTarget, deleteProfile, directRegistry, discover, login, normalizeServer, readProfile, readSelectedTarget, registryRequest, writeSelectedTarget } from "./registry.mjs";
10
9
 
11
10
  function ansi(code, value, options) { return options.color && process.stdout.isTTY ? `\u001B[${code}m${value}\u001B[0m` : value; }
12
11
  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 up [--dir <directory>] [--port <host-port>] [--skip-provision|--provision] [--no-start] [--detach] [--verbose]\n templates publish [--dry-run] [--server <Dome application URL>|--registry <Registry URL>]\n"; }
12
+ 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 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"; }
14
13
  function parse(argv) {
15
14
  if (!argv.length || argv.includes("--help") || argv.includes("-h")) return { help: true };
16
15
  const [command, ...rest] = argv;
17
- const options = { server: undefined, directory: undefined, port: 3000, dryRun: false, json: false, github: false, provision: true, forceProvision: false, start: true, detach: false, verbose: false, color: !process.env.NO_COLOR };
16
+ const options = { server: undefined, directory: undefined, 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 };
18
17
  const values = [];
19
18
  for (let i = 0; i < rest.length; i += 1) {
20
19
  const arg = rest[i];
21
- if (arg === "--server" || arg === "--registry" || arg === "--dir" || arg === "--port" || arg === "--color") { 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 === "--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."); } }
20
+ if (arg === "--server" || arg === "--registry" || arg === "--dir" || 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 === "--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
21
  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 === "--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);
23
22
  }
24
23
  if (options.server && options.registry) throw new Error("Use either --server or --registry, not both.");
25
24
  if (!["login", "logout", "doctor", "pull", "up", "publish"].includes(command)) throw new Error(`Unknown command: ${command}`);
26
25
  return { command, values, options };
27
26
  }
28
- function remoteTarget(options) { return options.registry ? directRegistry(options.registry) : options.server ?? DEFAULT_SERVER; }
27
+ function remoteTarget(options) { return options.registry ? directRegistry(options.registry) : options.server ?? readSelectedTarget() ?? directRegistry(DEFAULT_REGISTRY); }
28
+ function remoteLocation(options) { const target = remoteTarget(options); return typeof target === "string" ? `--server ${target}` : `--registry ${target.registry}`; }
29
+ const githubPublishingAccessMessage = "GitHub publishing access is required.";
30
+ function openBrowser(url) {
31
+ const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
32
+ const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
33
+ child.on("error", () => {});
34
+ child.unref();
35
+ }
36
+ async function requestGitHubPublishingAccess(remote, options, output, fetchImplementation) {
37
+ const { body } = await registryRequest(remote, "/v1/github/link/start", { method: "POST", authenticated: true, fetchImplementation });
38
+ if (typeof body.url !== "string") return;
39
+ if (options.verbose) output(line("info", `Opening GitHub authorization: ${body.url}`, options));
40
+ openBrowser(body.url);
41
+ }
29
42
  function packageReference(value) { const match = /^([a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*)(?:@(.+))?$/.exec(value ?? ""); if (!match) throw new Error("Template package must be fully qualified as scope/package or scope/package@version."); return { name: match[1], version: match[2] }; }
30
43
  function destination(name, directory) { return path.resolve(process.cwd(), directory ?? name.split("/")[1]); }
31
44
  function empty(dir) { return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0; }
@@ -40,14 +53,32 @@ async function pull(name, options, output, fetchImplementation) {
40
53
  output(line("success", `Pulled ${release.name}@${release.version}`, options)); output(` Saved to ${target}`); if (created) output(line("success", "Created .env from .env.example", options));
41
54
  }
42
55
  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.`); }
56
+ function json(text, command) { try { return JSON.parse(text.slice(text.indexOf("{"))); } catch { throw new Error(`${command} did not return valid JSON.`); } }
57
+ function runJSON(command, args, cwd, { reveal = false } = {}) {
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn(command, args, { cwd, stdio: ["inherit", "pipe", "inherit"] });
60
+ let stdout = "";
61
+ child.stdout.on("data", (chunk) => { stdout += chunk; if (reveal) process.stdout.write(chunk); });
62
+ child.on("error", () => reject(new Error(`${command} ${args.join(" ")} failed.`)));
63
+ child.on("close", (status) => status === 0 ? resolve(json(stdout, command)) : reject(new Error(`${command} ${args.join(" ")} failed.`)));
64
+ });
65
+ }
43
66
  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"))); }
44
- async function up(options, output) { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readManifest(target); output(line("info", `Checking ${target}`, options)); fs.writeFileSync(path.join(target, ".dome-compose.yaml"), renderCompose(manifest.app.runtime, options.port)); if (options.provision && (!configured(target) || options.forceProvision)) { output(line("info", "Setting up Dome resources", options)); run("dome", ["import", "dome.tf"], target); } 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); } }
67
+ 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); }
68
+ export function importHasPendingOutputs(job) { return (job?.outputs_state ?? job?.outputsState) === "pending"; }
69
+ export function importCreatedResources(job) { const summary = job?.plan_summary ?? job?.planSummary; return Number(summary?.create_count ?? summary?.createCount ?? 0) > 0; }
70
+ 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)); }
71
+ 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); } }
45
72
  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 } };
46
- const { target: registry, body: attempt } = await registryRequest(remoteTarget(options), "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation });
73
+ const remote = remoteTarget(options); let registry; let attempt;
74
+ try { ({ target: registry, body: attempt } = await registryRequest(remote, "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation })); } catch (caught) {
75
+ if (caught?.message === githubPublishingAccessMessage) await requestGitHubPublishingAccess(remote, options, output, fetchImplementation).catch(() => {});
76
+ throw caught;
77
+ }
47
78
  if (options.dryRun) { output(line("success", `Publish plan is valid for ${name}@${manifest.metadata.version}`, options)); return; }
48
79
  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(); }
49
80
  }
50
- async function doctor(subject, options, output, fetchImplementation) { const checks = []; const add = (name, passed, remediation) => checks.push({ name, passed, remediation }); try { if (subject === "pull") { const remote = remoteTarget(options); 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(remoteTarget(options).server ?? remoteTarget(options)); const location = options.registry ? `--registry ${options.registry}` : `--server ${options.server ?? DEFAULT_SERVER}`; add("Registry sign-in", Boolean(profile?.token), `Run npx @domesystems/templates login ${location}`); } else throw new Error("doctor expects pull, up, or publish."); } catch (error) { add("Preflight", false, error.message); }
51
- if (options.json) output(JSON.stringify({ server: options.registry ?? options.server ?? DEFAULT_SERVER, 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.`);
81
+ 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); }
82
+ 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.`);
52
83
  }
53
- export async function main(argv, { output = console.log, error = console.error, fetchImplementation = globalThis.fetch, sleep } = {}) { try { const parsed = parse(argv); if (parsed.help) return output(usage()); const { command, values, options } = parsed; const remote = remoteTarget(options); if (options.json) options.color = false; if (command === "login") { const destination = options.registry ?? options.server ?? DEFAULT_SERVER; output(line("info", `Opening sign-in for ${destination}`, options)); const session = await login(remote, { github: options.github, fetchImplementation, sleep, onStart: (started) => output(`Open ${started.verification_uri} and enter ${started.user_code ?? "the displayed code"}.`) }); 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); output(line("success", `Signed out of ${server}`, options)); return; } if (command === "pull") return pull(values[0], options, output, fetchImplementation); if (command === "up") return up(options, output); if (command === "publish") return publish(options, output, fetchImplementation); return doctor(values[0], options, output, fetchImplementation); } catch (caught) { error(line("error", caught.message, { color: false })); process.exitCode = 1; } }
84
+ 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 === "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/registry.mjs CHANGED
@@ -4,17 +4,20 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
6
  export const DEFAULT_SERVER = "https://app.domesystems.ai";
7
+ export const DEFAULT_REGISTRY = "https://templates-registry.domesystems.ai";
7
8
 
8
9
  function profileKey(server) {
9
10
  return crypto.createHash("sha256").update(new URL(server).origin).digest("hex").slice(0, 24);
10
11
  }
11
12
 
12
- function credentialPath(server) {
13
- const root = process.env.DOME_TEMPLATES_CONFIG_HOME
13
+ function configRoot() {
14
+ return process.env.DOME_TEMPLATES_CONFIG_HOME
14
15
  ?? path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "dome", "templates");
15
- return path.join(root, `${profileKey(server)}.json`);
16
16
  }
17
17
 
18
+ function credentialPath(server) { return path.join(configRoot(), `${profileKey(server)}.json`); }
19
+ function selectedTargetPath() { return path.join(configRoot(), "selected-target.json"); }
20
+
18
21
  export function normalizeServer(server = DEFAULT_SERVER) {
19
22
  const url = new URL(server);
20
23
  if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
@@ -31,6 +34,30 @@ export function directRegistry(server) {
31
34
  return { server: registry, registry };
32
35
  }
33
36
 
37
+ export function readSelectedTarget() {
38
+ try {
39
+ const selected = JSON.parse(fs.readFileSync(selectedTargetPath(), "utf8"));
40
+ if (selected.mode === "registry" && typeof selected.url === "string") return directRegistry(selected.url);
41
+ if (selected.mode === "server" && typeof selected.url === "string") return normalizeServer(selected.url);
42
+ } catch {}
43
+ return null;
44
+ }
45
+
46
+ export function writeSelectedTarget(target) {
47
+ const selected = typeof target === "string"
48
+ ? { mode: "server", url: normalizeServer(target) }
49
+ : { mode: "registry", url: normalizeServer(target.registry) };
50
+ fs.mkdirSync(configRoot(), { recursive: true, mode: 0o700 });
51
+ fs.writeFileSync(selectedTargetPath(), `${JSON.stringify(selected, null, 2)}\n`, { mode: 0o600 });
52
+ }
53
+
54
+ export function clearSelectedTarget(target) {
55
+ const current = readSelectedTarget();
56
+ const currentUrl = typeof current === "string" ? current : current?.registry;
57
+ const targetUrl = typeof target === "string" ? normalizeServer(target) : target.registry;
58
+ if (currentUrl === targetUrl) fs.rmSync(selectedTargetPath(), { force: true });
59
+ }
60
+
34
61
  export function readProfile(server) {
35
62
  try {
36
63
  const profile = JSON.parse(fs.readFileSync(credentialPath(server), "utf8"));
@@ -44,6 +44,25 @@ export function initializeEnvironment(templateDirectory) {
44
44
  return false;
45
45
  }
46
46
 
47
+ export function writeRuntimeEnvironment(templateDirectory, values) {
48
+ const environment = path.join(templateDirectory, ".env");
49
+ const required = ["DOME_TOKEN", "DOME_GATEWAY_URL"];
50
+ for (const key of required) {
51
+ if (typeof values[key] !== "string" || !values[key].trim() || /[\r\n]/.test(values[key])) {
52
+ throw new Error(`Cannot configure runtime: ${key} is missing or invalid.`);
53
+ }
54
+ }
55
+
56
+ let text = fs.existsSync(environment) ? fs.readFileSync(environment, "utf8") : "";
57
+ for (const key of required) {
58
+ const assignment = `${key}=${values[key]}`;
59
+ const pattern = new RegExp(`^${key}=.*$`, "m");
60
+ text = pattern.test(text) ? text.replace(pattern, assignment) : `${text}${text && !text.endsWith("\n") ? "\n" : ""}${assignment}\n`;
61
+ }
62
+ fs.writeFileSync(environment, text, { mode: 0o600 });
63
+ fs.chmodSync(environment, 0o600);
64
+ }
65
+
47
66
  export function writeLock(destination, release) {
48
67
  fs.writeFileSync(path.join(destination, ".dome-template-lock.yaml"), stringify({
49
68
  apiVersion: "templates.domesystems.ai/v1", package: release.name, version: release.version, sha256: release.sha256,