@percepta/create 3.1.0 → 3.1.3
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 +15 -10
- package/dist/{chunk-7NPWSTCY.js → chunk-CO3YWUD6.js} +31 -2
- package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
- package/dist/index.js +93 -73
- package/dist/{init-NP6GRXLL.js → init-EQZ2TCSJ.js} +2 -2
- package/dist/{status-BTHGN6QH.js → status-QW5TQDYY.js} +1 -1
- package/dist/{sync-3Q27L7XZ.js → sync-RLBZDOFB.js} +1 -1
- package/dist/{upstream-C5KFAHVR.js → upstream-TQFVPMEG.js} +1 -1
- package/package.json +3 -2
- package/templates/monorepo/.dockerignore +18 -0
- package/templates/monorepo/gitignore.template +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
- package/templates/webapp/AGENTS.md +17 -5
- package/templates/webapp/Dockerfile +16 -7
- package/templates/webapp/README.md +64 -2
- package/templates/webapp/agent-skills/deploy.md +50 -51
- package/templates/webapp/agent-skills/inngest.md +4 -4
- package/templates/webapp/agent-skills/langfuse.md +15 -14
- package/templates/webapp/agent-skills/llm.md +59 -0
- package/templates/webapp/agent-skills/oneshot.md +14 -1
- package/templates/webapp/agent-skills/ryvn.md +1 -1
- package/templates/webapp/deploy/README.md +41 -16
- package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +60 -11
- package/templates/webapp/env.example.template +20 -2
- package/templates/webapp/eslint.config.mjs +7 -0
- package/templates/webapp/gitignore.template +1 -0
- package/templates/webapp/next.config.ts +9 -0
- package/templates/webapp/package.json.template +6 -2
- package/templates/webapp/scripts/deploy-percepta-test.ts +837 -0
- package/templates/webapp/scripts/migrate.ts +3 -0
- package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +152 -32
- package/templates/webapp/scripts/seed.ts +1 -1
- package/templates/webapp/scripts/setup-database.ts +2 -1
- package/templates/webapp/scripts/start.sh +3 -2
- package/templates/webapp/scripts/with-local-env.ts +75 -0
- 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 +1 -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 +14 -0
- package/templates/webapp/src/drizzle/db.ts +2 -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/ssl.ts +5 -0
- package/templates/webapp/src/instrumentation.ts +102 -10
- 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/llm/LLMService.ts +88 -0
- package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
- package/templates/webapp/src/services/observability/initFaro.ts +1 -1
- package/templates/webapp/terraform/schema/main.tf +4 -0
- package/templates/webapp/terraform/schema/outputs.tf +9 -0
- package/templates/webapp/terraform/schema/variables.tf +19 -0
- package/templates/webapp/terraform/schema/versions.tf +38 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
|
@@ -6,10 +6,13 @@ async function main(): Promise<void> {
|
|
|
6
6
|
loadEnvConfig(process.cwd());
|
|
7
7
|
|
|
8
8
|
// Dynamically load because we need to load the environment variables before importing modules that depend on them.
|
|
9
|
+
const { getEnvConfig } = await import("../src/config/getEnvConfig");
|
|
9
10
|
const { client } = await import("../src/drizzle/db");
|
|
11
|
+
const { DATABASE_SCHEMA: databaseSchema } = getEnvConfig();
|
|
10
12
|
|
|
11
13
|
await migrate(drizzle(client), {
|
|
12
14
|
migrationsFolder: "./src/drizzle/migrations",
|
|
15
|
+
...(databaseSchema ? { migrationsSchema: databaseSchema } : {}),
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
await client.end();
|
|
@@ -1,37 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
/* eslint-disable
|
|
2
|
+
/* eslint-disable n/no-process-env */
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { createInterface } from "node:readline/promises";
|
|
10
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
11
|
|
|
12
12
|
const APP_NAME = "__APP_NAME__";
|
|
13
13
|
const DATABASE_SCHEMA = "__APP_NAME_SNAKE__";
|
|
14
14
|
const ENVIRONMENT = "percepta-test";
|
|
15
15
|
const INFRA_REMOTE = "git@github.com:Percepta-Core/infra.git";
|
|
16
16
|
|
|
17
|
+
type DeployPhase = "service" | "installation";
|
|
18
|
+
|
|
17
19
|
interface Options {
|
|
18
20
|
infraPath?: string;
|
|
19
21
|
yes: boolean;
|
|
20
22
|
noClone: boolean;
|
|
23
|
+
phase: DeployPhase;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function parseArgs(argv: string[]): Options {
|
|
24
27
|
const options: Options = {
|
|
25
28
|
yes: false,
|
|
26
29
|
noClone: false,
|
|
30
|
+
phase: "service",
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
for (let index = 0; index < argv.length; index++) {
|
|
30
34
|
const arg = argv[index];
|
|
31
|
-
if (arg === "--
|
|
35
|
+
if (arg === "--") {
|
|
36
|
+
continue;
|
|
37
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
32
38
|
options.yes = true;
|
|
33
39
|
} else if (arg === "--no-clone") {
|
|
34
40
|
options.noClone = true;
|
|
41
|
+
} else if (arg === "--service") {
|
|
42
|
+
options.phase = "service";
|
|
43
|
+
} else if (arg === "--installation" || arg === "--install") {
|
|
44
|
+
options.phase = "installation";
|
|
45
|
+
} else if (arg === "--phase") {
|
|
46
|
+
const value = argv[index + 1];
|
|
47
|
+
if (!value) throw new Error("--phase requires service or installation");
|
|
48
|
+
if (value === "service") {
|
|
49
|
+
options.phase = "service";
|
|
50
|
+
} else if (value === "installation" || value === "install") {
|
|
51
|
+
options.phase = "installation";
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Invalid --phase value "${value}". Use service or installation.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
index++;
|
|
35
58
|
} else if (arg === "--infra") {
|
|
36
59
|
const value = argv[index + 1];
|
|
37
60
|
if (!value) throw new Error("--infra requires a path");
|
|
@@ -135,7 +158,9 @@ async function resolveInfraRepo(
|
|
|
135
158
|
`Infra repo not found. Clone Percepta-Core/infra to ${infraPath}?`,
|
|
136
159
|
);
|
|
137
160
|
if (!shouldClone) {
|
|
138
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Skipped cloning infra repo. Re-run with --infra <path>.",
|
|
163
|
+
);
|
|
139
164
|
}
|
|
140
165
|
}
|
|
141
166
|
|
|
@@ -155,8 +180,14 @@ function assertCleanGitWorktree(repoPath: string): void {
|
|
|
155
180
|
);
|
|
156
181
|
}
|
|
157
182
|
|
|
158
|
-
function
|
|
159
|
-
|
|
183
|
+
function getDeployBranchName(phase: DeployPhase): string {
|
|
184
|
+
return phase === "service"
|
|
185
|
+
? `deploy/${APP_NAME}-${ENVIRONMENT}-service`
|
|
186
|
+
: `deploy/${APP_NAME}-${ENVIRONMENT}-installation`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function switchDeployBranch(repoPath: string, phase: DeployPhase): string {
|
|
190
|
+
const branchName = getDeployBranchName(phase);
|
|
160
191
|
const currentBranch = run(
|
|
161
192
|
"git",
|
|
162
193
|
["branch", "--show-current"],
|
|
@@ -165,6 +196,8 @@ function switchDeployBranch(repoPath: string): string {
|
|
|
165
196
|
);
|
|
166
197
|
if (currentBranch === branchName) return branchName;
|
|
167
198
|
|
|
199
|
+
run("git", ["fetch", "origin", "main"], repoPath);
|
|
200
|
+
|
|
168
201
|
let branchExists = false;
|
|
169
202
|
try {
|
|
170
203
|
run("git", ["rev-parse", "--verify", branchName], repoPath, "pipe");
|
|
@@ -176,7 +209,7 @@ function switchDeployBranch(repoPath: string): string {
|
|
|
176
209
|
if (branchExists) {
|
|
177
210
|
run("git", ["switch", branchName], repoPath);
|
|
178
211
|
} else {
|
|
179
|
-
run("git", ["switch", "-c", branchName], repoPath);
|
|
212
|
+
run("git", ["switch", "-c", branchName, "origin/main"], repoPath);
|
|
180
213
|
}
|
|
181
214
|
|
|
182
215
|
return branchName;
|
|
@@ -199,6 +232,7 @@ async function writeIfChanged(
|
|
|
199
232
|
async function copyDeployYaml(
|
|
200
233
|
packageDir: string,
|
|
201
234
|
infraRepo: string,
|
|
235
|
+
phase: DeployPhase,
|
|
202
236
|
): Promise<string[]> {
|
|
203
237
|
const serviceSource = path.join(
|
|
204
238
|
packageDir,
|
|
@@ -216,7 +250,11 @@ async function copyDeployYaml(
|
|
|
216
250
|
`${APP_NAME}.env.${ENVIRONMENT}.serviceinstallation.yaml`,
|
|
217
251
|
);
|
|
218
252
|
|
|
219
|
-
const serviceTarget = path.join(
|
|
253
|
+
const serviceTarget = path.join(
|
|
254
|
+
infraRepo,
|
|
255
|
+
"ryvn",
|
|
256
|
+
`${APP_NAME}.service.yaml`,
|
|
257
|
+
);
|
|
220
258
|
const installationTarget = path.join(
|
|
221
259
|
infraRepo,
|
|
222
260
|
"ryvn",
|
|
@@ -227,14 +265,18 @@ async function copyDeployYaml(
|
|
|
227
265
|
);
|
|
228
266
|
|
|
229
267
|
const changed: string[] = [];
|
|
230
|
-
if (
|
|
268
|
+
if (
|
|
269
|
+
phase === "service" &&
|
|
270
|
+
(await writeIfChanged(serviceTarget, await readFile(serviceSource, "utf8")))
|
|
271
|
+
) {
|
|
231
272
|
changed.push(path.relative(infraRepo, serviceTarget));
|
|
232
273
|
}
|
|
233
274
|
if (
|
|
234
|
-
|
|
275
|
+
phase === "installation" &&
|
|
276
|
+
(await writeIfChanged(
|
|
235
277
|
installationTarget,
|
|
236
278
|
await readFile(installationSource, "utf8"),
|
|
237
|
-
)
|
|
279
|
+
))
|
|
238
280
|
) {
|
|
239
281
|
changed.push(path.relative(infraRepo, installationTarget));
|
|
240
282
|
}
|
|
@@ -300,10 +342,89 @@ function getPrUrl(repoPath: string, branchName: string): string | null {
|
|
|
300
342
|
}
|
|
301
343
|
}
|
|
302
344
|
|
|
345
|
+
function getPrTitle(phase: DeployPhase): string {
|
|
346
|
+
return phase === "service"
|
|
347
|
+
? `Add ${APP_NAME} ${ENVIRONMENT} service`
|
|
348
|
+
: `Install ${APP_NAME} in ${ENVIRONMENT}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getPrBody(phase: DeployPhase): string {
|
|
352
|
+
if (phase === "service") {
|
|
353
|
+
return [
|
|
354
|
+
`Adds the Ryvn service for ${APP_NAME}.`,
|
|
355
|
+
"",
|
|
356
|
+
`Also creates the ${DATABASE_SCHEMA} schema in the shared demos database.`,
|
|
357
|
+
"",
|
|
358
|
+
"After merge:",
|
|
359
|
+
"1. Let GitOps import the service.",
|
|
360
|
+
"2. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
361
|
+
"3. Push the app to main or run the release workflow so Ryvn has a first release.",
|
|
362
|
+
`4. Open the installation PR with pnpm deploy:percepta-test:pr -- --phase installation --yes.`,
|
|
363
|
+
].join("\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return [
|
|
367
|
+
`Adds the ${ENVIRONMENT} ServiceInstallation for ${APP_NAME}.`,
|
|
368
|
+
"",
|
|
369
|
+
"Pre-merge checklist:",
|
|
370
|
+
"1. The Ryvn Service exists from the service/schema PR.",
|
|
371
|
+
"2. At least one Ryvn release exists for this service.",
|
|
372
|
+
"",
|
|
373
|
+
"After merge, import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI for the installation secrets.",
|
|
374
|
+
].join("\n");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function printNextSteps(phase: DeployPhase): void {
|
|
378
|
+
if (phase === "service") {
|
|
379
|
+
console.log("");
|
|
380
|
+
console.log("Next:");
|
|
381
|
+
console.log("1. Merge the service/schema PR.");
|
|
382
|
+
console.log("2. Wait for GitOps to import the service.");
|
|
383
|
+
console.log(
|
|
384
|
+
"3. Approve/apply the percepta-internal Terraform task if one is created.",
|
|
385
|
+
);
|
|
386
|
+
console.log("4. Push the app to main or run the release workflow.");
|
|
387
|
+
console.log(
|
|
388
|
+
"5. Run: pnpm deploy:percepta-test:pr -- --phase installation --yes",
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log("");
|
|
394
|
+
console.log("Next:");
|
|
395
|
+
console.log("1. Merge the installation PR.");
|
|
396
|
+
console.log("2. Let GitOps import the ServiceInstallation.");
|
|
397
|
+
console.log(
|
|
398
|
+
"3. Import deploy/ryvn/percepta-test.secrets.env in the Ryvn UI.",
|
|
399
|
+
);
|
|
400
|
+
console.log(
|
|
401
|
+
"4. Verify health with: ryvn get installation __APP_NAME__ -e percepta-test",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function confirmInstallationPrerequisites(
|
|
406
|
+
options: Options,
|
|
407
|
+
): Promise<void> {
|
|
408
|
+
if (options.phase !== "installation" || options.yes) return;
|
|
409
|
+
|
|
410
|
+
const message =
|
|
411
|
+
"Before opening the installation PR, confirm the service exists, " +
|
|
412
|
+
"and a first Ryvn release exists. Continue?";
|
|
413
|
+
|
|
414
|
+
if (!process.stdin.isTTY) {
|
|
415
|
+
throw new Error(`${message} Re-run with --yes to confirm.`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!(await confirm(message))) {
|
|
419
|
+
throw new Error("Skipped installation PR.");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
303
423
|
async function main(): Promise<void> {
|
|
304
424
|
const options = parseArgs(process.argv.slice(2));
|
|
305
425
|
assertCommand("git");
|
|
306
426
|
assertCommand("gh");
|
|
427
|
+
await confirmInstallationPrerequisites(options);
|
|
307
428
|
|
|
308
429
|
const packageDir = path.resolve(
|
|
309
430
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -314,16 +435,25 @@ async function main(): Promise<void> {
|
|
|
314
435
|
const infraRepo = await resolveInfraRepo(options, defaultInfraPath);
|
|
315
436
|
|
|
316
437
|
assertCleanGitWorktree(infraRepo);
|
|
317
|
-
const branchName = switchDeployBranch(infraRepo);
|
|
438
|
+
const branchName = switchDeployBranch(infraRepo, options.phase);
|
|
318
439
|
|
|
319
|
-
const changedPaths = await copyDeployYaml(
|
|
320
|
-
|
|
321
|
-
|
|
440
|
+
const changedPaths = await copyDeployYaml(
|
|
441
|
+
packageDir,
|
|
442
|
+
infraRepo,
|
|
443
|
+
options.phase,
|
|
444
|
+
);
|
|
445
|
+
if (options.phase === "service") {
|
|
446
|
+
const schemaPath = await ensureDatabaseSchema(infraRepo);
|
|
447
|
+
if (schemaPath) changedPaths.push(schemaPath);
|
|
448
|
+
}
|
|
322
449
|
|
|
323
450
|
if (changedPaths.length === 0) {
|
|
324
451
|
console.log(
|
|
325
|
-
|
|
452
|
+
options.phase === "service"
|
|
453
|
+
? "Infra repo already has the generated Ryvn service and database schema."
|
|
454
|
+
: "Infra repo already has the generated Ryvn installation.",
|
|
326
455
|
);
|
|
456
|
+
printNextSteps(options.phase);
|
|
327
457
|
return;
|
|
328
458
|
}
|
|
329
459
|
|
|
@@ -333,42 +463,32 @@ async function main(): Promise<void> {
|
|
|
333
463
|
return;
|
|
334
464
|
}
|
|
335
465
|
|
|
336
|
-
run(
|
|
337
|
-
"git",
|
|
338
|
-
["commit", "-m", `Add ${APP_NAME} ${ENVIRONMENT} deployment`],
|
|
339
|
-
infraRepo,
|
|
340
|
-
);
|
|
466
|
+
run("git", ["commit", "-m", getPrTitle(options.phase)], infraRepo);
|
|
341
467
|
run("git", ["push", "-u", "origin", branchName], infraRepo);
|
|
342
468
|
|
|
343
469
|
const existingPr = getPrUrl(infraRepo, branchName);
|
|
344
470
|
if (existingPr) {
|
|
345
471
|
console.log(`Infra PR: ${existingPr}`);
|
|
472
|
+
printNextSteps(options.phase);
|
|
346
473
|
return;
|
|
347
474
|
}
|
|
348
475
|
|
|
349
|
-
const body = [
|
|
350
|
-
`Adds the Ryvn service and ${ENVIRONMENT} installation for ${APP_NAME}.`,
|
|
351
|
-
"",
|
|
352
|
-
`Also creates the ${DATABASE_SCHEMA} schema in the shared demos database.`,
|
|
353
|
-
"",
|
|
354
|
-
"After merge, Ryvn GitOps should create the installation and the release workflow can deploy the app.",
|
|
355
|
-
].join("\n");
|
|
356
|
-
|
|
357
476
|
const prUrl = run(
|
|
358
477
|
"gh",
|
|
359
478
|
[
|
|
360
479
|
"pr",
|
|
361
480
|
"create",
|
|
362
481
|
"--title",
|
|
363
|
-
|
|
482
|
+
getPrTitle(options.phase),
|
|
364
483
|
"--body",
|
|
365
|
-
|
|
484
|
+
getPrBody(options.phase),
|
|
366
485
|
],
|
|
367
486
|
infraRepo,
|
|
368
487
|
"pipe",
|
|
369
488
|
);
|
|
370
489
|
|
|
371
490
|
console.log(`Infra PR: ${prUrl}`);
|
|
491
|
+
printNextSteps(options.phase);
|
|
372
492
|
}
|
|
373
493
|
|
|
374
494
|
void main().catch((error) => {
|
|
@@ -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,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
const SHARED_DATABASES = new Set(["demos", "internal_apps"]);
|
|
6
7
|
|
|
@@ -39,7 +40,7 @@ async function main(): Promise<void> {
|
|
|
39
40
|
user,
|
|
40
41
|
password,
|
|
41
42
|
database: "postgres", // Connect to default postgres database
|
|
42
|
-
ssl: useSSL,
|
|
43
|
+
ssl: getPgSslConfig(useSSL),
|
|
43
44
|
});
|
|
44
45
|
|
|
45
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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
type EnvMap = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
const LOCAL_ENV_PATH = path.join(
|
|
11
|
+
homedir(),
|
|
12
|
+
".config",
|
|
13
|
+
"percepta",
|
|
14
|
+
"create.env",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function parseEnvFile(filePath: string): EnvMap {
|
|
18
|
+
if (!existsSync(filePath)) return {};
|
|
19
|
+
|
|
20
|
+
const env: EnvMap = {};
|
|
21
|
+
const content = readFileSync(filePath, "utf8");
|
|
22
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
23
|
+
const line = rawLine.trim();
|
|
24
|
+
if (!line || line.startsWith("#")) continue;
|
|
25
|
+
|
|
26
|
+
const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
27
|
+
const separatorIndex = normalized.indexOf("=");
|
|
28
|
+
if (separatorIndex === -1) continue;
|
|
29
|
+
|
|
30
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
31
|
+
const rawValue = normalized.slice(separatorIndex + 1).trim();
|
|
32
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
33
|
+
|
|
34
|
+
env[key] = unquote(rawValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return env;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unquote(value: string): string {
|
|
41
|
+
if (
|
|
42
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
43
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
44
|
+
) {
|
|
45
|
+
return value.slice(1, -1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [command, ...args] = process.argv.slice(2);
|
|
52
|
+
if (!command) {
|
|
53
|
+
throw new Error("Usage: tsx scripts/with-local-env.ts <command> [...args]");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = spawn(command, args, {
|
|
57
|
+
env: {
|
|
58
|
+
...parseEnvFile(LOCAL_ENV_PATH),
|
|
59
|
+
...process.env,
|
|
60
|
+
},
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.on("error", (error) => {
|
|
65
|
+
throw error;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.on("exit", (code, signal) => {
|
|
69
|
+
if (signal) {
|
|
70
|
+
process.kill(process.pid, signal);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(code ?? 1);
|
|
75
|
+
});
|
|
@@ -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">
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { Button, Input } from "@percepta/design";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
|
+
import React, { useCallback } from "react";
|
|
8
|
+
import { Controller, useForm } from "react-hook-form";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import z from "zod";
|
|
11
|
+
import { FormItem } from "../../../../components/form/FormItem";
|
|
12
|
+
import { authClient } from "../../../../lib/auth-client";
|
|
13
|
+
|
|
14
|
+
const CREDENTIALS_SCHEMA = z.object({
|
|
15
|
+
name: z.string().min(1, "Name is required"),
|
|
16
|
+
email: z.string().email("Enter a valid email address"),
|
|
17
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type Credentials = z.infer<typeof CREDENTIALS_SCHEMA>;
|
|
21
|
+
|
|
22
|
+
export function CredentialsSignUpForm() {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
control,
|
|
28
|
+
formState: { isSubmitting },
|
|
29
|
+
handleSubmit,
|
|
30
|
+
} = useForm<Credentials>({
|
|
31
|
+
resolver: zodResolver(CREDENTIALS_SCHEMA),
|
|
32
|
+
defaultValues: {
|
|
33
|
+
name: "",
|
|
34
|
+
email: "",
|
|
35
|
+
password: "",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const callbackUrl = searchParams.get("callbackUrl") ?? "/";
|
|
40
|
+
|
|
41
|
+
const submit = useCallback(
|
|
42
|
+
async ({ name, email, password }: Credentials): Promise<void> => {
|
|
43
|
+
const { error } = await authClient.signUp.email({
|
|
44
|
+
name,
|
|
45
|
+
email,
|
|
46
|
+
password,
|
|
47
|
+
callbackURL: callbackUrl,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (error != null) {
|
|
51
|
+
toast.error(error.message ?? "Unable to create account.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
router.push(callbackUrl);
|
|
56
|
+
router.refresh();
|
|
57
|
+
},
|
|
58
|
+
[callbackUrl, router],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-8">
|
|
63
|
+
<div className="text-center">
|
|
64
|
+
<h1 className="text-2xl font-bold text-foreground">Create Account</h1>
|
|
65
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
66
|
+
Already have an account?{" "}
|
|
67
|
+
<Link
|
|
68
|
+
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
69
|
+
href={`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
70
|
+
>
|
|
71
|
+
Sign in
|
|
72
|
+
</Link>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
<form className="space-y-6" onSubmit={handleSubmit(submit)}>
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
<Controller
|
|
78
|
+
control={control}
|
|
79
|
+
name="name"
|
|
80
|
+
render={({ field, fieldState }) => (
|
|
81
|
+
<FormItem label="Name" fieldState={fieldState}>
|
|
82
|
+
<Input {...field} autoComplete="name" />
|
|
83
|
+
</FormItem>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
<Controller
|
|
87
|
+
control={control}
|
|
88
|
+
name="email"
|
|
89
|
+
render={({ field, fieldState }) => (
|
|
90
|
+
<FormItem label="Email" fieldState={fieldState}>
|
|
91
|
+
<Input {...field} type="email" autoComplete="email" />
|
|
92
|
+
</FormItem>
|
|
93
|
+
)}
|
|
94
|
+
/>
|
|
95
|
+
<Controller
|
|
96
|
+
control={control}
|
|
97
|
+
name="password"
|
|
98
|
+
render={({ field, fieldState }) => (
|
|
99
|
+
<FormItem label="Password" fieldState={fieldState}>
|
|
100
|
+
<Input {...field} type="password" autoComplete="new-password" />
|
|
101
|
+
</FormItem>
|
|
102
|
+
)}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex justify-end">
|
|
106
|
+
<Button type="submit" loading={isSubmitting}>
|
|
107
|
+
Create Account
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</form>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
import { Suspense } from "react";
|
|
5
|
+
import { auth } from "../../../../lib/auth";
|
|
6
|
+
import { CredentialsSignUpForm } from "./CredentialsSignUpForm";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: "Create Account — __APP_TITLE__",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default async function SignUpPage() {
|
|
13
|
+
const session = await auth.api.getSession({
|
|
14
|
+
headers: await headers(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (session?.user != null) {
|
|
18
|
+
redirect("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Suspense
|
|
23
|
+
fallback={
|
|
24
|
+
<p className="text-center text-sm text-muted-foreground">Loading...</p>
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<CredentialsSignUpForm />
|
|
28
|
+
</Suspense>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -18,7 +18,7 @@ export default function GlobalError({
|
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<html lang="en">
|
|
21
|
-
<body suppressHydrationWarning>
|
|
21
|
+
<body suppressHydrationWarning={true}>
|
|
22
22
|
<div style={{ padding: "2rem", textAlign: "center" }}>
|
|
23
23
|
<h1>Something went wrong</h1>
|
|
24
24
|
<button onClick={() => reset()}>Try again</button>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle, Button } from "@percepta/design";
|
|
3
4
|
import { FaroProvider as BaseFaroProvider } from "@percepta/next-utils/faro";
|
|
4
|
-
import { Alert, AlertTitle, AlertDescription, Button } from "@percepta/design";
|
|
5
5
|
import { type ReactNode } from "react";
|
|
6
6
|
|
|
7
7
|
// Import to trigger Faro initialization at module scope (earliest possible)
|
|
@@ -9,9 +9,7 @@ import "../services/observability/initFaro";
|
|
|
9
9
|
|
|
10
10
|
export function AppFaroProvider({ children }: { children: ReactNode }) {
|
|
11
11
|
return (
|
|
12
|
-
<BaseFaroProvider fallback={<ErrorFallback />}>
|
|
13
|
-
{children}
|
|
14
|
-
</BaseFaroProvider>
|
|
12
|
+
<BaseFaroProvider fallback={<ErrorFallback />}>{children}</BaseFaroProvider>
|
|
15
13
|
);
|
|
16
14
|
}
|
|
17
15
|
|