otavia 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.
Files changed (63) hide show
  1. package/bun.lock +589 -0
  2. package/package.json +35 -0
  3. package/src/cli.ts +153 -0
  4. package/src/commands/__tests__/aws-auth.test.ts +32 -0
  5. package/src/commands/__tests__/cell.test.ts +44 -0
  6. package/src/commands/__tests__/dev.test.ts +49 -0
  7. package/src/commands/__tests__/init.test.ts +47 -0
  8. package/src/commands/__tests__/setup.test.ts +263 -0
  9. package/src/commands/aws-auth.ts +32 -0
  10. package/src/commands/aws.ts +59 -0
  11. package/src/commands/cell.ts +33 -0
  12. package/src/commands/clean.ts +32 -0
  13. package/src/commands/deploy.ts +508 -0
  14. package/src/commands/dev/__tests__/fixtures/gateway-cell/cell.yaml +8 -0
  15. package/src/commands/dev/__tests__/gateway-backend-routes.test.ts +13 -0
  16. package/src/commands/dev/__tests__/gateway-forward-url.test.ts +20 -0
  17. package/src/commands/dev/__tests__/gateway-sso-base-url.test.ts +93 -0
  18. package/src/commands/dev/__tests__/tunnel.test.ts +93 -0
  19. package/src/commands/dev/__tests__/vite-dev-proxy-rules.test.ts +220 -0
  20. package/src/commands/dev/__tests__/well-known.test.ts +88 -0
  21. package/src/commands/dev/forward-url.ts +7 -0
  22. package/src/commands/dev/gateway.ts +421 -0
  23. package/src/commands/dev/main-frontend-runtime/main-entry.ts +35 -0
  24. package/src/commands/dev/main-frontend-runtime/vite-config.ts +210 -0
  25. package/src/commands/dev/mount-selection.ts +9 -0
  26. package/src/commands/dev/tunnel.ts +176 -0
  27. package/src/commands/dev/vite-dev.ts +382 -0
  28. package/src/commands/dev/well-known.ts +76 -0
  29. package/src/commands/dev.ts +107 -0
  30. package/src/commands/init.ts +69 -0
  31. package/src/commands/lint.ts +49 -0
  32. package/src/commands/setup.ts +887 -0
  33. package/src/commands/test.ts +331 -0
  34. package/src/commands/typecheck.ts +36 -0
  35. package/src/config/__tests__/load-cell-yaml.test.ts +248 -0
  36. package/src/config/__tests__/load-otavia-yaml.test.ts +492 -0
  37. package/src/config/__tests__/ports.test.ts +48 -0
  38. package/src/config/__tests__/resolve-cell-dir.test.ts +60 -0
  39. package/src/config/__tests__/resolve-params.test.ts +137 -0
  40. package/src/config/__tests__/resource-names.test.ts +62 -0
  41. package/src/config/cell-yaml-schema.ts +115 -0
  42. package/src/config/load-cell-yaml.ts +87 -0
  43. package/src/config/load-otavia-yaml.ts +256 -0
  44. package/src/config/otavia-yaml-schema.ts +49 -0
  45. package/src/config/ports.ts +57 -0
  46. package/src/config/resolve-cell-dir.ts +55 -0
  47. package/src/config/resolve-params.ts +160 -0
  48. package/src/config/resource-names.ts +60 -0
  49. package/src/deploy/__tests__/template.test.ts +137 -0
  50. package/src/deploy/api-gateway.ts +96 -0
  51. package/src/deploy/cloudflare-dns.ts +261 -0
  52. package/src/deploy/cloudfront.ts +228 -0
  53. package/src/deploy/dynamodb.ts +68 -0
  54. package/src/deploy/lambda.ts +121 -0
  55. package/src/deploy/s3.ts +57 -0
  56. package/src/deploy/template.ts +264 -0
  57. package/src/deploy/types.ts +16 -0
  58. package/src/local/docker.ts +175 -0
  59. package/src/local/dynamodb-local.ts +124 -0
  60. package/src/local/minio-local.ts +44 -0
  61. package/src/utils/env.test.ts +74 -0
  62. package/src/utils/env.ts +79 -0
  63. package/tsconfig.json +14 -0
@@ -0,0 +1,107 @@
1
+ import { resolve } from "node:path";
2
+ import { checkAwsCredentials } from "./aws-auth.js";
3
+ import { runGatewayDev } from "./dev/gateway.js";
4
+ import { startViteDev } from "./dev/vite-dev.js";
5
+ import { startTunnel } from "./dev/tunnel.js";
6
+ import { loadEnvForCell } from "../utils/env.js";
7
+ import { resolvePortsFromEnv } from "../config/ports.js";
8
+
9
+ export function resolveDevPublicBaseUrl(options: {
10
+ tunnelEnabled?: boolean;
11
+ tunnelPublicBaseUrl?: string;
12
+ gatewayOnly: boolean;
13
+ vitePort: number;
14
+ }): string | undefined {
15
+ if (options.tunnelEnabled) return options.tunnelPublicBaseUrl;
16
+ if (options.gatewayOnly) return undefined;
17
+ return `http://localhost:${options.vitePort}`;
18
+ }
19
+
20
+ export function resolveDevTunnelEnabled(options?: { tunnel?: boolean }): boolean {
21
+ return options?.tunnel ?? false;
22
+ }
23
+
24
+ /**
25
+ * Dev command: validate otavia.yaml, start backend gateway, then Vite dev server.
26
+ * When OTAVIA_DEV_GATEWAY_ONLY=1 (e.g. for e2e), only run gateway with PORT and optional
27
+ * DYNAMODB_ENDPOINT/S3_ENDPOINT overrides; do not start Vite.
28
+ * On SIGINT/SIGTERM stops and exits.
29
+ */
30
+ export async function devCommand(
31
+ rootDir: string,
32
+ options?: { tunnel?: boolean; tunnelHost?: string; tunnelConfig?: string; tunnelProtocol?: string }
33
+ ): Promise<void> {
34
+ const root = resolve(rootDir);
35
+ const aws = await checkAwsCredentials(root);
36
+ if (!aws.ok) {
37
+ console.error(
38
+ `AWS credentials are invalid or expired for profile "${aws.profile}".`
39
+ );
40
+ console.error("Run: bun run otavia aws login");
41
+ process.exit(1);
42
+ }
43
+ const stageEnv = loadEnvForCell(root, root, { stage: "dev" });
44
+ const ports = resolvePortsFromEnv("dev", { ...stageEnv, ...process.env });
45
+ const backendPort = ports.backend;
46
+ const vitePort = ports.frontend;
47
+ const gatewayOnly = process.env.OTAVIA_DEV_GATEWAY_ONLY === "1";
48
+ const overrides: { dynamoEndpoint?: string; s3Endpoint?: string } | undefined = gatewayOnly
49
+ ? (process.env.DYNAMODB_ENDPOINT || process.env.S3_ENDPOINT
50
+ ? {
51
+ dynamoEndpoint: process.env.DYNAMODB_ENDPOINT,
52
+ s3Endpoint: process.env.S3_ENDPOINT,
53
+ }
54
+ : undefined)
55
+ : undefined;
56
+
57
+ let tunnelHandle: { publicBaseUrl: string; stop: () => void } | undefined;
58
+ let publicBaseUrl: string | undefined;
59
+ const tunnelEnabled = resolveDevTunnelEnabled(options);
60
+ if (tunnelEnabled) {
61
+ tunnelHandle = await startTunnel(root, {
62
+ tunnelConfigPath: options?.tunnelConfig,
63
+ tunnelHost: options?.tunnelHost,
64
+ tunnelProtocol: options?.tunnelProtocol,
65
+ });
66
+ publicBaseUrl = tunnelHandle.publicBaseUrl;
67
+ console.log(`[tunnel] Started. Public base URL: ${publicBaseUrl}`);
68
+ }
69
+
70
+ const effectivePublicBaseUrl = resolveDevPublicBaseUrl({
71
+ tunnelEnabled,
72
+ tunnelPublicBaseUrl: publicBaseUrl,
73
+ gatewayOnly,
74
+ vitePort,
75
+ });
76
+ const server = await runGatewayDev(root, backendPort, overrides, {
77
+ publicBaseUrl: effectivePublicBaseUrl,
78
+ dynamodbPort: ports.dynamodb,
79
+ minioPort: ports.minio,
80
+ });
81
+
82
+ if (gatewayOnly) {
83
+ process.on("SIGINT", () => {
84
+ tunnelHandle?.stop();
85
+ server.stop();
86
+ process.exit(0);
87
+ });
88
+ process.on("SIGTERM", () => {
89
+ tunnelHandle?.stop();
90
+ server.stop();
91
+ process.exit(0);
92
+ });
93
+ await new Promise(() => {});
94
+ }
95
+
96
+ const viteHandle = await startViteDev(root, backendPort, vitePort, effectivePublicBaseUrl);
97
+
98
+ const cleanup = () => {
99
+ tunnelHandle?.stop();
100
+ server.stop();
101
+ viteHandle.stop();
102
+ process.exit(0);
103
+ };
104
+ process.on("SIGINT", cleanup);
105
+ process.on("SIGTERM", cleanup);
106
+ await new Promise(() => {});
107
+ }
@@ -0,0 +1,69 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { loadOtaviaYaml } from "../config/load-otavia-yaml.js";
4
+
5
+ const DEFAULT_MOUNT = "app";
6
+
7
+ function yamlScalar(s: string): string {
8
+ if (/^[\w.-]+$/.test(s)) return s;
9
+ return JSON.stringify(s);
10
+ }
11
+
12
+ function mergeGitignore(root: string): void {
13
+ const lines = ["node_modules/", "dist/", ".otavia/", ".env.local"];
14
+ const p = resolve(root, ".gitignore");
15
+ if (!existsSync(p)) {
16
+ writeFileSync(p, `${lines.join("\n")}\n`, "utf-8");
17
+ return;
18
+ }
19
+ const existing = readFileSync(p, "utf-8");
20
+ const missing = lines.filter((line) => !existing.split("\n").some((l) => l.trim() === line.trim()));
21
+ if (missing.length === 0) return;
22
+ appendFileSync(p, `${existing.endsWith("\n") ? "" : "\n"}${missing.join("\n")}\n`, "utf-8");
23
+ }
24
+
25
+ /**
26
+ * Scaffold a new Otavia stack: otavia.yaml, cells/<defaultMount>/cell.yaml, and optional .gitignore entries.
27
+ */
28
+ export function initCommand(
29
+ rootDir: string,
30
+ options: { force?: boolean; stackName?: string; domain?: string }
31
+ ): void {
32
+ const root = resolve(rootDir);
33
+ const configPath = resolve(root, "otavia.yaml");
34
+
35
+ if (existsSync(configPath) && !options.force) {
36
+ throw new Error("otavia.yaml already exists. Use --force to overwrite.");
37
+ }
38
+
39
+ const stackName =
40
+ options.stackName?.trim() ||
41
+ basename(root).replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-|-$/g, "").toLowerCase() ||
42
+ "my-stack";
43
+ const domainHost = options.domain?.trim() || "example.com";
44
+
45
+ const yamlContent = `# Otavia stack — edit stackName, domain, and cells.
46
+ stackName: ${yamlScalar(stackName)}
47
+ domain:
48
+ host: ${yamlScalar(domainHost)}
49
+ cells:
50
+ ${DEFAULT_MOUNT}: "@otavia/${DEFAULT_MOUNT}"
51
+ `;
52
+
53
+ writeFileSync(configPath, yamlContent, "utf-8");
54
+
55
+ const cellDir = resolve(root, "cells", DEFAULT_MOUNT);
56
+ mkdirSync(cellDir, { recursive: true });
57
+ const cellYamlPath = resolve(cellDir, "cell.yaml");
58
+ if (!existsSync(cellYamlPath) || options.force) {
59
+ writeFileSync(cellYamlPath, `name: ${DEFAULT_MOUNT}\n`, "utf-8");
60
+ }
61
+
62
+ mergeGitignore(root);
63
+
64
+ loadOtaviaYaml(root);
65
+
66
+ console.log(`Initialized Otavia stack in ${root}`);
67
+ console.log(` ${configPath}`);
68
+ console.log(` ${cellYamlPath}`);
69
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { loadOtaviaYaml } from "../config/load-otavia-yaml.js";
4
+ import { resolveCellDir } from "../config/resolve-cell-dir.js";
5
+
6
+ /**
7
+ * Run biome check in each resolved cellDir. With fix: add --write; with unsafe: add --unsafe.
8
+ * Aggregate exit codes; if any cell fails, exit(1). Cells without cell.yaml are skipped.
9
+ * Cells should have biome in their dependencies, or the monorepo root may provide it.
10
+ */
11
+ export async function lintCommand(
12
+ rootDir: string,
13
+ options?: { fix?: boolean; unsafe?: boolean }
14
+ ): Promise<void> {
15
+ const root = path.resolve(rootDir);
16
+ const otavia = loadOtaviaYaml(root);
17
+ let failed = false;
18
+
19
+ const args = [
20
+ "bun",
21
+ "x",
22
+ "biome",
23
+ "check",
24
+ ".",
25
+ ...(options?.fix ? ["--write"] : []),
26
+ ...(options?.unsafe ? ["--unsafe"] : []),
27
+ ];
28
+
29
+ for (const entry of otavia.cellsList) {
30
+ const cellDir = resolveCellDir(root, entry.package);
31
+ if (!fs.existsSync(path.join(cellDir, "cell.yaml"))) {
32
+ console.warn(`Skipping ${entry.mount}: cell not found`);
33
+ continue;
34
+ }
35
+
36
+ const proc = Bun.spawn(args, {
37
+ cwd: cellDir,
38
+ stdio: ["inherit", "inherit", "inherit"],
39
+ });
40
+ const exitCode = await proc.exited;
41
+ if (exitCode !== 0) {
42
+ failed = true;
43
+ }
44
+ }
45
+
46
+ if (failed) {
47
+ process.exit(1);
48
+ }
49
+ }