@domesystems/templates 0.1.0
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 +168 -0
- package/ai-agent-for-documentation/.env.example +12 -0
- package/ai-agent-for-documentation/app/agent.yaml +31 -0
- package/ai-agent-for-documentation/app/help.md +19 -0
- package/ai-agent-for-documentation/app/instructions.md +21 -0
- package/ai-agent-for-documentation/docker-compose.yml +18 -0
- package/ai-agent-for-documentation/dome.tf +146 -0
- package/ai-agent-for-documentation/rules/answerer.cedar +35 -0
- package/ai-agent-for-documentation/template.yaml +130 -0
- package/ai-agent-for-documentation/verification.json +7 -0
- package/ai-agent-for-notion/.env.example +19 -0
- package/ai-agent-for-notion/app/agent.yaml +32 -0
- package/ai-agent-for-notion/app/help.md +14 -0
- package/ai-agent-for-notion/app/instructions.md +16 -0
- package/ai-agent-for-notion/docker-compose.yml +18 -0
- package/ai-agent-for-notion/dome.tf +163 -0
- package/ai-agent-for-notion/guards/redact-ssn-from-pages.json +22 -0
- package/ai-agent-for-notion/rules/answerer.cedar +29 -0
- package/ai-agent-for-notion/template.yaml +142 -0
- package/ai-agent-for-notion/verification.json +7 -0
- package/ai-agent-for-release-notes/.env.example +15 -0
- package/ai-agent-for-release-notes/app/agent.yaml +36 -0
- package/ai-agent-for-release-notes/app/help.md +20 -0
- package/ai-agent-for-release-notes/app/instructions.md +19 -0
- package/ai-agent-for-release-notes/docker-compose.yml +18 -0
- package/ai-agent-for-release-notes/dome.tf +165 -0
- package/ai-agent-for-release-notes/guards/block-leaked-keys.json +38 -0
- package/ai-agent-for-release-notes/rules/release-notes.cedar +49 -0
- package/ai-agent-for-release-notes/template.yaml +155 -0
- package/bin/dome-templates.mjs +5 -0
- package/package.json +29 -0
- package/src/cli.mjs +118 -0
- package/src/manifest.mjs +22 -0
- package/src/runtime.mjs +22 -0
- package/src/template-files.mjs +28 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"json": {
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"fieldActions": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": {
|
|
8
|
+
"path": "**.text",
|
|
9
|
+
"contentMatchers": [
|
|
10
|
+
{
|
|
11
|
+
"substring": {
|
|
12
|
+
"value": "BEGIN RSA PRIVATE KEY"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"substring": {
|
|
17
|
+
"value": "BEGIN OPENSSH PRIVATE KEY"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"substring": {
|
|
22
|
+
"value": "BEGIN PRIVATE KEY"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"substring": {
|
|
27
|
+
"value": "BEGIN EC PRIVATE KEY"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"action": "FILTER_ACTION_BLOCK"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// draft-release-notes — authorization rules
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of boundary, which is why this template exists alongside the other
|
|
4
|
+
// two. The first is the familiar one: a write tool is on the gateway and denied.
|
|
5
|
+
// The second is the one MCP consolidation forces.
|
|
6
|
+
//
|
|
7
|
+
// GitHub replaced nine per-pull-request reads with a single `pull_request_read`
|
|
8
|
+
// taking a `method` — get, get_diff, get_files, get_reviews, get_commits and
|
|
9
|
+
// four more. A tool-name allow-list can only take all nine or none. Release
|
|
10
|
+
// notes are written from titles, bodies and commits; the diff is the source
|
|
11
|
+
// code, and there is no reason for it to reach a model. So the tool is
|
|
12
|
+
// permitted and the one method is refused, on the argument.
|
|
13
|
+
|
|
14
|
+
permit ( principal, action == Dome::Action::"mcp:discover", resource );
|
|
15
|
+
|
|
16
|
+
permit (
|
|
17
|
+
principal,
|
|
18
|
+
action == Dome::Action::"mcp:call",
|
|
19
|
+
resource == Dome::MCPTool::"github/list_pull_requests"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
permit (
|
|
23
|
+
principal,
|
|
24
|
+
action == Dome::Action::"mcp:call",
|
|
25
|
+
resource == Dome::MCPTool::"github/pull_request_read"
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
permit (
|
|
29
|
+
principal,
|
|
30
|
+
action == Dome::Action::"mcp:call",
|
|
31
|
+
resource == Dome::MCPTool::"github/list_commits"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// A forbid is the only thing that narrows the auto-generated Agent Form, which
|
|
35
|
+
// permits every tool attached to the gateway.
|
|
36
|
+
forbid (
|
|
37
|
+
principal,
|
|
38
|
+
action == Dome::Action::"mcp:call",
|
|
39
|
+
resource == Dome::MCPTool::"github/create_issue"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Same tool as the permit above. The gateway evaluates this against the real
|
|
43
|
+
// call arguments, so `method: get` is served and `method: get_diff` is refused.
|
|
44
|
+
forbid (
|
|
45
|
+
principal,
|
|
46
|
+
action == Dome::Action::"mcp:call",
|
|
47
|
+
resource == Dome::MCPTool::"github/pull_request_read"
|
|
48
|
+
)
|
|
49
|
+
when { resource.arguments.method == "get_diff" };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
apiVersion: templates.domesystems.ai/v1
|
|
2
|
+
kind: Template
|
|
3
|
+
|
|
4
|
+
metadata:
|
|
5
|
+
name: draft-release-notes
|
|
6
|
+
slug: ai-agent-for-release-notes
|
|
7
|
+
title: Draft release notes from merged pull requests
|
|
8
|
+
target_query: github ai agent release notes
|
|
9
|
+
summary: >-
|
|
10
|
+
A scheduled job that turns merged pull requests into a release-note draft.
|
|
11
|
+
It can read one repository's
|
|
12
|
+
pull requests and commits. It cannot open an issue, merge, or cut the
|
|
13
|
+
release — and it cannot read the source diff, which is the same tool it is
|
|
14
|
+
allowed to call, refused on one of its parameters.
|
|
15
|
+
domain: engineering
|
|
16
|
+
shape: scheduled-job
|
|
17
|
+
ui: run
|
|
18
|
+
business_pattern: consumes
|
|
19
|
+
governance_interest: [read-only, tool-scoped-access, argument-scoped-access]
|
|
20
|
+
|
|
21
|
+
# Library-only topology and documentation data. `dome.tf` is the deployable source.
|
|
22
|
+
catalog:
|
|
23
|
+
gateway:
|
|
24
|
+
name: release
|
|
25
|
+
description: Read-only GitHub surface for the release-notes job
|
|
26
|
+
default: true
|
|
27
|
+
|
|
28
|
+
connections:
|
|
29
|
+
- name: github
|
|
30
|
+
protocol: streamable-http
|
|
31
|
+
url: https://api.githubcopilot.com/mcp/
|
|
32
|
+
auth:
|
|
33
|
+
method: api-key
|
|
34
|
+
credential: shared
|
|
35
|
+
secret:
|
|
36
|
+
env: GITHUB_TOKEN
|
|
37
|
+
variable: github_token
|
|
38
|
+
label: a GitHub fine-grained personal access token
|
|
39
|
+
steps:
|
|
40
|
+
- "Create one at https://github.com/settings/personal-access-tokens/new"
|
|
41
|
+
- "Scope it to the single repository you want release notes for"
|
|
42
|
+
- "Read access to Contents and Pull requests is enough"
|
|
43
|
+
- "export GITHUB_TOKEN=github_pat_..."
|
|
44
|
+
note: "It goes into Dome, not into your app. This template never writes."
|
|
45
|
+
# `get_pull_request` used to be here and does not exist. GitHub
|
|
46
|
+
# consolidated nine per-PR reads into one `pull_request_read` taking a
|
|
47
|
+
# `method`, with no deprecated alias — and `FilterTools` drops unknown
|
|
48
|
+
# names silently, so the template would have quietly lost a tool.
|
|
49
|
+
# Verified against github/github-mcp-server, then against the live
|
|
50
|
+
# connection with `lint --live`.
|
|
51
|
+
permit:
|
|
52
|
+
- list_pull_requests
|
|
53
|
+
- pull_request_read
|
|
54
|
+
- list_commits
|
|
55
|
+
# A real write, on the gateway on purpose so the model can try it. Cedar
|
|
56
|
+
# forbids it, and a forbid beats the auto-generated Agent Form permit.
|
|
57
|
+
forbid:
|
|
58
|
+
- create_issue
|
|
59
|
+
# And the consolidation problem itself. `pull_request_read` is permitted,
|
|
60
|
+
# but one of its nine methods returns the source diff — which release
|
|
61
|
+
# notes do not need and which puts the code in the model's context. A
|
|
62
|
+
# tool-name allow-list cannot express that. Cedar on the arguments can,
|
|
63
|
+
# and the gateway enforces it on the real call, not just in `simulate`.
|
|
64
|
+
forbid_arguments:
|
|
65
|
+
- tool: pull_request_read
|
|
66
|
+
when: 'resource.arguments.method == "get_diff"'
|
|
67
|
+
why: the diff is the source code, and release notes are written from titles and commits
|
|
68
|
+
example_denied: { method: get_diff, owner: OWNER, repo: REPO, pullNumber: 1 }
|
|
69
|
+
example_allowed: { method: get, owner: OWNER, repo: REPO, pullNumber: 1 }
|
|
70
|
+
|
|
71
|
+
model:
|
|
72
|
+
name: sonnet
|
|
73
|
+
provider: anthropic
|
|
74
|
+
model_id: claude-sonnet-5
|
|
75
|
+
secret:
|
|
76
|
+
env: ANTHROPIC_API_KEY
|
|
77
|
+
variable: anthropic_api_key
|
|
78
|
+
label: an Anthropic API key
|
|
79
|
+
|
|
80
|
+
pool:
|
|
81
|
+
name: release-pool
|
|
82
|
+
default: true
|
|
83
|
+
|
|
84
|
+
agent:
|
|
85
|
+
name: release-notes-writer
|
|
86
|
+
|
|
87
|
+
rules:
|
|
88
|
+
file: rules/release-notes.cedar
|
|
89
|
+
|
|
90
|
+
guards:
|
|
91
|
+
filters:
|
|
92
|
+
- name: block-leaked-keys
|
|
93
|
+
description: Withhold a tool result carrying a private key
|
|
94
|
+
why: >-
|
|
95
|
+
Pull request bodies are prose written in a hurry, and private keys end
|
|
96
|
+
up in them. This agent pipes that prose into a model, so a leaked key
|
|
97
|
+
would be copied into a provider's context and into the draft. The
|
|
98
|
+
result is blocked rather than redacted: if a key is in the payload,
|
|
99
|
+
the right answer is that the agent does not get the payload.
|
|
100
|
+
config: guards/block-leaked-keys.json
|
|
101
|
+
bind: { tool: github, direction: response }
|
|
102
|
+
|
|
103
|
+
quotas:
|
|
104
|
+
# A headless job on a schedule is the thing most likely to run away
|
|
105
|
+
# unnoticed, so the cap is tighter than the interactive templates'.
|
|
106
|
+
- subject: agent
|
|
107
|
+
agent: release-notes-writer
|
|
108
|
+
unit: dome_usd
|
|
109
|
+
limit: 2
|
|
110
|
+
window: daily
|
|
111
|
+
name: release-notes-writer daily spend
|
|
112
|
+
# Counted in calls, not dollars, because what goes wrong with a repository
|
|
113
|
+
# crawl is a rate limit rather than a bill.
|
|
114
|
+
- subject: tool-connection
|
|
115
|
+
connection: github
|
|
116
|
+
unit: calls
|
|
117
|
+
limit: 1000
|
|
118
|
+
window: daily
|
|
119
|
+
name: github calls per day
|
|
120
|
+
|
|
121
|
+
app:
|
|
122
|
+
runtime:
|
|
123
|
+
pattern: run
|
|
124
|
+
language: python
|
|
125
|
+
agent:
|
|
126
|
+
prompt: app/instructions.md
|
|
127
|
+
help: app/help.md
|
|
128
|
+
# The run surface uses the first starter as its task prompt, and `${VAR}`
|
|
129
|
+
# is filled from the declared run parameters below. Without the repo in
|
|
130
|
+
# here the agent was told it would be "given the run parameters" and then
|
|
131
|
+
# given nothing.
|
|
132
|
+
starters:
|
|
133
|
+
- Draft release notes for pull requests merged into ${TARGET_REPO} in the
|
|
134
|
+
14 days before ${TODAY}.
|
|
135
|
+
model: claude-sonnet-5
|
|
136
|
+
max_tokens: 8192
|
|
137
|
+
skills:
|
|
138
|
+
- name: draft-release-notes
|
|
139
|
+
description: Read merged pull requests in a window and write user-facing notes.
|
|
140
|
+
tools:
|
|
141
|
+
- github/list_pull_requests
|
|
142
|
+
- github/pull_request_read
|
|
143
|
+
- github/list_commits
|
|
144
|
+
- github/create_issue
|
|
145
|
+
invoker:
|
|
146
|
+
kind: cron
|
|
147
|
+
schedule: "0 9 * * MON"
|
|
148
|
+
options:
|
|
149
|
+
identity:
|
|
150
|
+
mode: none
|
|
151
|
+
# Run parameters. `${VAR}` in a starter, prompt or help file is filled from
|
|
152
|
+
# these; the label is what `.env.example` tells the reader to put there.
|
|
153
|
+
env:
|
|
154
|
+
- name: TARGET_REPO
|
|
155
|
+
label: "the repository to draft notes for, as owner/repo — your choice, and the token must reach it"
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@domesystems/templates",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Initialize and run Dome governed-agent templates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dome-templates": "bin/dome-templates.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"ai-agent-for-documentation",
|
|
13
|
+
"ai-agent-for-notion",
|
|
14
|
+
"ai-agent-for-release-notes"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"validate": "node scripts/validate-templates.mjs",
|
|
24
|
+
"test": "node --test"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"yaml": "2.9.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { readManifest } from "./manifest.mjs";
|
|
6
|
+
import { imageForRuntime, renderCompose } from "./runtime.mjs";
|
|
7
|
+
import { copyTemplate, initializeEnvironment } from "./template-files.mjs";
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return `Usage:\n npx @domesystems/templates init <template> [--dir <directory>] [--force]\n npx @domesystems/templates up <template> [--dir <directory>] [--skip-provision] [--no-start] [--detach] [--force]\n\nCommands:\n init Copy a template locally and create .env from .env.example.\n up Initialize (when needed), generate Compose from template.yaml, provision Dome, and start it.\n\nThe generated .dome-compose.yaml is ignored by Git. Runtime images are selected\nfrom app.runtime.language and app.runtime.pattern in template.yaml.\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new Error(`${message}\n\n${usage()}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArguments(argv) {
|
|
20
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
21
|
+
const [command, slug, ...rest] = argv;
|
|
22
|
+
const options = { force: false, provision: true, start: true, detach: false, directory: undefined };
|
|
23
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
24
|
+
const argument = rest[index];
|
|
25
|
+
if (argument === "--force") options.force = true;
|
|
26
|
+
else if (argument === "--skip-provision") options.provision = false;
|
|
27
|
+
else if (argument === "--no-start") options.start = false;
|
|
28
|
+
else if (argument === "--detach") options.detach = true;
|
|
29
|
+
else if (argument === "--dir") {
|
|
30
|
+
options.directory = rest[index + 1];
|
|
31
|
+
if (!options.directory) fail("--dir requires a directory.");
|
|
32
|
+
index += 1;
|
|
33
|
+
} else {
|
|
34
|
+
fail(`Unknown option: ${argument}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!["init", "up"].includes(command)) fail(`Unknown command: ${command}`);
|
|
38
|
+
if (!slug) fail("A template slug is required.");
|
|
39
|
+
return { command, slug, options };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function templateSource(slug) {
|
|
43
|
+
if (slug.includes("/") || slug.includes("\\") || slug === "." || slug === "..") {
|
|
44
|
+
fail(`Invalid template slug: ${slug}`);
|
|
45
|
+
}
|
|
46
|
+
const source = path.join(packageRoot, slug);
|
|
47
|
+
if (!fs.statSync(path.join(source, "template.yaml"), { throwIfNoEntry: false })?.isFile()) {
|
|
48
|
+
fail(`Unknown template: ${slug}`);
|
|
49
|
+
}
|
|
50
|
+
return source;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function targetDirectory(slug, directory) {
|
|
54
|
+
return path.resolve(process.cwd(), directory ?? slug);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isEmpty(directory) {
|
|
58
|
+
return !fs.existsSync(directory) || fs.readdirSync(directory).length === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function init(slug, options, output) {
|
|
62
|
+
const source = templateSource(slug);
|
|
63
|
+
const target = targetDirectory(slug, options.directory);
|
|
64
|
+
if (!isEmpty(target) && !options.force) {
|
|
65
|
+
throw new Error(`${target} already exists and is not empty. Choose --dir or pass --force.`);
|
|
66
|
+
}
|
|
67
|
+
copyTemplate(source, target);
|
|
68
|
+
const environmentCreated = initializeEnvironment(target);
|
|
69
|
+
output(`Initialized ${slug} in ${target}.`);
|
|
70
|
+
if (environmentCreated) output("Created .env from .env.example. Add the required values before a live run.");
|
|
71
|
+
return target;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveUpDirectory(slug, options, output) {
|
|
75
|
+
const target = targetDirectory(slug, options.directory);
|
|
76
|
+
if (fs.statSync(path.join(target, "template.yaml"), { throwIfNoEntry: false })?.isFile()) return target;
|
|
77
|
+
return init(slug, options, output);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function run(command, args, cwd) {
|
|
81
|
+
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
|
|
82
|
+
if (result.error) throw new Error(`Could not run ${command}: ${result.error.message}`);
|
|
83
|
+
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited with status ${result.status}.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function main(argv, { output = console.log, error = console.error } = {}) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = parseArguments(argv);
|
|
89
|
+
if (parsed.help) {
|
|
90
|
+
output(usage());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { command, slug, options } = parsed;
|
|
94
|
+
if (command === "init") {
|
|
95
|
+
init(slug, options, output);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const target = resolveUpDirectory(slug, options, output);
|
|
100
|
+
const manifest = readManifest(target);
|
|
101
|
+
const composePath = path.join(target, ".dome-compose.yaml");
|
|
102
|
+
fs.writeFileSync(composePath, renderCompose(manifest.app.runtime));
|
|
103
|
+
output(`Generated ${path.basename(composePath)} using ${imageForRuntime(manifest.app.runtime)}.`);
|
|
104
|
+
|
|
105
|
+
if (options.provision) run("dome", ["import", "dome.tf"], target);
|
|
106
|
+
if (options.start) {
|
|
107
|
+
const args = ["compose", "-f", ".dome-compose.yaml", "up"];
|
|
108
|
+
if (options.detach) args.push("--detach");
|
|
109
|
+
run("docker", args, target);
|
|
110
|
+
}
|
|
111
|
+
if (!options.provision || !options.start) {
|
|
112
|
+
output("Skipped requested steps. Re-run without --skip-provision or --no-start when ready.");
|
|
113
|
+
}
|
|
114
|
+
} catch (caught) {
|
|
115
|
+
error(`Error: ${caught.message}`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/manifest.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseDocument } from "yaml";
|
|
4
|
+
|
|
5
|
+
export function readManifest(templateDirectory) {
|
|
6
|
+
const manifestPath = path.join(templateDirectory, "template.yaml");
|
|
7
|
+
if (!fs.statSync(manifestPath, { throwIfNoEntry: false })?.isFile()) {
|
|
8
|
+
throw new Error(`No template.yaml found in ${templateDirectory}.`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const document = parseDocument(fs.readFileSync(manifestPath, "utf8"));
|
|
12
|
+
if (document.errors.length) {
|
|
13
|
+
throw new Error(`Invalid template.yaml: ${document.errors.map((error) => error.message).join("; ")}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const manifest = document.toJS() ?? {};
|
|
17
|
+
const runtime = manifest.app?.runtime;
|
|
18
|
+
if (!runtime?.pattern || !runtime?.language) {
|
|
19
|
+
throw new Error("template.yaml must declare app.runtime.pattern and app.runtime.language.");
|
|
20
|
+
}
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const runtimeImages = {
|
|
2
|
+
"python:chat": "ghcr.io/dome-systems/runtime-py-chat",
|
|
3
|
+
"typescript:chat": "ghcr.io/dome-systems/runtime-ts-chat",
|
|
4
|
+
"python:run": "ghcr.io/dome-systems/runtime-py-run",
|
|
5
|
+
"typescript:run": "ghcr.io/dome-systems/runtime-ts-run",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function imageForRuntime(runtime) {
|
|
9
|
+
if (runtime.image) return runtime.image;
|
|
10
|
+
const key = `${runtime.language}:${runtime.pattern}`;
|
|
11
|
+
const image = runtimeImages[key];
|
|
12
|
+
if (!image) {
|
|
13
|
+
const supported = Object.keys(runtimeImages).join(", ");
|
|
14
|
+
throw new Error(`Unsupported runtime ${key}. Set app.runtime.image explicitly, or use one of: ${supported}.`);
|
|
15
|
+
}
|
|
16
|
+
return `${image}:${runtime.image_tag ?? "latest"}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderCompose(runtime) {
|
|
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 ports:\n - \"3000: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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const excludedNames = new Set([".env", ".git", ".terraform", "node_modules", "docker-compose.yml", ".dome-compose.yaml"]);
|
|
5
|
+
|
|
6
|
+
export function copyTemplate(source, destination) {
|
|
7
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
8
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
9
|
+
if (excludedNames.has(entry.name)) continue;
|
|
10
|
+
const from = path.join(source, entry.name);
|
|
11
|
+
const to = path.join(destination, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
copyTemplate(from, to);
|
|
14
|
+
} else if (entry.isFile()) {
|
|
15
|
+
fs.copyFileSync(from, to);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function initializeEnvironment(templateDirectory) {
|
|
21
|
+
const example = path.join(templateDirectory, ".env.example");
|
|
22
|
+
const environment = path.join(templateDirectory, ".env");
|
|
23
|
+
if (fs.statSync(example, { throwIfNoEntry: false })?.isFile() && !fs.existsSync(environment)) {
|
|
24
|
+
fs.copyFileSync(example, environment);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|