@astralarium/repo-dev 0.0.0-stage → 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/allocation.d.ts +15 -0
  4. package/dist/allocation.js +28 -0
  5. package/dist/bin/db-image.d.ts +2 -0
  6. package/dist/bin/db-image.js +29 -0
  7. package/dist/bin/run-dev.d.ts +2 -0
  8. package/dist/bin/run-dev.js +15 -0
  9. package/dist/bin/siblings-inputs.d.ts +2 -0
  10. package/dist/bin/siblings-inputs.js +20 -0
  11. package/dist/config.d.ts +240 -0
  12. package/dist/config.js +90 -0
  13. package/dist/environment.d.ts +19 -0
  14. package/dist/environment.js +45 -0
  15. package/dist/exec.d.ts +4 -0
  16. package/dist/exec.js +14 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +12 -0
  19. package/dist/launcher/hosts.d.ts +26 -0
  20. package/dist/launcher/hosts.js +152 -0
  21. package/dist/launcher/launcher.d.ts +23 -0
  22. package/dist/launcher/launcher.js +452 -0
  23. package/dist/launcher/servers.d.ts +39 -0
  24. package/dist/launcher/servers.js +154 -0
  25. package/dist/load-config.d.ts +3 -0
  26. package/dist/load-config.js +6 -0
  27. package/dist/ports.d.ts +3 -0
  28. package/dist/ports.js +32 -0
  29. package/dist/process.d.ts +43 -0
  30. package/dist/process.js +172 -0
  31. package/dist/resources.d.ts +23 -0
  32. package/dist/resources.js +74 -0
  33. package/dist/sibling-inputs.d.ts +5 -0
  34. package/dist/sibling-inputs.js +32 -0
  35. package/dist/source-hash.d.ts +2 -0
  36. package/dist/source-hash.js +29 -0
  37. package/dist/testkit/built-worker.d.ts +29 -0
  38. package/dist/testkit/built-worker.js +48 -0
  39. package/dist/testkit/env-template.d.ts +8 -0
  40. package/dist/testkit/env-template.js +14 -0
  41. package/dist/testkit/index.d.ts +8 -0
  42. package/dist/testkit/index.js +8 -0
  43. package/dist/testkit/ports.d.ts +8 -0
  44. package/dist/testkit/ports.js +20 -0
  45. package/dist/testkit/temp.d.ts +2 -0
  46. package/dist/testkit/temp.js +10 -0
  47. package/dist/testkit/test-cache.d.ts +14 -0
  48. package/dist/testkit/test-cache.js +174 -0
  49. package/dist/testkit/worker-build.d.ts +26 -0
  50. package/dist/testkit/worker-build.js +46 -0
  51. package/dist/vite.d.ts +96 -0
  52. package/dist/vite.js +254 -0
  53. package/dist/wait.d.ts +9 -0
  54. package/dist/wait.js +38 -0
  55. package/package.json +71 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astralarium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # `@astralarium/repo-dev`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Local dev launcher.
4
+ Apps declare one `DevConfig` (`defineDevConfig`).
5
+
6
+ Needs [live](https://github.com/astralarium/live), mkcert,
7
+ and Podman or Docker for an app with a database.
8
+
9
+ ```sh
10
+ pnpm dev [start|stop|build|certs|reset]
11
+ ```
12
+
13
+ Rerun `start` to apply config changes.
14
+ Checkouts get a database, ports and hostname:
15
+ `<worktree>.<domain>.localhost` for a git worktree.
16
+ `DEV_LAN=1` also serves `<stem>.local` over mDNS.
17
+
18
+ Logs: `live ps`, `live tail -n 60 <server>`.
@@ -0,0 +1,15 @@
1
+ import { type DatabaseAllocation } from "./config.ts";
2
+ /** Generated per-checkout dev state: `<root>/.local/`. */
3
+ export declare function localDir(root: string): string;
4
+ /** Per-checkout dev resources, persisted in `<localDir>/dev.json`. */
5
+ export interface Allocation {
6
+ /** The dev database; absent for an app without one. */
7
+ database?: DatabaseAllocation;
8
+ /** Dev server port by `live` session name. */
9
+ ports: Record<string, number>;
10
+ /** Debugger port by `live` session name, for servers declaring one. */
11
+ inspectorPorts: Record<string, number>;
12
+ }
13
+ /** Saved allocation in `<local>/dev.json`; missing or invalid content
14
+ * counts as none. */
15
+ export declare function readAllocation(local: string): Partial<Allocation>;
@@ -0,0 +1,28 @@
1
+ /** Per-checkout dev resources the launcher allocates. */
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { z } from "zod";
5
+ import { PORT } from "./config.js";
6
+ /** Generated per-checkout dev state: `<root>/.local/`. */
7
+ export function localDir(root) {
8
+ return resolve(root, ".local");
9
+ }
10
+ /** `dev.json` as saved: any field may be missing. */
11
+ const savedAllocation = z
12
+ .object({
13
+ database: z.object({ name: z.string().min(1), port: PORT }),
14
+ ports: z.record(z.string(), PORT),
15
+ inspectorPorts: z.record(z.string(), PORT),
16
+ })
17
+ .partial();
18
+ /** Saved allocation in `<local>/dev.json`; missing or invalid content
19
+ * counts as none. */
20
+ export function readAllocation(local) {
21
+ try {
22
+ const result = savedAllocation.safeParse(JSON.parse(readFileSync(resolve(local, "dev.json"), "utf8")));
23
+ return result.success ? result.data : {};
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /** `db-image --app <module>`: writes the database image manifest test
3
+ * runs select with `DB_TEST_IMAGE` to `.turbo/db-image.json` in the app
4
+ * module's package. */
5
+ import { resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { appArgs } from "@astralarium/repo-deploy-info/server";
8
+ import { nearestPackage } from "@astralarium/repo-package-files";
9
+ import { ExpectedError, log } from "@astralarium/repo-telemetry";
10
+ import { loadDevConfig } from "../load-config.js";
11
+ /** `.turbo/db-image.json` of the package holding `app`. */
12
+ function imageManifest(app) {
13
+ const file = resolve(app);
14
+ const pkg = nearestPackage(file);
15
+ if (!pkg)
16
+ throw new ExpectedError(`${app}: outside a package`);
17
+ return pathToFileURL(resolve(pkg.dir, ".turbo/db-image.json"));
18
+ }
19
+ try {
20
+ const { app } = appArgs(process.argv.slice(2), "Usage: db-image --app <module>", { positionals: 0 });
21
+ const { database } = await loadDevConfig(app);
22
+ if (!database)
23
+ throw new ExpectedError(`${app}: dev config declares no database`);
24
+ database.writeTestImage(imageManifest(app));
25
+ }
26
+ catch (exception) {
27
+ log.error("db_image.failed", "db-image failed", { exception });
28
+ process.exitCode = 1;
29
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /** `run-dev --app <module> [command]`: the launcher over the app module's
3
+ * `dev` config; `--help` lists its commands. */
4
+ import { appArgs } from "@astralarium/repo-deploy-info/server";
5
+ import { log } from "@astralarium/repo-telemetry";
6
+ import { main } from "../launcher/launcher.js";
7
+ import { loadDevConfig } from "../load-config.js";
8
+ try {
9
+ const { app, argv } = appArgs(process.argv.slice(2), "Usage: run-dev --app <module> [command] [options]");
10
+ await main(await loadDevConfig(app), argv);
11
+ }
12
+ catch (exception) {
13
+ log.error("run_dev.failed", "run-dev failed", { exception });
14
+ process.exitCode = 1;
15
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /** `siblings-inputs --app <module>`: refreshes each workspace package's
3
+ * sibling fingerprints (`.turbo/siblings.json`) before turbo hashes. */
4
+ import { resolve } from "node:path";
5
+ import { appArgs, loadCheckouts } from "@astralarium/repo-deploy-info/server";
6
+ import { workspaceRoot } from "@astralarium/repo-package-files";
7
+ import { ExpectedError, log } from "@astralarium/repo-telemetry";
8
+ import { writeSiblingInputs } from "../sibling-inputs.js";
9
+ try {
10
+ const { app } = appArgs(process.argv.slice(2), "Usage: siblings-inputs --app <module>", { positionals: 0 });
11
+ const checkouts = await loadCheckouts(app);
12
+ const workspace = workspaceRoot(resolve(app));
13
+ if (!workspace)
14
+ throw new ExpectedError(`${app}: outside a pnpm workspace`);
15
+ writeSiblingInputs(workspace, Object.values(checkouts.siblings));
16
+ }
17
+ catch (exception) {
18
+ log.error("siblings_inputs.failed", "siblings-inputs failed", { exception });
19
+ process.exitCode = 1;
20
+ }
@@ -0,0 +1,240 @@
1
+ import { z } from "zod";
2
+ import type { ResourceScope } from "./resources.ts";
3
+ /** Everything the shared dev tooling varies per app. */
4
+ export interface DevConfig<Database extends DatabaseProvider | undefined = DatabaseProvider | undefined> {
5
+ /**
6
+ * Workspace root: the `pnpm-workspace.yaml` directory. Holds `.local/`
7
+ * and `pnpm-lock.yaml`; labels the resources this checkout owns.
8
+ */
9
+ root: string;
10
+ /** Command developers run the launcher as, for usage and hints: `pnpm dev`. */
11
+ program: string;
12
+ /** Hostname label every checkout serves under: `app` →
13
+ * `app.localhost`, `feature-x.app.localhost`. */
14
+ domain: string;
15
+ /** The app's database; absent for an app without one. */
16
+ database?: Database;
17
+ /** Dev servers the launcher runs under `live`; names and ports unique. */
18
+ servers: readonly DevServerConfig[];
19
+ /** Keys a developer may set in `<root>/.env` or the shell, besides
20
+ * `DEV_LAN`. */
21
+ overrideKeys: readonly string[];
22
+ /**
23
+ * Process environment per server name, derived from one allocation. The
24
+ * launcher adds the listener settings each server opts into
25
+ * (`listenerEnv`) under it.
26
+ */
27
+ environment(input: DevEnvironmentInput<Database>): Record<string, Record<string, string>>;
28
+ /** What `reset` deletes, as the usage text names it: "users, decks". */
29
+ data: string;
30
+ }
31
+ /**
32
+ * Database the launcher and the test harness provision; `@astralarium/repo-postgres/provision`
33
+ * implements Postgres. Instances are named per checkout; a test run gets a
34
+ * disposable one.
35
+ */
36
+ export interface DatabaseProvider<Instance extends TestDatabase = TestDatabase> {
37
+ /** Default instance and image base name; `DB_NAME` and `--db-name` override it. */
38
+ name: string;
39
+ /**
40
+ * Claims the checkout's dev instance: `saved` while it still holds for
41
+ * `owner` (name claimable, port held by the owned running instance or
42
+ * free), else the next free name (`<base>-2`, …) and port.
43
+ */
44
+ allocate(input: DatabaseAllocationInput): Promise<DatabaseAllocation>;
45
+ /**
46
+ * Dev instance on persistent storage, schema current and the app's data
47
+ * loaded. `resources` stops an instance this call started; a running one
48
+ * is left alone.
49
+ */
50
+ ensure(resources: ResourceScope, options: EnsureDatabaseOptions): Promise<void>;
51
+ /** Deletes dev instance `name` and its data, then rebuilds `image` from refreshed bases. */
52
+ reset(name: string, image: string): void;
53
+ /** Stops dev instance `name` when `owner` may claim it; a missing runtime is fine. */
54
+ stop(name: string, owner: string): void;
55
+ /** Rebuilds `image` from refreshed bases. */
56
+ build(image: string): void;
57
+ /**
58
+ * Disposable instance on the current schema in the foreground, loaded
59
+ * from `snapshot` when given; prints its connection, resolves once
60
+ * removed.
61
+ */
62
+ runDisposable(options: DisposableDatabaseOptions): Promise<void>;
63
+ /** Writes a full dump of the source database, migrated onto the
64
+ * current schema, to stdout; everything else goes to stderr. */
65
+ upgradeDump(options: UpgradeDumpOptions): Promise<void>;
66
+ /** The app's connection string to the dev instance. */
67
+ connectionString(allocation: DatabaseAllocation): string;
68
+ /** Disposable seeded instance for one test run; `resources` removes it. */
69
+ startTestInstance(resources: ResourceScope, name: string): Promise<Instance>;
70
+ /** Writes the image manifest test runs select with `DB_TEST_IMAGE`. */
71
+ writeTestImage(file: URL): void;
72
+ }
73
+ /** What `DatabaseProvider.allocate` chooses from. */
74
+ export interface DatabaseAllocationInput {
75
+ /** Requested instance base name. */
76
+ base: string;
77
+ /** Owning checkout: `DevConfig.root`. */
78
+ owner: string;
79
+ /** Previous allocation of this checkout. */
80
+ saved: Partial<DatabaseAllocation>;
81
+ }
82
+ /** The checkout's dev instance. */
83
+ export interface DatabaseAllocation {
84
+ /** Instance name; persistent storage derives from it. */
85
+ name: string;
86
+ /** Published loopback port. */
87
+ port: number;
88
+ }
89
+ /** What `DatabaseProvider.ensure` starts. */
90
+ export interface EnsureDatabaseOptions {
91
+ allocation: DatabaseAllocation;
92
+ /** Image base name. */
93
+ image: string;
94
+ /** Owning checkout, labeled on the instance. */
95
+ owner: string;
96
+ /** `DevConfig.program`, for hints naming a launcher command. */
97
+ program: string;
98
+ }
99
+ /** What a disposable instance command runs beside. */
100
+ interface DisposableCommandOptions {
101
+ /** The checkout's dev instance: names derive from it; its port stays free. */
102
+ allocation: DatabaseAllocation;
103
+ /** Image base name. */
104
+ image: string;
105
+ /** Stops the command; its instances go. */
106
+ signal: AbortSignal;
107
+ }
108
+ /** What `DatabaseProvider.runDisposable` serves. */
109
+ export interface DisposableDatabaseOptions extends DisposableCommandOptions {
110
+ /** Server arguments appended to the image command. */
111
+ serverArgs: readonly string[];
112
+ /** Dump loaded before the instance accepts connections. */
113
+ snapshot?: {
114
+ /** Absolute path of a full dump. */
115
+ dump: string;
116
+ /** Runs each schema module's scrub after the load. */
117
+ scrub: boolean;
118
+ };
119
+ }
120
+ /** What `DatabaseProvider.upgradeDump` migrates. */
121
+ export interface UpgradeDumpOptions extends DisposableCommandOptions {
122
+ /** Absolute paths of migrate files, run on a copy of the source. */
123
+ migrations: readonly string[];
124
+ }
125
+ /** A test run's seeded instance: a database per Vitest project. */
126
+ export interface TestDatabase {
127
+ /**
128
+ * Creates the project's private database from the seeded template;
129
+ * duplicate names fail without dropping data. Returns the app's
130
+ * connection string.
131
+ */
132
+ createDatabase(name: string): Promise<string>;
133
+ /** Administrative connection to the project's database. */
134
+ adminConnectionString(name: string): string;
135
+ /** Container running the instance, for fixtures that run tools inside it. */
136
+ readonly container?: TestContainer;
137
+ }
138
+ /** Container running a test instance. */
139
+ export interface TestContainer {
140
+ /** Container runtime binary. */
141
+ runtime: string;
142
+ /** Container name. */
143
+ name: string;
144
+ }
145
+ /** The dev database as `DevConfig.environment` receives it. */
146
+ export interface DevDatabase extends DatabaseAllocation {
147
+ /** `DatabaseProvider.connectionString` of the allocation. */
148
+ connectionString: string;
149
+ }
150
+ /** One dev server under `live`. */
151
+ export interface DevServerConfig {
152
+ /** `live` session name. */
153
+ name: string;
154
+ /** App directory relative to `DevConfig.root`; runs `pnpm dev --port <port>`. */
155
+ dir: string;
156
+ /** Preferred port; a taken one moves to the next free port up. */
157
+ port: number;
158
+ /** Preferred debugger port, allocated like `port`; `servers[name].inspectorPort`. */
159
+ inspectorPort?: number;
160
+ /** Browser-facing: serves on the checkout hostname and binds
161
+ * `DEV_SERVER_HOST` (every interface with `DEV_LAN`, else `localhost`). */
162
+ host?: boolean;
163
+ /** Listens over HTTPS with the launcher's certificates. */
164
+ https?: boolean;
165
+ /** Readiness: every probe passes through the actual listener. */
166
+ ready: readonly ReadyProbe[];
167
+ }
168
+ /** Expected response of one readiness request. */
169
+ export interface ReadyProbe {
170
+ /** Path on the server's origin. */
171
+ path: string;
172
+ /** Expected status; 200 by default. */
173
+ status?: number;
174
+ /** Exact expected body; any body by default. */
175
+ body?: string;
176
+ }
177
+ /** Present override values; empty strings dropped. */
178
+ export type Overrides = Partial<Record<string, string>>;
179
+ /** The checkout's hostnames and certificate paths. */
180
+ export interface LocalHosts {
181
+ /** Loopback hostname: `<stem>.localhost`. */
182
+ hostname: string;
183
+ /** mDNS hostname: `<stem>.local`. */
184
+ lanHostname: string;
185
+ /** Hostnames `host` servers answer on: `hostname`, plus `lanHostname`
186
+ * with `DEV_LAN`. */
187
+ hostnames: readonly string[];
188
+ /** `DEV_LAN` set: `host` servers listen on every interface. */
189
+ lan: boolean;
190
+ /** TLS key path: `<localDir>/certs/<hostname>-key.pem`. */
191
+ sslKey: string;
192
+ /** TLS certificate path: `<localDir>/certs/<hostname>.pem`; covers
193
+ * `localhost`, `hostname` and `lanHostname`. */
194
+ sslCert: string;
195
+ }
196
+ /** One dev server's allocation. */
197
+ export interface DevServerAllocation {
198
+ /** Allocated listener port. */
199
+ port: number;
200
+ /** Origin on `port`: the checkout hostname for a `host` server
201
+ * (`https://app.localhost:<port>`), else `localhost`. */
202
+ origin: string;
203
+ /** Every origin on `port`: one per `LocalHosts.hostnames` entry for a
204
+ * `host` server, else `[origin]`. */
205
+ origins: readonly string[];
206
+ /** Allocated debugger port; present when the server declares one. */
207
+ inspectorPort?: number;
208
+ }
209
+ /** What `DevConfig.environment` derives the server environments from. */
210
+ export interface DevEnvironmentInput<Database extends DatabaseProvider | undefined = DatabaseProvider | undefined> {
211
+ /** Generated per-checkout state: `<root>/.local/`. */
212
+ localDir: string;
213
+ /** `DevConfig.overrideKeys` and `DEV_LAN` from the shell over
214
+ * `<root>/.env`. */
215
+ overrides: Overrides;
216
+ /** Hostnames and certificate paths. */
217
+ hosts: LocalHosts;
218
+ /** Per-checkout auth signing secret, generated once under `localDir`. */
219
+ authSecret: string;
220
+ /** The dev database; `undefined` for an app without one. */
221
+ database: Database extends DatabaseProvider ? DevDatabase : undefined;
222
+ /** Allocation by server name. */
223
+ servers: Record<string, DevServerAllocation>;
224
+ }
225
+ /** `DB_NAME` from the environment; empty or unset ⇒ `database.name`. */
226
+ export declare function envDbName(database: Pick<DatabaseProvider, "name">): string;
227
+ /** Instance, image and volume names: lowercase letters, digits, `_` and
228
+ * `-`. Container runtimes accept them. */
229
+ export declare const RESOURCE_NAME: RegExp;
230
+ /** Environment variable name. */
231
+ export declare const ENV_KEY: RegExp;
232
+ /** Absolute path of an existing directory. */
233
+ export declare function isDirectory(path: string): boolean;
234
+ /** TCP port number. */
235
+ export declare const PORT: z.ZodInt;
236
+ /** Validates the declaration, keeping the declared `database` type. */
237
+ export declare function defineDevConfig<Database extends DatabaseProvider | undefined = undefined>(config: DevConfig<Database>): DevConfig<Database> & {
238
+ database: Database;
239
+ };
240
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * App-side dev configuration: what the launcher and the test harness vary
3
+ * per app. The app owns one `DevConfig` and passes it to the shared entry
4
+ * points; `defineDevConfig` validates it.
5
+ */
6
+ import { existsSync, realpathSync, statSync } from "node:fs";
7
+ import { isAbsolute, relative, resolve } from "node:path";
8
+ import { ExpectedError } from "@astralarium/repo-telemetry";
9
+ import { z } from "zod";
10
+ /** `DB_NAME` from the environment; empty or unset ⇒ `database.name`. */
11
+ export function envDbName(database) {
12
+ const name = process.env.DB_NAME;
13
+ return name === undefined || name === "" ? database.name : name;
14
+ }
15
+ /** Instance, image and volume names: lowercase letters, digits, `_` and
16
+ * `-`. Container runtimes accept them. */
17
+ export const RESOURCE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
18
+ /** One DNS label. */
19
+ const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
20
+ /** Environment variable name. */
21
+ export const ENV_KEY = /^[A-Z_][A-Z0-9_]*$/;
22
+ /** Absolute path of an existing directory. */
23
+ export function isDirectory(path) {
24
+ try {
25
+ return isAbsolute(path) && statSync(path).isDirectory();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /** `path` is an existing directory at or below `root`, symlinks
32
+ * resolved. */
33
+ function isDirectoryUnder(root, path) {
34
+ if (!isDirectory(path))
35
+ return false;
36
+ const inside = relative(realpathSync(root), realpathSync(path));
37
+ return !inside.startsWith("..") && !isAbsolute(inside);
38
+ }
39
+ const unique = (items) => new Set(items).size === items.length;
40
+ /** TCP port number. */
41
+ export const PORT = z.int().min(1).max(65535);
42
+ const probe = z.object({
43
+ path: z.string().startsWith("/"),
44
+ status: z.int().min(100).max(599).optional(),
45
+ body: z.string().optional(),
46
+ });
47
+ const server = z.object({
48
+ name: z.string().min(1),
49
+ dir: z.string().min(1),
50
+ port: PORT,
51
+ inspectorPort: PORT.optional(),
52
+ host: z.boolean().optional(),
53
+ https: z.boolean().optional(),
54
+ ready: z.array(probe),
55
+ });
56
+ const provider = z.custom((value) => typeof value === "object" &&
57
+ value !== null &&
58
+ typeof value.name === "string" &&
59
+ typeof value.ensure === "function", { error: "database must be a DatabaseProvider" });
60
+ const devConfig = z
61
+ .object({
62
+ root: z
63
+ .string()
64
+ .refine(isDirectory, { error: "root must be an existing directory" })
65
+ .refine((root) => existsSync(resolve(root, "pnpm-workspace.yaml")), {
66
+ error: "root must hold pnpm-workspace.yaml",
67
+ }),
68
+ program: z.string().min(1),
69
+ domain: z.string().regex(DNS_LABEL),
70
+ database: provider.optional(),
71
+ servers: z
72
+ .array(server)
73
+ .refine((servers) => unique(servers.map(({ name }) => name)), {
74
+ error: "server names must be unique",
75
+ })
76
+ .refine((servers) => unique(servers.flatMap(({ port, inspectorPort }) => inspectorPort === undefined ? [port] : [port, inspectorPort])), { error: "server ports must be unique" }),
77
+ overrideKeys: z
78
+ .array(z.string().regex(ENV_KEY))
79
+ .refine(unique, { error: "overrideKeys must be unique" }),
80
+ environment: z.custom((value) => typeof value === "function", { error: "environment must be a function" }),
81
+ data: z.string().min(1),
82
+ })
83
+ .refine(({ root, servers }) => servers.every(({ dir }) => isDirectoryUnder(root, resolve(root, dir))), { error: "every server dir must be an existing directory under root" });
84
+ /** Validates the declaration, keeping the declared `database` type. */
85
+ export function defineDevConfig(config) {
86
+ const result = devConfig.safeParse(config);
87
+ if (!result.success)
88
+ throw new ExpectedError(`invalid dev config:\n${z.prettifyError(result.error)}`);
89
+ return config;
90
+ }
@@ -0,0 +1,19 @@
1
+ /** Child-process runtime settings, independent of app bindings. */
2
+ import type { DevServerConfig, LocalHosts } from "./config.ts";
3
+ /** Listener settings `server` opts into: the bind address when `host`,
4
+ * certificates when `https`, the debugger port when allocated. */
5
+ export declare function listenerEnv(hosts: LocalHosts, server: Pick<DevServerConfig, "host" | "https">, inspectorPort?: number): Record<string, string>;
6
+ /** Runtime options of a launched app process. */
7
+ export interface ProcessEnvironmentOptions {
8
+ /**
9
+ * App execution mode: `dev` runs with development semantics,
10
+ * `build` reads no dot-env; every other mode is production.
11
+ */
12
+ mode: string;
13
+ /** Named Wrangler environment; omitted selects local development. */
14
+ cloudflareEnv?: string;
15
+ /** Explicit Node flags; never inherited from the shell. */
16
+ nodeOptions?: string;
17
+ }
18
+ /** Minimal host paths plus explicit app bindings and runtime options. */
19
+ export declare function appProcessEnv(env: Record<string, string>, options: ProcessEnvironmentOptions): NodeJS.ProcessEnv;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Listener settings of a launched server, the contract between the
3
+ * launcher and the app's Vite or wrangler config (`@astralarium/repo-dev/vite`
4
+ * `devListener`, `cloudflareDevOptions`): `DEV_SERVER_HOST` (a `host`
5
+ * server's bind address: `::` with `DEV_LAN`, else `localhost`), `SSL_KEY`
6
+ * and `SSL_CERT` (an `https` server's certificate paths),
7
+ * `DEV_INSPECTOR_PORT` (the allocated debugger port). `appProcessEnv`
8
+ * blanks them all, so a dot-env file supplies none.
9
+ */
10
+ const LISTENER_KEYS = [
11
+ "DEV_SERVER_HOST",
12
+ "SSL_KEY",
13
+ "SSL_CERT",
14
+ "DEV_INSPECTOR_PORT",
15
+ ];
16
+ /** Listener settings `server` opts into: the bind address when `host`,
17
+ * certificates when `https`, the debugger port when allocated. */
18
+ export function listenerEnv(hosts, server, inspectorPort) {
19
+ return {
20
+ ...(server.host && { DEV_SERVER_HOST: hosts.lan ? "::" : "localhost" }),
21
+ ...(server.https && { SSL_KEY: hosts.sslKey, SSL_CERT: hosts.sslCert }),
22
+ ...(inspectorPort !== undefined && {
23
+ DEV_INSPECTOR_PORT: String(inspectorPort),
24
+ }),
25
+ };
26
+ }
27
+ /** Minimal host paths plus explicit app bindings and runtime options. */
28
+ export function appProcessEnv(env, options) {
29
+ const host = {};
30
+ for (const key of ["PATH", "HOME", "TMPDIR"]) {
31
+ const value = process.env[key];
32
+ if (value !== undefined)
33
+ host[key] = value;
34
+ }
35
+ return {
36
+ ...host,
37
+ // Empty overrides block Vite's app-local .env fallback.
38
+ ...Object.fromEntries(LISTENER_KEYS.map((key) => [key, ""])),
39
+ ...env,
40
+ NODE_ENV: options.mode === "dev" ? "development" : "production",
41
+ NODE_OPTIONS: options.nodeOptions,
42
+ CLOUDFLARE_ENV: options.cloudflareEnv,
43
+ CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV: options.mode === "build" ? "false" : "true",
44
+ };
45
+ }
package/dist/exec.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Exit status 0. */
2
+ export declare function succeeds(command: string, args: string[]): boolean;
3
+ /** Trimmed stdout, or null on non-zero exit. */
4
+ export declare function output(command: string, args: string[]): string | null;
package/dist/exec.js ADDED
@@ -0,0 +1,14 @@
1
+ /** Synchronous command probes; a missing binary counts as failure. */
2
+ import { spawnSync } from "node:child_process";
3
+ /** Exit status 0. */
4
+ export function succeeds(command, args) {
5
+ return spawnSync(command, args, { stdio: "ignore" }).status === 0;
6
+ }
7
+ /** Trimmed stdout, or null on non-zero exit. */
8
+ export function output(command, args) {
9
+ const result = spawnSync(command, args, {
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "ignore"],
12
+ });
13
+ return result.status === 0 ? result.stdout.trim() : null;
14
+ }
@@ -0,0 +1,12 @@
1
+ /** Dev configuration, child processes, readiness and turbo fingerprints. */
2
+ export * from "./allocation.ts";
3
+ export { type DatabaseAllocation, type DatabaseAllocationInput, type DatabaseProvider, defineDevConfig, type DevConfig, type DevDatabase, type DevEnvironmentInput, type DevServerAllocation, type DevServerConfig, type DisposableDatabaseOptions, type EnsureDatabaseOptions, envDbName, isDirectory, type LocalHosts, type Overrides, type ReadyProbe, RESOURCE_NAME, type TestContainer, type TestDatabase, type UpgradeDumpOptions, } from "./config.ts";
4
+ export { appProcessEnv, type ProcessEnvironmentOptions, } from "./environment.ts";
5
+ export * from "./exec.ts";
6
+ export * from "./load-config.ts";
7
+ export * from "./ports.ts";
8
+ export * from "./process.ts";
9
+ export * from "./resources.ts";
10
+ export * from "./sibling-inputs.ts";
11
+ export * from "./source-hash.ts";
12
+ export { isPortOpen, READY_TIMEOUT_MS, waitFor } from "./wait.ts";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /** Dev configuration, child processes, readiness and turbo fingerprints. */
2
+ export * from "./allocation.js";
3
+ export { defineDevConfig, envDbName, isDirectory, RESOURCE_NAME, } from "./config.js";
4
+ export { appProcessEnv, } from "./environment.js";
5
+ export * from "./exec.js";
6
+ export * from "./load-config.js";
7
+ export * from "./ports.js";
8
+ export * from "./process.js";
9
+ export * from "./resources.js";
10
+ export * from "./sibling-inputs.js";
11
+ export * from "./source-hash.js";
12
+ export { isPortOpen, READY_TIMEOUT_MS, waitFor } from "./wait.js";
@@ -0,0 +1,26 @@
1
+ import type { DevConfig, LocalHosts, Overrides } from "../config.ts";
2
+ /** `name` as a DNS label: lowercase, runs outside `[a-z0-9-]` as `-`,
3
+ * `-<suffix>` appended, at most 63 characters with `suffix` intact;
4
+ * empty when nothing of `name` survives. */
5
+ export declare function dnsLabel(name: string, suffix?: string): string;
6
+ /** Temp directories, real paths: `os.tmpdir()`, macOS's per-user temp
7
+ * directory (independent of `TMPDIR`), `/tmp`, `/var/tmp`. */
8
+ export declare function tempRoots(): string[];
9
+ /**
10
+ * The checkout's hostname stem: `<domain>` for a repository's main
11
+ * worktree, `<name>.<domain>` for a linked worktree (git's worktree name)
12
+ * or a checkout under a temp directory (its first directory below it,
13
+ * plus a hash of its path when deeper: `copy-1-ab12cd`). The temp-directory
14
+ * rule wins over git's worktree name.
15
+ */
16
+ export declare function checkoutStem(app: Pick<DevConfig, "root" | "domain">, temps?: readonly string[]): string;
17
+ /** Hostnames and certificate paths of hostname stem `stem`. */
18
+ export declare function localHosts(local: string, overrides: Overrides, stem: string): LocalHosts;
19
+ /** IPv4 address `DEV_LAN` advertises: the default route interface's.
20
+ * Throws `ExpectedError` without `dns-sd` or a default route. */
21
+ export declare function lanAddress(): string;
22
+ /** Stops the checkout's mDNS advertisement; a missing session is fine. */
23
+ export declare function stopAdvertising(root: string): void;
24
+ /** Advertises `hosts.lanHostname` → `address` over mDNS (`dns-sd`)
25
+ * under `live`, replacing a running advertisement. */
26
+ export declare function advertiseHost(root: string, hosts: LocalHosts, address: string, port: number): void;