Skip to main content

newgit_core/
templates.rs

1/// Starter resource templates. `companions` are additional resource
2/// definitions the template depends on, and `companion_trackers` are lanes
3/// it deposits into; both are created alongside it when absent (e.g. the
4/// user-owned `pnpm-store` that `deps` depends on, or the `db-snapshots`
5/// lane a Postgres dump lands in).
6#[derive(Debug, Clone, Copy)]
7pub struct ResourceTemplate {
8    pub name: &'static str,
9    pub description: &'static str,
10    pub contents: &'static str,
11    pub companions: &'static [CompanionFile],
12    pub companion_trackers: &'static [CompanionTracker],
13}
14
15#[derive(Debug, Clone, Copy)]
16pub struct CompanionFile {
17    pub name: &'static str,
18    pub contents: &'static str,
19}
20
21/// A tracker a template's checkpoint deposits into. Created through the same
22/// CLI path as `newgit tracker create`, so trackers stay CLI-managed and
23/// templates never hand-write tracker TOML.
24#[derive(Debug, Clone, Copy)]
25pub struct CompanionTracker {
26    pub name: &'static str,
27    pub audience: &'static str,
28    pub merge_with_source: bool,
29}
30
31pub const RESOURCE_TEMPLATES: &[ResourceTemplate] = &[
32    ResourceTemplate {
33        name: "process",
34        description: "a branch-local long-running process with its own port",
35        companions: &[],
36        companion_trackers: &[],
37        contents: r#"kind = "process"
38ownership = "branch"
39# Trackers or resources that must be ready first, e.g. ["deps", "runtime-env"].
40depends_on = []
41
42[ports]
43app = { start = 3100, env = "PORT" }
44
45[actions.start]
46# Edit to your dev command, e.g. "pnpm dev" or "bin/rails server".
47command = "npm run dev"
48long_running = true
49
50[actions.stop]
51signal = "term"
52
53[exports]
54APP_URL = "http://127.0.0.1:{{ports.app}}"
55
56# Most tools read their port from a committed config file rather than argv.
57# Uncomment and point this at yours. There is no template file: `find` names
58# the project's working default, so a clone without newgit still starts on it.
59#
60# `find` is literal, never a regex, and must match exactly once — which is
61# also the drift detector. When the default changes upstream, the bind fails
62# naming the file and the string instead of quietly doing nothing.
63#
64# [[render]]
65# path = "vite.config.ts"
66# replace = [
67#   { find = "port: 3000", with = "port: {{ports.app}}" },
68# ]
69"#,
70    },
71    ResourceTemplate {
72        name: "pnpm",
73        description: "dependency install via pnpm; recomputed per instance, hardlinked from one store",
74        // The shared package store is user-owned: newgit must never delete
75        // or rewrite it. Created alongside so `depends_on` resolves.
76        companions: &[CompanionFile {
77            name: "pnpm-store",
78            contents: r#"kind = "external-store"
79ownership = "user"
80
81[checkpoint]
82mode = "hash"
83paths = ["pnpm-lock.yaml"]
84
85[restore]
86mode = "none"
87"#,
88        }],
89        companion_trackers: &[],
90        contents: r#"# Every instance installs its own dependencies: two branches with
91# different lockfiles must not share a tree, or one branch's install
92# rewrites the other's. What that costs is your package manager's call.
93# pnpm hardlinks from one shared store, so instance ten adds directory
94# entries, not gigabytes; `npm ci` expands a full copy every time.
95kind = "command"
96ownership = "workspace"
97depends_on = ["pnpm-store"]
98
99[identity]
100paths = ["package.json", "pnpm-lock.yaml"]
101
102[actions.prepare]
103command = "pnpm install --frozen-lockfile"
104
105[checkpoint]
106mode = "hash"
107paths = ["package.json", "pnpm-lock.yaml"]
108
109[restore]
110mode = "recompute"
111action = "prepare"
112"#,
113    },
114    ResourceTemplate {
115        name: "command-snapshot",
116        description: "a daemon-owned database captured through the daemon into a tracker",
117        companions: &[],
118        // The dump has to land somewhere versioned; this is the one seam
119        // between the two primitives, so the lane ships with the template.
120        companion_trackers: &[CompanionTracker {
121            name: "db-snapshots",
122            audience: "project-devs",
123            merge_with_source: false,
124        }],
125        contents: r#"kind = "command-snapshot"
126ownership = "branch"
127
128# A branch-local database, named after the instance so instances never share
129# one. Edit the commands for your database; the shape is what matters:
130# checkpoint emits a dump into the lane, restore reads it back.
131
132[actions.prepare]
133command = "createdb {{branch.slug}} || true"
134
135# A convenience command, not a lifecycle hook: no stage runs `migrate`, and
136# nothing but you ever will — `newgit action <this resource>.migrate`. Only
137# `prepare` is run for you (by `spawn`, and by a `recompute` restore).
138[actions.migrate]
139command = "npm run db:migrate"
140
141[checkpoint]
142mode = "command"
143command = "pg_dump {{branch.slug}} > {{snapshot.path}}/db.sql && echo {{snapshot.path}}/db.sql"
144into_tracker = "db-snapshots"
145
146[restore]
147mode = "command"
148command = "dropdb {{branch.slug}} --if-exists && createdb {{branch.slug}} && psql --quiet {{branch.slug}} < {{state_ref}}"
149
150[cleanup]
151command = "dropdb {{branch.slug}} --if-exists"
152
153[exports]
154DATABASE_URL = "postgres://localhost/{{branch.slug}}"
155"#,
156    },
157    ResourceTemplate {
158        name: "external",
159        description: "a resource another system owns; newgit holds only a handle",
160        companions: &[],
161        companion_trackers: &[],
162        contents: r#"kind = "external"
163ownership = "external"
164
165# newgit does not own this resource, so it never assumes deletion semantics
166# it did not author: `ownership = "external"` means cleanup runs exactly the
167# command below and nothing else.
168#
169# `captures` reads names out of the prepare command's stdout — either a flat
170# JSON object or KEY=VALUE lines — and publishes them as this resource's
171# exports, so `newgit run` and the hooks below can use them.
172#
173# When `captures` is set, stdout belongs to newgit: send anything else the
174# command prints to stderr, or a chatty CLI's progress output will be
175# interleaved with the values and they will not parse. A declared name that
176# never turns up is reported as a warning, not an error.
177
178[actions.prepare]
179# Edit to your provisioning command.
180command = "cloudctl preview create --branch {{branch.name}} --json"
181captures = ["PREVIEW_ID", "PREVIEW_URL"]
182
183[checkpoint]
184mode = "external"
185state_ref = "{{exports.PREVIEW_ID}}"
186
187[restore]
188# The handle is recorded, not re-created: an undo does not rewind another
189# system. Change to `mode = "command"` if yours can be rewound.
190mode = "external"
191
192[cleanup]
193command = "cloudctl preview delete {{state_ref}}"
194"#,
195    },
196];
197
198pub fn resource_template(name: &str) -> Option<&'static ResourceTemplate> {
199    RESOURCE_TEMPLATES
200        .iter()
201        .find(|template| template.name == name)
202}