@percepta/create 3.0.1 → 3.1.2
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 +16 -9
- package/dist/{chunk-GEVZERMP.js → chunk-CG7IJSB4.js} +33 -2
- package/dist/{chunk-R4FWPE4A.js → chunk-DCM7JOSC.js} +2 -2
- package/dist/index.js +281 -82
- package/dist/{init-Z4VGBHAK.js → init-XDWSYHYK.js} +1 -1
- package/dist/{status-MITGDLTT.js → status-BTHGN6QH.js} +1 -1
- package/dist/{sync-J4SFZHDX.js → sync-3Q27L7XZ.js} +1 -1
- package/dist/{upstream-AQI7P4EU.js → upstream-C5KFAHVR.js} +1 -1
- package/package.json +3 -2
- package/templates/monorepo/gitignore.template +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +3 -2
- package/templates/webapp/AGENTS.md +8 -2
- package/templates/webapp/Dockerfile +0 -1
- package/templates/webapp/README.md +1 -0
- package/templates/webapp/agent-skills/database.md +1 -0
- package/templates/webapp/agent-skills/deploy.md +45 -32
- package/templates/webapp/agent-skills/oneshot.md +3 -3
- package/templates/webapp/deploy/README.md +32 -6
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +0 -2
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +28 -31
- package/templates/webapp/drizzle.config.ts +15 -6
- package/templates/webapp/env.example.template +1 -0
- package/templates/webapp/eslint.config.mjs +8 -0
- package/templates/webapp/gitignore.template +1 -0
- package/templates/webapp/package.json.template +6 -6
- package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +495 -0
- package/templates/webapp/scripts/seed.ts +1 -1
- package/templates/webapp/scripts/setup-database.ts +16 -1
- package/templates/webapp/scripts/start.sh +3 -2
- package/templates/webapp/src/app/(app)/layout.tsx +1 -5
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +11 -1
- package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +113 -0
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +30 -0
- package/templates/webapp/src/app/global-error.tsx +3 -1
- package/templates/webapp/src/components/FaroProvider.tsx +2 -4
- package/templates/webapp/src/components/form/FormItem.tsx +2 -2
- package/templates/webapp/src/config/getEnvConfig.ts +1 -0
- package/templates/webapp/src/drizzle/db.ts +5 -1
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +3 -3
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +7 -19
- package/templates/webapp/src/drizzle/searchPath.test.ts +21 -0
- package/templates/webapp/src/drizzle/searchPath.ts +16 -0
- package/templates/webapp/src/drizzle/ssl.ts +5 -0
- package/templates/webapp/src/lib/auth/index.ts +1 -1
- package/templates/webapp/src/lib/auth-client.ts +1 -1
- package/templates/webapp/src/services/observability/initFaro.ts +1 -1
- package/templates/webapp/src/styles/globals.css +0 -7
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/* eslint-disable n/no-process-env */
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const APP_NAME = "__APP_NAME__";
|
|
13
|
+
const DATABASE_SCHEMA = "__APP_NAME_SNAKE__";
|
|
14
|
+
const ENVIRONMENT = "percepta-test";
|
|
15
|
+
const INFRA_REMOTE = "git@github.com:Percepta-Core/infra.git";
|
|
16
|
+
|
|
17
|
+
type DeployPhase = "service" | "installation";
|
|
18
|
+
|
|
19
|
+
interface Options {
|
|
20
|
+
infraPath?: string;
|
|
21
|
+
yes: boolean;
|
|
22
|
+
noClone: boolean;
|
|
23
|
+
phase: DeployPhase;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv: string[]): Options {
|
|
27
|
+
const options: Options = {
|
|
28
|
+
yes: false,
|
|
29
|
+
noClone: false,
|
|
30
|
+
phase: "service",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < argv.length; index++) {
|
|
34
|
+
const arg = argv[index];
|
|
35
|
+
if (arg === "--yes" || arg === "-y") {
|
|
36
|
+
options.yes = true;
|
|
37
|
+
} else if (arg === "--no-clone") {
|
|
38
|
+
options.noClone = true;
|
|
39
|
+
} else if (arg === "--service") {
|
|
40
|
+
options.phase = "service";
|
|
41
|
+
} else if (arg === "--installation" || arg === "--install") {
|
|
42
|
+
options.phase = "installation";
|
|
43
|
+
} else if (arg === "--phase") {
|
|
44
|
+
const value = argv[index + 1];
|
|
45
|
+
if (!value) throw new Error("--phase requires service or installation");
|
|
46
|
+
if (value === "service") {
|
|
47
|
+
options.phase = "service";
|
|
48
|
+
} else if (value === "installation" || value === "install") {
|
|
49
|
+
options.phase = "installation";
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid --phase value "${value}". Use service or installation.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
index++;
|
|
56
|
+
} else if (arg === "--infra") {
|
|
57
|
+
const value = argv[index + 1];
|
|
58
|
+
if (!value) throw new Error("--infra requires a path");
|
|
59
|
+
options.infraPath = path.resolve(value);
|
|
60
|
+
index++;
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return options;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function run(
|
|
70
|
+
command: string,
|
|
71
|
+
args: string[],
|
|
72
|
+
cwd: string,
|
|
73
|
+
stdio: "inherit" | "pipe" = "inherit",
|
|
74
|
+
): string {
|
|
75
|
+
const result = execFileSync(command, args, {
|
|
76
|
+
cwd,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
stdio,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return typeof result === "string" ? result.trim() : "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertCommand(command: string): void {
|
|
85
|
+
try {
|
|
86
|
+
run(command, ["--version"], process.cwd(), "pipe");
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Required command not found or not authenticated: ${command}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findUp(startDir: string, fileName: string): string | null {
|
|
95
|
+
let dir = startDir;
|
|
96
|
+
while (true) {
|
|
97
|
+
if (existsSync(path.join(dir, fileName))) return dir;
|
|
98
|
+
const parent = path.dirname(dir);
|
|
99
|
+
if (parent === dir) return null;
|
|
100
|
+
dir = parent;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isGitRepo(dir: string): boolean {
|
|
105
|
+
if (!existsSync(dir)) return false;
|
|
106
|
+
try {
|
|
107
|
+
run("git", ["rev-parse", "--is-inside-work-tree"], dir, "pipe");
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function confirm(message: string): Promise<boolean> {
|
|
115
|
+
const rl = createInterface({ input, output });
|
|
116
|
+
try {
|
|
117
|
+
const answer = (await rl.question(`${message} [Y/n] `))
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
121
|
+
} finally {
|
|
122
|
+
rl.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function resolveInfraRepo(
|
|
127
|
+
options: Options,
|
|
128
|
+
defaultInfraPath: string,
|
|
129
|
+
): Promise<string> {
|
|
130
|
+
const infraPath =
|
|
131
|
+
options.infraPath ?? process.env.INFRA_REPO ?? defaultInfraPath;
|
|
132
|
+
|
|
133
|
+
if (isGitRepo(infraPath)) {
|
|
134
|
+
console.log(`Using infra repo: ${infraPath}`);
|
|
135
|
+
return infraPath;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (existsSync(infraPath)) {
|
|
139
|
+
throw new Error(`${infraPath} exists but is not a git repository`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.noClone) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Infra repo not found at ${infraPath}. Re-run with --infra <path>.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!options.yes) {
|
|
149
|
+
if (!process.stdin.isTTY) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Infra repo not found at ${infraPath}. Re-run with --yes to clone or --infra <path>.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const shouldClone = await confirm(
|
|
156
|
+
`Infra repo not found. Clone Percepta-Core/infra to ${infraPath}?`,
|
|
157
|
+
);
|
|
158
|
+
if (!shouldClone) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Skipped cloning infra repo. Re-run with --infra <path>.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await mkdir(path.dirname(infraPath), { recursive: true });
|
|
166
|
+
console.log(`Cloning ${INFRA_REMOTE} to ${infraPath}...`);
|
|
167
|
+
run("git", ["clone", INFRA_REMOTE, infraPath], process.cwd());
|
|
168
|
+
console.log(`Cloned infra repo: ${infraPath}`);
|
|
169
|
+
return infraPath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function assertCleanGitWorktree(repoPath: string): void {
|
|
173
|
+
const status = run("git", ["status", "--porcelain"], repoPath, "pipe");
|
|
174
|
+
if (status.length === 0) return;
|
|
175
|
+
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Infra repo has local changes. Commit/stash them first or use --infra with a clean checkout.\n${status}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getDeployBranchName(phase: DeployPhase): string {
|
|
182
|
+
return phase === "service"
|
|
183
|
+
? `deploy/${APP_NAME}-${ENVIRONMENT}-service`
|
|
184
|
+
: `deploy/${APP_NAME}-${ENVIRONMENT}-installation`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function switchDeployBranch(repoPath: string, phase: DeployPhase): string {
|
|
188
|
+
const branchName = getDeployBranchName(phase);
|
|
189
|
+
const currentBranch = run(
|
|
190
|
+
"git",
|
|
191
|
+
["branch", "--show-current"],
|
|
192
|
+
repoPath,
|
|
193
|
+
"pipe",
|
|
194
|
+
);
|
|
195
|
+
if (currentBranch === branchName) return branchName;
|
|
196
|
+
|
|
197
|
+
run("git", ["fetch", "origin", "main"], repoPath);
|
|
198
|
+
|
|
199
|
+
let branchExists = false;
|
|
200
|
+
try {
|
|
201
|
+
run("git", ["rev-parse", "--verify", branchName], repoPath, "pipe");
|
|
202
|
+
branchExists = true;
|
|
203
|
+
} catch {
|
|
204
|
+
branchExists = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (branchExists) {
|
|
208
|
+
run("git", ["switch", branchName], repoPath);
|
|
209
|
+
} else {
|
|
210
|
+
run("git", ["switch", "-c", branchName, "origin/main"], repoPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return branchName;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function writeIfChanged(
|
|
217
|
+
filePath: string,
|
|
218
|
+
content: string,
|
|
219
|
+
): Promise<boolean> {
|
|
220
|
+
if (existsSync(filePath)) {
|
|
221
|
+
const existing = await readFile(filePath, "utf8");
|
|
222
|
+
if (existing === content) return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
226
|
+
await writeFile(filePath, content);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function copyDeployYaml(
|
|
231
|
+
packageDir: string,
|
|
232
|
+
infraRepo: string,
|
|
233
|
+
phase: DeployPhase,
|
|
234
|
+
): Promise<string[]> {
|
|
235
|
+
const serviceSource = path.join(
|
|
236
|
+
packageDir,
|
|
237
|
+
"deploy",
|
|
238
|
+
"ryvn",
|
|
239
|
+
`${APP_NAME}.service.yaml`,
|
|
240
|
+
);
|
|
241
|
+
const installationSource = path.join(
|
|
242
|
+
packageDir,
|
|
243
|
+
"deploy",
|
|
244
|
+
"ryvn",
|
|
245
|
+
"environments",
|
|
246
|
+
ENVIRONMENT,
|
|
247
|
+
"installations",
|
|
248
|
+
`${APP_NAME}.env.${ENVIRONMENT}.serviceinstallation.yaml`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const serviceTarget = path.join(
|
|
252
|
+
infraRepo,
|
|
253
|
+
"ryvn",
|
|
254
|
+
`${APP_NAME}.service.yaml`,
|
|
255
|
+
);
|
|
256
|
+
const installationTarget = path.join(
|
|
257
|
+
infraRepo,
|
|
258
|
+
"ryvn",
|
|
259
|
+
"environments",
|
|
260
|
+
ENVIRONMENT,
|
|
261
|
+
"installations",
|
|
262
|
+
`${APP_NAME}.env.${ENVIRONMENT}.serviceinstallation.yaml`,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const changed: string[] = [];
|
|
266
|
+
if (
|
|
267
|
+
phase === "service" &&
|
|
268
|
+
(await writeIfChanged(serviceTarget, await readFile(serviceSource, "utf8")))
|
|
269
|
+
) {
|
|
270
|
+
changed.push(path.relative(infraRepo, serviceTarget));
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
phase === "installation" &&
|
|
274
|
+
(await writeIfChanged(
|
|
275
|
+
installationTarget,
|
|
276
|
+
await readFile(installationSource, "utf8"),
|
|
277
|
+
))
|
|
278
|
+
) {
|
|
279
|
+
changed.push(path.relative(infraRepo, installationTarget));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return changed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function ensureDatabaseSchema(infraRepo: string): Promise<string | null> {
|
|
286
|
+
const databasesPath = path.join(
|
|
287
|
+
infraRepo,
|
|
288
|
+
"terraform",
|
|
289
|
+
"percepta-internal",
|
|
290
|
+
"databases.tf",
|
|
291
|
+
);
|
|
292
|
+
const content = await readFile(databasesPath, "utf8");
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
content.includes(`resource "postgresql_schema" "${DATABASE_SCHEMA}"`) ||
|
|
296
|
+
content.includes(`name = "${DATABASE_SCHEMA}"`)
|
|
297
|
+
) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const block = `resource "postgresql_schema" "${DATABASE_SCHEMA}" {
|
|
302
|
+
provider = postgresql.percepta_internal
|
|
303
|
+
database = postgresql_database.demos.name
|
|
304
|
+
name = "${DATABASE_SCHEMA}"
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
await appendFile(
|
|
309
|
+
databasesPath,
|
|
310
|
+
`${content.endsWith("\n") ? "\n" : "\n\n"}${block}`,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return path.relative(infraRepo, databasesPath);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function hasStagedChanges(repoPath: string): boolean {
|
|
317
|
+
try {
|
|
318
|
+
run("git", ["diff", "--cached", "--quiet"], repoPath, "pipe");
|
|
319
|
+
return false;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const status =
|
|
322
|
+
typeof error === "object" && error !== null && "status" in error
|
|
323
|
+
? (error as { status?: number }).status
|
|
324
|
+
: undefined;
|
|
325
|
+
if (status === 1) return true;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getPrUrl(repoPath: string, branchName: string): string | null {
|
|
331
|
+
try {
|
|
332
|
+
return run(
|
|
333
|
+
"gh",
|
|
334
|
+
["pr", "view", branchName, "--json", "url", "--jq", ".url"],
|
|
335
|
+
repoPath,
|
|
336
|
+
"pipe",
|
|
337
|
+
);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getPrTitle(phase: DeployPhase): string {
|
|
344
|
+
return phase === "service"
|
|
345
|
+
? `Add ${APP_NAME} ${ENVIRONMENT} service`
|
|
346
|
+
: `Install ${APP_NAME} in ${ENVIRONMENT}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getPrBody(phase: DeployPhase): string {
|
|
350
|
+
if (phase === "service") {
|
|
351
|
+
return [
|
|
352
|
+
`Adds the Ryvn service for ${APP_NAME}.`,
|
|
353
|
+
"",
|
|
354
|
+
`Also creates the ${DATABASE_SCHEMA} schema in the shared demos database.`,
|
|
355
|
+
"",
|
|
356
|
+
"After merge:",
|
|
357
|
+
"1. Let GitOps import the service.",
|
|
358
|
+
"2. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
359
|
+
"3. Push the app to main or run the release workflow so Ryvn has a first release.",
|
|
360
|
+
`4. Open the installation PR with pnpm deploy:percepta-test -- --phase installation --yes.`,
|
|
361
|
+
].join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [
|
|
365
|
+
`Adds the ${ENVIRONMENT} ServiceInstallation for ${APP_NAME}.`,
|
|
366
|
+
"",
|
|
367
|
+
"Pre-merge checklist:",
|
|
368
|
+
"1. The Ryvn Service exists from the service/schema PR.",
|
|
369
|
+
"2. At least one Ryvn release exists for this service.",
|
|
370
|
+
"",
|
|
371
|
+
"After merge, import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI for the installation secrets.",
|
|
372
|
+
].join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function printNextSteps(phase: DeployPhase): void {
|
|
376
|
+
if (phase === "service") {
|
|
377
|
+
console.log("");
|
|
378
|
+
console.log("Next:");
|
|
379
|
+
console.log("1. Merge the service/schema PR.");
|
|
380
|
+
console.log("2. Wait for GitOps to import the service.");
|
|
381
|
+
console.log(
|
|
382
|
+
"3. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
383
|
+
);
|
|
384
|
+
console.log("4. Push the app to main or run the release workflow.");
|
|
385
|
+
console.log(
|
|
386
|
+
"5. Run: pnpm deploy:percepta-test -- --phase installation --yes",
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log("");
|
|
392
|
+
console.log("Next:");
|
|
393
|
+
console.log("1. Merge the installation PR.");
|
|
394
|
+
console.log("2. Let GitOps import the ServiceInstallation.");
|
|
395
|
+
console.log(
|
|
396
|
+
"3. Import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI.",
|
|
397
|
+
);
|
|
398
|
+
console.log(
|
|
399
|
+
"4. Verify health with: ryvn get installation __APP_NAME__ -e percepta-test",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function confirmInstallationPrerequisites(
|
|
404
|
+
options: Options,
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
if (options.phase !== "installation" || options.yes) return;
|
|
407
|
+
|
|
408
|
+
const message =
|
|
409
|
+
"Before opening the installation PR, confirm the service exists, " +
|
|
410
|
+
"and a first Ryvn release exists. Continue?";
|
|
411
|
+
|
|
412
|
+
if (!process.stdin.isTTY) {
|
|
413
|
+
throw new Error(`${message} Re-run with --yes to confirm.`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!(await confirm(message))) {
|
|
417
|
+
throw new Error("Skipped installation PR.");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function main(): Promise<void> {
|
|
422
|
+
const options = parseArgs(process.argv.slice(2));
|
|
423
|
+
assertCommand("git");
|
|
424
|
+
assertCommand("gh");
|
|
425
|
+
await confirmInstallationPrerequisites(options);
|
|
426
|
+
|
|
427
|
+
const packageDir = path.resolve(
|
|
428
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
429
|
+
"..",
|
|
430
|
+
);
|
|
431
|
+
const monorepoRoot = findUp(packageDir, "pnpm-workspace.yaml") ?? packageDir;
|
|
432
|
+
const defaultInfraPath = path.resolve(monorepoRoot, "..", "infra");
|
|
433
|
+
const infraRepo = await resolveInfraRepo(options, defaultInfraPath);
|
|
434
|
+
|
|
435
|
+
assertCleanGitWorktree(infraRepo);
|
|
436
|
+
const branchName = switchDeployBranch(infraRepo, options.phase);
|
|
437
|
+
|
|
438
|
+
const changedPaths = await copyDeployYaml(
|
|
439
|
+
packageDir,
|
|
440
|
+
infraRepo,
|
|
441
|
+
options.phase,
|
|
442
|
+
);
|
|
443
|
+
if (options.phase === "service") {
|
|
444
|
+
const schemaPath = await ensureDatabaseSchema(infraRepo);
|
|
445
|
+
if (schemaPath) changedPaths.push(schemaPath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (changedPaths.length === 0) {
|
|
449
|
+
console.log(
|
|
450
|
+
options.phase === "service"
|
|
451
|
+
? "Infra repo already has the generated Ryvn service and database schema."
|
|
452
|
+
: "Infra repo already has the generated Ryvn installation.",
|
|
453
|
+
);
|
|
454
|
+
printNextSteps(options.phase);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
run("git", ["add", ...changedPaths], infraRepo);
|
|
459
|
+
if (!hasStagedChanges(infraRepo)) {
|
|
460
|
+
console.log("No infra changes to commit.");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
run("git", ["commit", "-m", getPrTitle(options.phase)], infraRepo);
|
|
465
|
+
run("git", ["push", "-u", "origin", branchName], infraRepo);
|
|
466
|
+
|
|
467
|
+
const existingPr = getPrUrl(infraRepo, branchName);
|
|
468
|
+
if (existingPr) {
|
|
469
|
+
console.log(`Infra PR: ${existingPr}`);
|
|
470
|
+
printNextSteps(options.phase);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const prUrl = run(
|
|
475
|
+
"gh",
|
|
476
|
+
[
|
|
477
|
+
"pr",
|
|
478
|
+
"create",
|
|
479
|
+
"--title",
|
|
480
|
+
getPrTitle(options.phase),
|
|
481
|
+
"--body",
|
|
482
|
+
getPrBody(options.phase),
|
|
483
|
+
],
|
|
484
|
+
infraRepo,
|
|
485
|
+
"pipe",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
console.log(`Infra PR: ${prUrl}`);
|
|
489
|
+
printNextSteps(options.phase);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
void main().catch((error) => {
|
|
493
|
+
console.error(error instanceof Error ? error.message : error);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
});
|
|
@@ -18,7 +18,7 @@ const DEFAULT_USER = {
|
|
|
18
18
|
|
|
19
19
|
async function main(): Promise<void> {
|
|
20
20
|
loadEnvConfig(process.cwd());
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
22
22
|
(globalThis as any).AsyncLocalStorage = AsyncLocalStorage;
|
|
23
23
|
|
|
24
24
|
const { auth } = await import("../src/lib/auth");
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { loadEnvConfig } from "@next/env";
|
|
2
2
|
import { Pool } from "pg";
|
|
3
3
|
import { getEnvConfig } from "../src/config/getEnvConfig";
|
|
4
|
+
import { getPgSslConfig } from "../src/drizzle/ssl";
|
|
5
|
+
|
|
6
|
+
const SHARED_DATABASES = new Set(["demos", "internal_apps"]);
|
|
4
7
|
|
|
5
8
|
async function main(): Promise<void> {
|
|
6
9
|
loadEnvConfig(process.cwd());
|
|
@@ -11,6 +14,7 @@ async function main(): Promise<void> {
|
|
|
11
14
|
DATABASE_USERNAME: user,
|
|
12
15
|
DATABASE_PASSWORD: password,
|
|
13
16
|
DATABASE_NAME: database,
|
|
17
|
+
DATABASE_SCHEMA: databaseSchema,
|
|
14
18
|
DATABASE_USE_SSL: useSSL,
|
|
15
19
|
} = getEnvConfig();
|
|
16
20
|
|
|
@@ -18,6 +22,17 @@ async function main(): Promise<void> {
|
|
|
18
22
|
console.log(`📍 Host: ${host}:${port}`);
|
|
19
23
|
console.log(`👤 User: ${user}`);
|
|
20
24
|
|
|
25
|
+
if (SHARED_DATABASES.has(database)) {
|
|
26
|
+
console.log(
|
|
27
|
+
`✅ ${database} is a shared database; skipping CREATE DATABASE. ` +
|
|
28
|
+
"The database is managed by infra.",
|
|
29
|
+
);
|
|
30
|
+
if (databaseSchema) {
|
|
31
|
+
console.log(`📦 Using schema: ${databaseSchema}`);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
// First, connect to the default 'postgres' database to create our target database
|
|
22
37
|
const adminClient = new Pool({
|
|
23
38
|
host,
|
|
@@ -25,7 +40,7 @@ async function main(): Promise<void> {
|
|
|
25
40
|
user,
|
|
26
41
|
password,
|
|
27
42
|
database: "postgres", // Connect to default postgres database
|
|
28
|
-
ssl: useSSL,
|
|
43
|
+
ssl: getPgSslConfig(useSSL),
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
try {
|
|
@@ -17,8 +17,9 @@ echo "Running database migrations..."
|
|
|
17
17
|
if pnpm db:setup-and-migrate; then
|
|
18
18
|
echo "✅ Database migrations completed successfully"
|
|
19
19
|
else
|
|
20
|
-
echo "❌ Database migration failed. App will start
|
|
20
|
+
echo "❌ Database migration failed. App will not start."
|
|
21
21
|
echo "Check your database configuration and connectivity."
|
|
22
|
+
exit 1
|
|
22
23
|
fi
|
|
23
24
|
|
|
24
25
|
# Setup readonly database user for EDW (only if READONLY_SECRET_NAME is set)
|
|
@@ -49,4 +50,4 @@ fi
|
|
|
49
50
|
|
|
50
51
|
# Start the Next.js application
|
|
51
52
|
echo "Starting Next.js server on port ${PORT:-3000}..."
|
|
52
|
-
exec pnpm start
|
|
53
|
+
exec pnpm start
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Header from "../../components/Header";
|
|
3
3
|
|
|
4
|
-
export default function AppLayout({
|
|
5
|
-
children,
|
|
6
|
-
}: {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
}) {
|
|
4
|
+
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|
9
5
|
return (
|
|
10
6
|
<>
|
|
11
7
|
<Header />
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { Button, Input } from "@percepta/design";
|
|
5
|
+
import Link from "next/link";
|
|
5
6
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
7
|
import React, { useCallback } from "react";
|
|
7
8
|
import { Controller, useForm } from "react-hook-form";
|
|
8
9
|
import { toast } from "sonner";
|
|
9
10
|
import z from "zod";
|
|
10
11
|
import { FormItem } from "../../../../components/form/FormItem";
|
|
11
|
-
import { authClient } from "../../../../lib/auth-client";
|
|
12
12
|
import { IS_DEV } from "../../../../config/isDev";
|
|
13
|
+
import { authClient } from "../../../../lib/auth-client";
|
|
13
14
|
|
|
14
15
|
const CREDENTIALS_SCHEMA = z.object({
|
|
15
16
|
email: z.string().min(1, "Email is required"),
|
|
@@ -66,6 +67,15 @@ export function CredentialsSignInForm() {
|
|
|
66
67
|
<div className="space-y-8">
|
|
67
68
|
<div className="text-center">
|
|
68
69
|
<h1 className="text-2xl font-bold text-foreground">Sign In</h1>
|
|
70
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
71
|
+
Need an account?{" "}
|
|
72
|
+
<Link
|
|
73
|
+
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
74
|
+
href={`/auth/signup?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
75
|
+
>
|
|
76
|
+
Create one
|
|
77
|
+
</Link>
|
|
78
|
+
</p>
|
|
69
79
|
</div>
|
|
70
80
|
<form className="space-y-6" onSubmit={handleSubmit(submit)}>
|
|
71
81
|
<div className="space-y-4">
|