vyft 0.1.0-alpha
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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +200 -0
- package/dist/docker.d.ts +48 -0
- package/dist/docker.js +855 -0
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +28 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +100 -0
- package/dist/interpolate.d.ts +2 -0
- package/dist/interpolate.js +8 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/resource.d.ts +78 -0
- package/dist/resource.js +35 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.js +0 -0
- package/dist/swarm/factories.d.ts +8 -0
- package/dist/swarm/factories.js +50 -0
- package/dist/swarm/index.d.ts +10 -0
- package/dist/swarm/index.js +5 -0
- package/dist/swarm/types.d.ts +25 -0
- package/dist/swarm/types.js +0 -0
- package/dist/symbols.d.ts +8 -0
- package/dist/symbols.js +1 -0
- package/package.json +68 -0
- package/templates/fullstack/apps/api/Dockerfile +21 -0
- package/templates/fullstack/apps/api/package.json +26 -0
- package/templates/fullstack/apps/api/src/auth.ts +21 -0
- package/templates/fullstack/apps/api/src/db.ts +16 -0
- package/templates/fullstack/apps/api/src/index.ts +17 -0
- package/templates/fullstack/apps/api/src/router.ts +11 -0
- package/templates/fullstack/apps/api/src/schema.ts +11 -0
- package/templates/fullstack/apps/api/tsconfig.json +8 -0
- package/templates/fullstack/apps/web/index.html +12 -0
- package/templates/fullstack/apps/web/package.json +21 -0
- package/templates/fullstack/apps/web/src/app.tsx +8 -0
- package/templates/fullstack/apps/web/src/main.tsx +9 -0
- package/templates/fullstack/apps/web/tsconfig.json +7 -0
- package/templates/fullstack/apps/web/vite.config.ts +14 -0
- package/templates/fullstack/compose.yaml +14 -0
- package/templates/fullstack/dockerignore +7 -0
- package/templates/fullstack/gitignore +3 -0
- package/templates/fullstack/package.json +17 -0
- package/templates/fullstack/pnpm-workspace.yaml +2 -0
- package/templates/fullstack/tsconfig.json +11 -0
- package/templates/fullstack/vyft.config.ts +37 -0
package/dist/exec.d.ts
ADDED
package/dist/exec.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function exec(command, cwd, env, log) {
|
|
3
|
+
const start = performance.now();
|
|
4
|
+
log?.debug({ command, cwd }, "process spawning");
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const proc = spawn(command, {
|
|
7
|
+
cwd,
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
shell: true,
|
|
10
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
11
|
+
});
|
|
12
|
+
proc.on("close", (code) => {
|
|
13
|
+
const durationMs = Math.round(performance.now() - start);
|
|
14
|
+
if (code !== 0) {
|
|
15
|
+
log?.debug({ command, cwd, exitCode: code, durationMs }, "process exited with error");
|
|
16
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
log?.debug({ command, cwd, exitCode: 0, durationMs }, "process exited");
|
|
20
|
+
resolve();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
proc.on("error", (err) => {
|
|
24
|
+
log?.debug({ err, command, cwd }, "process spawn failed");
|
|
25
|
+
reject(err);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { interpolate } from "./interpolate.js";
|
|
2
|
+
export type { EnvValue, HealthCheckConfig, Interpolation, Reference, Resource, ResourceLimits, ResourceType, Secret, SecretConfig, Service, ServiceConfig, Site, SiteConfig, Volume, VolumeConfig, } from "./resource.js";
|
|
3
|
+
export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
|
|
4
|
+
export type { Runtime } from "./runtime.js";
|
|
5
|
+
export type { RuntimeMeta, RuntimeRef } from "./symbols.js";
|
|
6
|
+
export { VYFT_RUNTIME } from "./symbols.js";
|
package/dist/index.js
ADDED
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(directory?: string): Promise<void>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { confirm, intro, isCancel, log, outro, text } from "@clack/prompts";
|
|
5
|
+
const TEMPLATES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../templates/fullstack");
|
|
6
|
+
async function walkDir(dir) {
|
|
7
|
+
const files = [];
|
|
8
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const full = path.join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
files.push(...(await walkDir(full)));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
files.push(full);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
async function dirExists(dir) {
|
|
21
|
+
try {
|
|
22
|
+
const s = await stat(dir);
|
|
23
|
+
return s.isDirectory();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function dirIsNonEmpty(dir) {
|
|
30
|
+
try {
|
|
31
|
+
const entries = await readdir(dir);
|
|
32
|
+
return entries.length > 0;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function init(directory) {
|
|
39
|
+
intro("Create a new project");
|
|
40
|
+
const name = await text({
|
|
41
|
+
message: "Project name",
|
|
42
|
+
placeholder: "my-app",
|
|
43
|
+
defaultValue: directory || "my-app",
|
|
44
|
+
validate(value) {
|
|
45
|
+
if (!value)
|
|
46
|
+
return "Project name is required";
|
|
47
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
48
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (isCancel(name))
|
|
52
|
+
return;
|
|
53
|
+
const domain = await text({
|
|
54
|
+
message: "Domain",
|
|
55
|
+
placeholder: "example.com",
|
|
56
|
+
validate(value) {
|
|
57
|
+
if (!value)
|
|
58
|
+
return "Domain is required";
|
|
59
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(value))
|
|
60
|
+
return "Enter a valid domain (e.g. example.com)";
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (isCancel(domain))
|
|
64
|
+
return;
|
|
65
|
+
const targetDir = path.resolve(process.cwd(), directory || name);
|
|
66
|
+
if ((await dirExists(targetDir)) && (await dirIsNonEmpty(targetDir))) {
|
|
67
|
+
const ok = await confirm({
|
|
68
|
+
message: `Directory ${path.basename(targetDir)} is not empty. Continue?`,
|
|
69
|
+
});
|
|
70
|
+
if (isCancel(ok) || !ok)
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const templateFiles = await walkDir(TEMPLATES_DIR);
|
|
74
|
+
const replacements = {
|
|
75
|
+
"{{name}}": name,
|
|
76
|
+
"{{domain}}": domain,
|
|
77
|
+
};
|
|
78
|
+
for (const templateFile of templateFiles) {
|
|
79
|
+
let relativePath = path.relative(TEMPLATES_DIR, templateFile);
|
|
80
|
+
if (relativePath === "gitignore")
|
|
81
|
+
relativePath = ".gitignore";
|
|
82
|
+
if (relativePath === "dockerignore")
|
|
83
|
+
relativePath = ".dockerignore";
|
|
84
|
+
const destPath = path.join(targetDir, relativePath);
|
|
85
|
+
await mkdir(path.dirname(destPath), { recursive: true });
|
|
86
|
+
let content = await readFile(templateFile, "utf-8");
|
|
87
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
88
|
+
content = content.replaceAll(placeholder, value);
|
|
89
|
+
}
|
|
90
|
+
await writeFile(destPath, content);
|
|
91
|
+
log.step(relativePath);
|
|
92
|
+
}
|
|
93
|
+
outro("Project created");
|
|
94
|
+
console.log();
|
|
95
|
+
console.log("Next steps:");
|
|
96
|
+
console.log(` cd ${path.relative(process.cwd(), targetDir)}`);
|
|
97
|
+
console.log(" pnpm install");
|
|
98
|
+
console.log(" pnpm dev");
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isReference } from "./resource.js";
|
|
2
|
+
export function interpolate(strings, ...values) {
|
|
3
|
+
const hasRef = values.some(isReference);
|
|
4
|
+
if (!hasRef) {
|
|
5
|
+
throw new Error("interpolate() requires at least one Reference — use a plain string instead");
|
|
6
|
+
}
|
|
7
|
+
return { type: "interpolation", strings, values };
|
|
8
|
+
}
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RuntimeRef } from "./symbols.js";
|
|
2
|
+
export type ResourceType = "volume" | "secret" | "service" | "site";
|
|
3
|
+
export type VolumeConfig = {};
|
|
4
|
+
export interface SecretConfig {
|
|
5
|
+
length?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface HealthCheckConfig {
|
|
8
|
+
command: string[];
|
|
9
|
+
interval?: string;
|
|
10
|
+
timeout?: string;
|
|
11
|
+
retries?: number;
|
|
12
|
+
startPeriod?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ResourceLimits {
|
|
15
|
+
memory?: string;
|
|
16
|
+
cpus?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Volume extends RuntimeRef {
|
|
19
|
+
type: "volume";
|
|
20
|
+
id: string;
|
|
21
|
+
config: VolumeConfig;
|
|
22
|
+
}
|
|
23
|
+
export interface Secret extends RuntimeRef {
|
|
24
|
+
type: "secret";
|
|
25
|
+
id: string;
|
|
26
|
+
config: SecretConfig;
|
|
27
|
+
}
|
|
28
|
+
export interface Service extends RuntimeRef {
|
|
29
|
+
type: "service";
|
|
30
|
+
id: string;
|
|
31
|
+
config: ServiceConfig;
|
|
32
|
+
host: string;
|
|
33
|
+
port: number;
|
|
34
|
+
url: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Site extends RuntimeRef {
|
|
37
|
+
type: "site";
|
|
38
|
+
id: string;
|
|
39
|
+
config: SiteConfig;
|
|
40
|
+
url: string;
|
|
41
|
+
}
|
|
42
|
+
export type Resource = Volume | Secret | Service | Site;
|
|
43
|
+
export type Reference = Secret;
|
|
44
|
+
export interface Interpolation {
|
|
45
|
+
type: "interpolation";
|
|
46
|
+
strings: TemplateStringsArray;
|
|
47
|
+
values: Array<Reference | string>;
|
|
48
|
+
}
|
|
49
|
+
export type EnvValue = string | Reference | Interpolation;
|
|
50
|
+
export declare function isReference(value: unknown): value is Reference;
|
|
51
|
+
export declare function isSecret(value: unknown): value is Secret;
|
|
52
|
+
export declare function isInterpolation(value: unknown): value is Interpolation;
|
|
53
|
+
export interface ServiceConfig {
|
|
54
|
+
image: string | {
|
|
55
|
+
context?: string;
|
|
56
|
+
dockerfile?: string;
|
|
57
|
+
};
|
|
58
|
+
route?: string;
|
|
59
|
+
port?: number;
|
|
60
|
+
env?: Record<string, EnvValue>;
|
|
61
|
+
command?: string[];
|
|
62
|
+
volumes?: Array<{
|
|
63
|
+
volume: Volume;
|
|
64
|
+
mount: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export interface SiteConfig {
|
|
68
|
+
route: string;
|
|
69
|
+
spa?: boolean;
|
|
70
|
+
build: {
|
|
71
|
+
cwd: string;
|
|
72
|
+
output?: string;
|
|
73
|
+
command?: string;
|
|
74
|
+
env?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare function validateId(id: string): void;
|
|
78
|
+
export declare function validateRoute(route: string): void;
|
package/dist/resource.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isReference(value) {
|
|
2
|
+
return isSecret(value);
|
|
3
|
+
}
|
|
4
|
+
export function isSecret(value) {
|
|
5
|
+
return (typeof value === "object" &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
value.type === "secret");
|
|
8
|
+
}
|
|
9
|
+
export function isInterpolation(value) {
|
|
10
|
+
return (typeof value === "object" &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
value.type === "interpolation");
|
|
13
|
+
}
|
|
14
|
+
// Validation
|
|
15
|
+
const ID_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/;
|
|
16
|
+
const ROUTE_PATTERN = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(\/.+)?$/;
|
|
17
|
+
export function validateId(id) {
|
|
18
|
+
if (!id || id.length < 1) {
|
|
19
|
+
throw new Error("Resource ID cannot be empty");
|
|
20
|
+
}
|
|
21
|
+
if (id.length > 63) {
|
|
22
|
+
throw new Error("Resource ID cannot exceed 63 characters");
|
|
23
|
+
}
|
|
24
|
+
if (!ID_PATTERN.test(id)) {
|
|
25
|
+
throw new Error(`Invalid resource ID "${id}": must start with a letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function validateRoute(route) {
|
|
29
|
+
if (!route) {
|
|
30
|
+
throw new Error("Route cannot be empty");
|
|
31
|
+
}
|
|
32
|
+
if (!ROUTE_PATTERN.test(route)) {
|
|
33
|
+
throw new Error(`Invalid route "${route}": must be a valid domain with optional path`);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/runtime.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Secret, SecretConfig, Service, Site, SiteConfig, Volume } from "../resource.js";
|
|
2
|
+
import type { StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig } from "./types.js";
|
|
3
|
+
export declare function createFactories<D extends StorageDriver = "local">(swarmConfig: SwarmConfig<D>): {
|
|
4
|
+
volume(id: string, config?: SwarmVolumeConfig<D>): Volume;
|
|
5
|
+
service(id: string, config: SwarmServiceConfig): Service;
|
|
6
|
+
secret(id: string, config?: SecretConfig): Secret;
|
|
7
|
+
site(id: string, config: SiteConfig): Site;
|
|
8
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { validateId, validateRoute } from "../resource.js";
|
|
2
|
+
import { VYFT_RUNTIME } from "../symbols.js";
|
|
3
|
+
function attachRuntime(obj, config) {
|
|
4
|
+
return Object.assign(obj, {
|
|
5
|
+
[VYFT_RUNTIME]: { name: "swarm", config },
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function createFactories(swarmConfig) {
|
|
9
|
+
return {
|
|
10
|
+
volume(id, config = {}) {
|
|
11
|
+
validateId(id);
|
|
12
|
+
return attachRuntime({ type: "volume", id, config }, swarmConfig);
|
|
13
|
+
},
|
|
14
|
+
service(id, config) {
|
|
15
|
+
validateId(id);
|
|
16
|
+
if (config.route) {
|
|
17
|
+
validateRoute(config.route);
|
|
18
|
+
}
|
|
19
|
+
const port = config.port || 3000;
|
|
20
|
+
return attachRuntime({
|
|
21
|
+
type: "service",
|
|
22
|
+
id,
|
|
23
|
+
config,
|
|
24
|
+
host: id,
|
|
25
|
+
port,
|
|
26
|
+
url: config.route
|
|
27
|
+
? `https://${config.route}`
|
|
28
|
+
: `http://${id}:${port}`,
|
|
29
|
+
}, swarmConfig);
|
|
30
|
+
},
|
|
31
|
+
secret(id, config = {}) {
|
|
32
|
+
validateId(id);
|
|
33
|
+
return attachRuntime({
|
|
34
|
+
type: "secret",
|
|
35
|
+
id,
|
|
36
|
+
config,
|
|
37
|
+
}, swarmConfig);
|
|
38
|
+
},
|
|
39
|
+
site(id, config) {
|
|
40
|
+
validateId(id);
|
|
41
|
+
validateRoute(config.route);
|
|
42
|
+
return attachRuntime({
|
|
43
|
+
type: "site",
|
|
44
|
+
id,
|
|
45
|
+
config,
|
|
46
|
+
url: `https://${config.route}`,
|
|
47
|
+
}, swarmConfig);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createFactories } from "./factories.js";
|
|
2
|
+
export type { DriverVolumeOptions, StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig, } from "./types.js";
|
|
3
|
+
import type { Secret, SecretConfig, Service, Site, SiteConfig, Volume } from "../resource.js";
|
|
4
|
+
import type { StorageDriver, SwarmConfig, SwarmServiceConfig, SwarmVolumeConfig } from "./types.js";
|
|
5
|
+
export declare function swarm<D extends StorageDriver = "local">(config?: SwarmConfig<D>): {
|
|
6
|
+
volume: (id: string, config?: SwarmVolumeConfig<D>) => Volume;
|
|
7
|
+
service: (id: string, config: SwarmServiceConfig) => Service;
|
|
8
|
+
secret: (id: string, config?: SecretConfig) => Secret;
|
|
9
|
+
site: (id: string, config: SiteConfig) => Site;
|
|
10
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HealthCheckConfig, ResourceLimits, ServiceConfig, VolumeConfig } from "../resource.js";
|
|
2
|
+
export interface DriverVolumeOptions {
|
|
3
|
+
local: {
|
|
4
|
+
size?: string;
|
|
5
|
+
};
|
|
6
|
+
overlay2: {};
|
|
7
|
+
btrfs: {
|
|
8
|
+
size?: string;
|
|
9
|
+
};
|
|
10
|
+
zfs: {
|
|
11
|
+
size?: string;
|
|
12
|
+
compression?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type StorageDriver = keyof DriverVolumeOptions;
|
|
16
|
+
export interface SwarmConfig<D extends StorageDriver = "local"> {
|
|
17
|
+
storageDriver?: D;
|
|
18
|
+
}
|
|
19
|
+
export type SwarmVolumeConfig<D extends StorageDriver = "local"> = VolumeConfig & DriverVolumeOptions[D];
|
|
20
|
+
export interface SwarmServiceConfig extends ServiceConfig {
|
|
21
|
+
replicas?: number;
|
|
22
|
+
healthCheck?: HealthCheckConfig;
|
|
23
|
+
resources?: ResourceLimits;
|
|
24
|
+
restartPolicy?: "none" | "on-failure" | "any";
|
|
25
|
+
}
|
|
File without changes
|
package/dist/symbols.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VYFT_RUNTIME = Symbol.for("vyft.runtime");
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vyft",
|
|
3
|
+
"version": "0.1.0-alpha",
|
|
4
|
+
"description": "Deploy apps to Docker Swarm with TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vyft": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./swarm": {
|
|
18
|
+
"types": "./dist/swarm/index.d.ts",
|
|
19
|
+
"import": "./dist/swarm/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"templates"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"docker",
|
|
28
|
+
"swarm",
|
|
29
|
+
"deploy",
|
|
30
|
+
"infrastructure",
|
|
31
|
+
"devops",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/vyftlabs/vyft.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/vyftlabs/vyft/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/vyftlabs/vyft#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "^2.4.4",
|
|
44
|
+
"@types/dockerode": "^4.0.1",
|
|
45
|
+
"@types/node": "^25.3.1",
|
|
46
|
+
"@types/tar-fs": "^2.0.4",
|
|
47
|
+
"tsx": "^4.21.0",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "^1.0.1",
|
|
53
|
+
"commander": "^14.0.3",
|
|
54
|
+
"dockerode": "^4.0.9",
|
|
55
|
+
"pino": "^10.3.1",
|
|
56
|
+
"tar-fs": "^3.1.1"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsc",
|
|
63
|
+
"lint": "biome check src/",
|
|
64
|
+
"format:check": "biome format src/",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
FROM node:22-slim AS base
|
|
2
|
+
RUN corepack enable
|
|
3
|
+
|
|
4
|
+
FROM base AS deps
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
|
7
|
+
COPY apps/api/package.json ./apps/api/
|
|
8
|
+
RUN pnpm install --frozen-lockfile
|
|
9
|
+
|
|
10
|
+
FROM deps AS build
|
|
11
|
+
COPY apps/api/ ./apps/api/
|
|
12
|
+
RUN pnpm --filter {{name}}-api build
|
|
13
|
+
|
|
14
|
+
FROM build AS deploy
|
|
15
|
+
RUN pnpm --filter {{name}}-api --prod deploy --legacy /prod/api
|
|
16
|
+
|
|
17
|
+
FROM base
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
COPY --from=deploy /prod/api .
|
|
20
|
+
EXPOSE 3000
|
|
21
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}-api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": ["dist"],
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch --env-file=../../.env src/index.ts",
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.14.1",
|
|
12
|
+
"@hono/trpc-server": "^0.3.4",
|
|
13
|
+
"@trpc/server": "^11.1.2",
|
|
14
|
+
"better-auth": "^1.2.8",
|
|
15
|
+
"drizzle-orm": "^0.44.2",
|
|
16
|
+
"hono": "^4.7.10",
|
|
17
|
+
"postgres": "^3.4.7",
|
|
18
|
+
"zod": "^3.25.17"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.15.0",
|
|
22
|
+
"drizzle-kit": "^0.31.1",
|
|
23
|
+
"tsx": "^4.19.4",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { betterAuth } from 'better-auth';
|
|
3
|
+
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
4
|
+
import { db } from './db.js';
|
|
5
|
+
|
|
6
|
+
function env(key: string): string {
|
|
7
|
+
const fileKey = `${key}_FILE`;
|
|
8
|
+
const filePath = process.env[fileKey];
|
|
9
|
+
if (filePath) return readFileSync(filePath, 'utf-8').trim();
|
|
10
|
+
const value = process.env[key];
|
|
11
|
+
if (value) return value;
|
|
12
|
+
throw new Error(`Missing env var: ${key} or ${fileKey}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const auth = betterAuth({
|
|
16
|
+
database: drizzleAdapter(db, { provider: 'pg' }),
|
|
17
|
+
secret: env('AUTH_SECRET'),
|
|
18
|
+
basePath: '/api/auth',
|
|
19
|
+
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
|
|
20
|
+
emailAndPassword: { enabled: true },
|
|
21
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
3
|
+
import postgres from 'postgres';
|
|
4
|
+
import * as schema from './schema.js';
|
|
5
|
+
|
|
6
|
+
function env(key: string): string {
|
|
7
|
+
const fileKey = `${key}_FILE`;
|
|
8
|
+
const filePath = process.env[fileKey];
|
|
9
|
+
if (filePath) return readFileSync(filePath, 'utf-8').trim();
|
|
10
|
+
const value = process.env[key];
|
|
11
|
+
if (value) return value;
|
|
12
|
+
throw new Error(`Missing env var: ${key} or ${fileKey}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const client = postgres(env('DATABASE_URL'));
|
|
16
|
+
export const db = drizzle(client, { schema });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { trpcServer } from '@hono/trpc-server';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { auth } from './auth.js';
|
|
5
|
+
import { appRouter } from './router.js';
|
|
6
|
+
|
|
7
|
+
const app = new Hono().basePath('/api');
|
|
8
|
+
|
|
9
|
+
app.on(['GET', 'POST'], '/auth/**', (c) => auth.handler(c.req.raw));
|
|
10
|
+
|
|
11
|
+
app.use('/trpc/*', trpcServer({ router: appRouter }));
|
|
12
|
+
|
|
13
|
+
app.get('/health', (c) => c.json({ ok: true }));
|
|
14
|
+
|
|
15
|
+
serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
|
16
|
+
console.log(`Server running on http://localhost:${info.port}`);
|
|
17
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
|
+
|
|
3
|
+
export const users = pgTable('users', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
email: text('email').notNull().unique(),
|
|
7
|
+
emailVerified: timestamp('email_verified'),
|
|
8
|
+
image: text('image'),
|
|
9
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
10
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
11
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{name}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|