@domesystems/templates 0.3.0 → 0.3.1

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
@@ -3,22 +3,33 @@
3
3
  Pull immutable Dome template releases, publish public templates from GitHub, and run an existing local directory.
4
4
 
5
5
  ```bash
6
- npx @domesystems/templates login --github
7
- npx @domesystems/templates pull dome-systems/ai-agent-for-documentation --dir ai-agent-for-documentation
6
+ npx @domesystems/templates login --registry https://templates-registry.domesystems.ai --github
7
+ npx @domesystems/templates pull dome-systems/ai-agent-for-documentation --registry https://templates-registry.domesystems.ai --dir ai-agent-for-documentation
8
8
  npx @domesystems/templates up --dir ai-agent-for-documentation
9
9
  ```
10
10
 
11
- Every remote command takes `--server <Dome application URL>`. The selected app server discovers its Registry endpoint; the CLI never derives one by rewriting hostnames. Credentials are isolated per server profile.
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.
12
+
13
+ Use direct Registry endpoints when the app-server discovery document is unavailable:
14
+
15
+ ```bash
16
+ # Production
17
+ npx @domesystems/templates login --registry https://templates-registry.domesystems.ai
18
+ npx @domesystems/templates pull dome-systems/ai-agent-for-documentation --registry https://templates-registry.domesystems.ai
19
+
20
+ # Development
21
+ npx @domesystems/templates login --registry https://templates-registry-dev.domesystems.ai
22
+ ```
12
23
 
13
24
  ## Commands
14
25
 
15
26
  ```text
16
- templates login [--server <URL>] [--github]
17
- templates logout [--server <URL>]
18
- templates doctor <pull|up|publish> [--dir <directory>] [--server <URL>] [--json]
19
- templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>]
27
+ templates login [--server <URL>|--registry <URL>] [--github]
28
+ templates logout [--server <URL>|--registry <URL>]
29
+ templates doctor <pull|up|publish> [--dir <directory>] [--server <URL>|--registry <URL>] [--json]
30
+ templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]
20
31
  templates up [--dir <directory>] [--port <host-port>] [--skip-provision|--provision] [--no-start] [--detach]
21
- templates publish [--dry-run] [--server <URL>]
32
+ templates publish [--dry-run] [--server <URL>|--registry <URL>]
22
33
  ```
23
34
 
24
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domesystems/templates",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Pull, publish, and run versioned Dome templates.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.mjs CHANGED
@@ -6,11 +6,11 @@ import { readManifest, readPublishManifest } from "./manifest.mjs";
6
6
  import { imageForRuntime, renderCompose } from "./runtime.mjs";
7
7
  import { extractRelease, initializeEnvironment } from "./template-files.mjs";
8
8
  import { createArchive, publishSource } from "./publish-files.mjs";
9
- import { DEFAULT_SERVER, deleteProfile, discover, login, normalizeServer, readProfile, registryRequest } from "./registry.mjs";
9
+ import { DEFAULT_SERVER, deleteProfile, directRegistry, discover, login, normalizeServer, readProfile, registryRequest } from "./registry.mjs";
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>] [--github]\n templates logout [--server <Dome application URL>]\n templates doctor <pull|up|publish> [package] [--dir <directory>] [--server <URL>] [--json]\n templates pull <scope/package[@version]> [--dir <directory>] [--server <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>]\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 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"; }
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;
@@ -18,21 +18,23 @@ function parse(argv) {
18
18
  const values = [];
19
19
  for (let i = 0; i < rest.length; i += 1) {
20
20
  const arg = rest[i];
21
- if (arg === "--server" || 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 === "--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."); } }
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."); } }
22
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 === "--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
23
  }
24
+ if (options.server && options.registry) throw new Error("Use either --server or --registry, not both.");
24
25
  if (!["login", "logout", "doctor", "pull", "up", "publish"].includes(command)) throw new Error(`Unknown command: ${command}`);
25
26
  return { command, values, options };
26
27
  }
28
+ function remoteTarget(options) { return options.registry ? directRegistry(options.registry) : options.server ?? DEFAULT_SERVER; }
27
29
  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] }; }
28
30
  function destination(name, directory) { return path.resolve(process.cwd(), directory ?? name.split("/")[1]); }
29
31
  function empty(dir) { return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0; }
30
32
  async function pull(name, options, output, fetchImplementation) {
31
33
  const ref = packageReference(name); const target = destination(ref.name, options.directory); if (!empty(target)) throw new Error(`${target} already exists and is not empty. Choose an empty --dir.`);
32
34
  output(line("info", `Fetching ${ref.name}@${ref.version ?? "latest"}`, options));
33
- const [scope, slug] = ref.name.split("/"); const { body: release } = await registryRequest(options.server ?? DEFAULT_SERVER, `/v1/packages/${scope}/${slug}${ref.version ? `/${encodeURIComponent(ref.version)}` : ""}`, { fetchImplementation });
35
+ const [scope, slug] = ref.name.split("/"); const remote = remoteTarget(options); const { body: release } = await registryRequest(remote, `/v1/packages/${scope}/${slug}${ref.version ? `/${encodeURIComponent(ref.version)}` : ""}`, { fetchImplementation });
34
36
  if (!release.name || !release.version || !release.sha256 || !release.source) throw new Error("Registry returned incomplete release metadata.");
35
- const { body: download } = await registryRequest(options.server ?? DEFAULT_SERVER, `/v1/packages/${scope}/${slug}/${encodeURIComponent(release.version)}/download-url`, { method: "POST", fetchImplementation });
37
+ const { body: download } = await registryRequest(remote, `/v1/packages/${scope}/${slug}/${encodeURIComponent(release.version)}/download-url`, { method: "POST", fetchImplementation });
36
38
  const response = await fetchImplementation(download.url, { headers: { accept: "application/zip" } }); if (!response.ok) throw new Error(`Could not download ${ref.name}: ${response.status} ${response.statusText}.`);
37
39
  fs.mkdirSync(target, { recursive: true }); await extractRelease(Buffer.from(await response.arrayBuffer()), target, release); const created = initializeEnvironment(target);
38
40
  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,11 +43,11 @@ function run(command, args, cwd) { const result = spawnSync(command, args, { cwd
41
43
  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"))); }
42
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); } }
43
45
  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 } };
44
- const { target: registry, body: attempt } = await registryRequest(options.server ?? DEFAULT_SERVER, "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation });
46
+ const { target: registry, body: attempt } = await registryRequest(remoteTarget(options), "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation });
45
47
  if (options.dryRun) { output(line("success", `Publish plan is valid for ${name}@${manifest.metadata.version}`, options)); return; }
46
- 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.server, `/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(); }
48
+ 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(); }
47
49
  }
48
- async function doctor(subject, options, output, fetchImplementation) { const checks = []; const add = (name, passed, remediation) => checks.push({ name, passed, remediation }); try { if (subject === "pull") { await discover(options.server ?? DEFAULT_SERVER, 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(options.server ?? DEFAULT_SERVER); add("Registry sign-in", Boolean(profile?.token), `Run npx @domesystems/templates login --server ${options.server ?? DEFAULT_SERVER}`); } else throw new Error("doctor expects pull, up, or publish."); } catch (error) { add("Preflight", false, error.message); }
49
- if (options.json) output(JSON.stringify({ server: 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.`);
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.`);
50
52
  }
51
- 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; if (options.json) options.color = false; if (command === "login") { output(line("info", `Opening sign-in for ${options.server ?? DEFAULT_SERVER}`, options)); const session = await login(options.server ?? DEFAULT_SERVER, { 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 = options.server ?? DEFAULT_SERVER; const profile = readProfile(server); if (profile?.token) await registryRequest(server, "/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; } }
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; } }
package/src/registry.mjs CHANGED
@@ -26,6 +26,11 @@ export function normalizeServer(server = DEFAULT_SERVER) {
26
26
  return url.toString().replace(/\/$/, "");
27
27
  }
28
28
 
29
+ export function directRegistry(server) {
30
+ const registry = normalizeServer(server);
31
+ return { server: registry, registry };
32
+ }
33
+
29
34
  export function readProfile(server) {
30
35
  try {
31
36
  const profile = JSON.parse(fs.readFileSync(credentialPath(server), "utf8"));
@@ -60,8 +65,12 @@ export async function discover(server, fetchImplementation = globalThis.fetch) {
60
65
  return { server: selected, registry: endpoint.toString().replace(/\/$/, "") };
61
66
  }
62
67
 
68
+ async function targetFor(server, fetchImplementation) {
69
+ return typeof server === "string" ? discover(server, fetchImplementation) : server;
70
+ }
71
+
63
72
  export async function registryRequest(server, pathname, { method = "GET", body, authenticated = false, fetchImplementation = globalThis.fetch } = {}) {
64
- const target = await discover(server, fetchImplementation);
73
+ const target = await targetFor(server, fetchImplementation);
65
74
  const headers = { accept: "application/json" };
66
75
  if (body !== undefined) headers["content-type"] = "application/json";
67
76
  if (authenticated) {
@@ -76,7 +85,7 @@ export async function registryRequest(server, pathname, { method = "GET", body,
76
85
  }
77
86
 
78
87
  export async function login(server, { github = false, fetchImplementation = globalThis.fetch, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), onStart } = {}) {
79
- const target = await discover(server, fetchImplementation);
88
+ const target = await targetFor(server, fetchImplementation);
80
89
  const response = await fetchImplementation(`${target.registry}/v1/auth/device/start`, {
81
90
  method: "POST", headers: { accept: "application/json", "content-type": "application/json" }, body: JSON.stringify({ github }),
82
91
  });