@agentuity/cli 0.0.108 → 0.0.110
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/dist/build-report.d.ts +201 -0
- package/dist/build-report.d.ts.map +1 -0
- package/dist/build-report.js +335 -0
- package/dist/build-report.js.map +1 -0
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +9 -3
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/index.d.ts.map +1 -1
- package/dist/cmd/build/index.js +44 -1
- package/dist/cmd/build/index.js.map +1 -1
- package/dist/cmd/build/typecheck.d.ts +7 -1
- package/dist/cmd/build/typecheck.d.ts.map +1 -1
- package/dist/cmd/build/typecheck.js +11 -1
- package/dist/cmd/build/typecheck.js.map +1 -1
- package/dist/cmd/build/vite/index.d.ts +2 -1
- package/dist/cmd/build/vite/index.d.ts.map +1 -1
- package/dist/cmd/build/vite/index.js +2 -1
- package/dist/cmd/build/vite/index.js.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.js +2 -4
- package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/registry-generator.js +56 -18
- package/dist/cmd/build/vite/registry-generator.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts +3 -0
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +14 -1
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/build/vite-bundler.d.ts +3 -0
- package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
- package/dist/cmd/build/vite-bundler.js +14 -5
- package/dist/cmd/build/vite-bundler.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +86 -9
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
- package/dist/cmd/cloud/deployment/show.js +0 -1
- package/dist/cmd/cloud/deployment/show.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +109 -15
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/project/auth/generate.d.ts +5 -0
- package/dist/cmd/project/auth/generate.d.ts.map +1 -0
- package/dist/cmd/project/auth/generate.js +102 -0
- package/dist/cmd/project/auth/generate.js.map +1 -0
- package/dist/cmd/project/auth/index.d.ts +2 -0
- package/dist/cmd/project/auth/index.d.ts.map +1 -0
- package/dist/cmd/project/auth/index.js +21 -0
- package/dist/cmd/project/auth/index.js.map +1 -0
- package/dist/cmd/project/auth/init.d.ts +2 -0
- package/dist/cmd/project/auth/init.d.ts.map +1 -0
- package/dist/cmd/project/auth/init.js +220 -0
- package/dist/cmd/project/auth/init.js.map +1 -0
- package/dist/cmd/project/auth/shared.d.ts +88 -0
- package/dist/cmd/project/auth/shared.d.ts.map +1 -0
- package/dist/cmd/project/auth/shared.js +435 -0
- package/dist/cmd/project/auth/shared.js.map +1 -0
- package/dist/cmd/project/index.d.ts.map +1 -1
- package/dist/cmd/project/index.js +9 -1
- package/dist/cmd/project/index.js.map +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +106 -0
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/types.d.ts +1 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/package.json +5 -4
- package/src/build-report.ts +457 -0
- package/src/cmd/build/entry-generator.ts +9 -3
- package/src/cmd/build/index.ts +51 -1
- package/src/cmd/build/typecheck.ts +19 -1
- package/src/cmd/build/vite/index.ts +4 -1
- package/src/cmd/build/vite/metadata-generator.ts +4 -6
- package/src/cmd/build/vite/registry-generator.ts +58 -19
- package/src/cmd/build/vite/vite-builder.ts +18 -1
- package/src/cmd/build/vite-bundler.ts +17 -4
- package/src/cmd/cloud/deploy.ts +105 -11
- package/src/cmd/cloud/deployment/show.ts +0 -1
- package/src/cmd/dev/index.ts +115 -14
- package/src/cmd/project/auth/generate.ts +116 -0
- package/src/cmd/project/auth/index.ts +21 -0
- package/src/cmd/project/auth/init.ts +263 -0
- package/src/cmd/project/auth/shared.ts +534 -0
- package/src/cmd/project/index.ts +9 -1
- package/src/cmd/project/template-flow.ts +125 -0
- package/src/types.ts +1 -2
package/src/cmd/dev/index.ts
CHANGED
|
@@ -371,6 +371,7 @@ export const command = createCommand({
|
|
|
371
371
|
// Vite stays running and handles frontend changes via HMR
|
|
372
372
|
let shouldRestart = false;
|
|
373
373
|
let gravityProcess: ProcessLike | null = null;
|
|
374
|
+
let gravityHeartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
374
375
|
let stdinListenerRegistered = false; // Track if stdin listener is already registered
|
|
375
376
|
|
|
376
377
|
const restartServer = () => {
|
|
@@ -395,6 +396,8 @@ export const command = createCommand({
|
|
|
395
396
|
let cleaningUp = false;
|
|
396
397
|
// Track if shutdown was requested (SIGINT/SIGTERM) to break the main loop
|
|
397
398
|
let shutdownRequested = false;
|
|
399
|
+
// Store stdin data handler reference for cleanup
|
|
400
|
+
let stdinDataHandler: ((data: Buffer | string) => void) | null = null;
|
|
398
401
|
|
|
399
402
|
/**
|
|
400
403
|
* Centralized cleanup function for all resources.
|
|
@@ -425,6 +428,12 @@ export const command = createCommand({
|
|
|
425
428
|
logger.debug('Error stopping Bun server during cleanup: %s', err);
|
|
426
429
|
}
|
|
427
430
|
|
|
431
|
+
// Stop gravity heartbeat interval
|
|
432
|
+
if (gravityHeartbeatInterval) {
|
|
433
|
+
clearInterval(gravityHeartbeatInterval);
|
|
434
|
+
gravityHeartbeatInterval = null;
|
|
435
|
+
}
|
|
436
|
+
|
|
428
437
|
// Kill gravity client with SIGTERM first, then SIGKILL as fallback
|
|
429
438
|
if (gravityProcess) {
|
|
430
439
|
logger.debug('Killing gravity process...');
|
|
@@ -479,6 +488,21 @@ export const command = createCommand({
|
|
|
479
488
|
if (!exitAfter) {
|
|
480
489
|
cleaningUp = false;
|
|
481
490
|
} else {
|
|
491
|
+
// Clean up stdin keyboard handler right before exiting
|
|
492
|
+
// This must happen AFTER all async cleanup to keep event loop alive
|
|
493
|
+
if (stdinListenerRegistered && process.stdin.isTTY) {
|
|
494
|
+
try {
|
|
495
|
+
if (stdinDataHandler) {
|
|
496
|
+
process.stdin.removeListener('data', stdinDataHandler);
|
|
497
|
+
stdinDataHandler = null;
|
|
498
|
+
}
|
|
499
|
+
process.stdin.setRawMode(false);
|
|
500
|
+
process.stdin.pause();
|
|
501
|
+
process.stdin.unref();
|
|
502
|
+
} catch {
|
|
503
|
+
// Ignore errors during final cleanup
|
|
504
|
+
}
|
|
505
|
+
}
|
|
482
506
|
logger.debug('Exiting with code %d', exitCode);
|
|
483
507
|
originalExit(exitCode);
|
|
484
508
|
}
|
|
@@ -497,6 +521,12 @@ export const command = createCommand({
|
|
|
497
521
|
logger.debug('Error stopping Bun server for restart: %s', err);
|
|
498
522
|
}
|
|
499
523
|
|
|
524
|
+
// Stop gravity heartbeat interval
|
|
525
|
+
if (gravityHeartbeatInterval) {
|
|
526
|
+
clearInterval(gravityHeartbeatInterval);
|
|
527
|
+
gravityHeartbeatInterval = null;
|
|
528
|
+
}
|
|
529
|
+
|
|
500
530
|
// Kill gravity client
|
|
501
531
|
if (gravityProcess) {
|
|
502
532
|
try {
|
|
@@ -515,23 +545,37 @@ export const command = createCommand({
|
|
|
515
545
|
|
|
516
546
|
// SIGINT/SIGTERM: coordinate shutdown between bundle and dev resources
|
|
517
547
|
let signalHandlersRegistered = false;
|
|
548
|
+
let exitingFromSignal = false;
|
|
518
549
|
if (!signalHandlersRegistered) {
|
|
519
550
|
signalHandlersRegistered = true;
|
|
520
551
|
|
|
521
|
-
const safeExit =
|
|
552
|
+
const safeExit = (code: number, reason?: string) => {
|
|
553
|
+
// Prevent multiple signal handlers from racing
|
|
554
|
+
if (exitingFromSignal) return;
|
|
555
|
+
exitingFromSignal = true;
|
|
556
|
+
|
|
522
557
|
if (reason) {
|
|
523
558
|
logger.debug('DevMode terminating (%d) due to: %s', code, reason);
|
|
524
559
|
}
|
|
525
560
|
shutdownRequested = true;
|
|
526
|
-
|
|
561
|
+
// Run cleanup and ensure we wait for it to complete before exiting
|
|
562
|
+
cleanup(true, code).catch((err) => {
|
|
563
|
+
logger.debug('Cleanup error: %s', err);
|
|
564
|
+
originalExit(1);
|
|
565
|
+
});
|
|
527
566
|
};
|
|
528
567
|
|
|
529
568
|
process.on('SIGINT', () => {
|
|
530
|
-
|
|
569
|
+
safeExit(0, 'SIGINT');
|
|
531
570
|
});
|
|
532
571
|
|
|
533
572
|
process.on('SIGTERM', () => {
|
|
534
|
-
|
|
573
|
+
safeExit(0, 'SIGTERM');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Handle SIGHUP (terminal closed) - same as SIGINT
|
|
577
|
+
process.on('SIGHUP', () => {
|
|
578
|
+
safeExit(0, 'SIGHUP');
|
|
535
579
|
});
|
|
536
580
|
|
|
537
581
|
// Handle uncaught exceptions - clean up and exit rather than limping on
|
|
@@ -553,6 +597,20 @@ export const command = createCommand({
|
|
|
553
597
|
|
|
554
598
|
// Ensure resources are always cleaned up on exit (synchronous fallback)
|
|
555
599
|
process.on('exit', () => {
|
|
600
|
+
// Clean up stdin keyboard handler
|
|
601
|
+
if (stdinListenerRegistered && process.stdin.isTTY) {
|
|
602
|
+
try {
|
|
603
|
+
if (stdinDataHandler) {
|
|
604
|
+
process.stdin.removeListener('data', stdinDataHandler);
|
|
605
|
+
}
|
|
606
|
+
process.stdin.setRawMode(false);
|
|
607
|
+
process.stdin.pause();
|
|
608
|
+
process.stdin.unref();
|
|
609
|
+
} catch {
|
|
610
|
+
// Ignore errors during exit cleanup
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
556
614
|
// Kill gravity client with SIGKILL for immediate termination
|
|
557
615
|
if (gravityProcess && gravityProcess.exitCode === null) {
|
|
558
616
|
try {
|
|
@@ -862,6 +920,7 @@ export const command = createCommand({
|
|
|
862
920
|
project.projectId,
|
|
863
921
|
'--token',
|
|
864
922
|
process.env.AGENTUITY_SDK_KEY!, // set above
|
|
923
|
+
'--health-check',
|
|
865
924
|
],
|
|
866
925
|
{
|
|
867
926
|
cwd: rootDir,
|
|
@@ -881,13 +940,46 @@ export const command = createCommand({
|
|
|
881
940
|
});
|
|
882
941
|
}
|
|
883
942
|
|
|
884
|
-
// Log gravity output
|
|
943
|
+
// Log gravity output and detect heartbeat port
|
|
885
944
|
(async () => {
|
|
886
945
|
try {
|
|
887
946
|
if (gravityProcess?.stdout) {
|
|
888
947
|
for await (const chunk of gravityProcess.stdout) {
|
|
889
948
|
const text = new TextDecoder().decode(chunk);
|
|
890
|
-
|
|
949
|
+
const trimmed = text.trim();
|
|
950
|
+
|
|
951
|
+
// Check for heartbeat port announcement
|
|
952
|
+
const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
|
|
953
|
+
if (match) {
|
|
954
|
+
const heartbeatPort = parseInt(match[1], 10);
|
|
955
|
+
logger.debug('Gravity heartbeat port detected: %d', heartbeatPort);
|
|
956
|
+
|
|
957
|
+
// Start sending heartbeats every 5 seconds
|
|
958
|
+
if (!gravityHeartbeatInterval) {
|
|
959
|
+
const sendHeartbeat = async () => {
|
|
960
|
+
try {
|
|
961
|
+
await fetch(
|
|
962
|
+
`http://127.0.0.1:${heartbeatPort}/heartbeat`,
|
|
963
|
+
{
|
|
964
|
+
method: 'POST',
|
|
965
|
+
signal: AbortSignal.timeout(2000),
|
|
966
|
+
}
|
|
967
|
+
);
|
|
968
|
+
logger.trace('Gravity heartbeat sent');
|
|
969
|
+
} catch (err) {
|
|
970
|
+
logger.trace('Gravity heartbeat failed: %s', err);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// Send initial heartbeat immediately
|
|
975
|
+
sendHeartbeat();
|
|
976
|
+
|
|
977
|
+
// Then send every 5 seconds
|
|
978
|
+
gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
|
|
979
|
+
}
|
|
980
|
+
} else if (trimmed) {
|
|
981
|
+
logger.debug('[gravity] %s', trimmed);
|
|
982
|
+
}
|
|
891
983
|
}
|
|
892
984
|
}
|
|
893
985
|
} catch (err) {
|
|
@@ -930,12 +1022,23 @@ export const command = createCommand({
|
|
|
930
1022
|
console.log(tui.muted(' q') + ' - quit\n');
|
|
931
1023
|
};
|
|
932
1024
|
|
|
933
|
-
|
|
1025
|
+
// Store handler reference for cleanup
|
|
1026
|
+
stdinDataHandler = (data) => {
|
|
934
1027
|
const key = data.toString();
|
|
935
1028
|
|
|
936
|
-
// Handle Ctrl+C
|
|
937
|
-
if (key === '\u0003') {
|
|
938
|
-
|
|
1029
|
+
// Handle Ctrl+C or q - trigger graceful shutdown
|
|
1030
|
+
if (key === '\u0003' || key === 'q') {
|
|
1031
|
+
// Remove stdin listener immediately to prevent re-entrancy
|
|
1032
|
+
if (stdinDataHandler) {
|
|
1033
|
+
process.stdin.removeListener('data', stdinDataHandler);
|
|
1034
|
+
stdinDataHandler = null;
|
|
1035
|
+
}
|
|
1036
|
+
// Set shutdown flag and trigger cleanup directly
|
|
1037
|
+
shutdownRequested = true;
|
|
1038
|
+
cleanup(true, 0).catch((err) => {
|
|
1039
|
+
logger.debug('Cleanup error: %s', err);
|
|
1040
|
+
originalExit(1);
|
|
1041
|
+
});
|
|
939
1042
|
return;
|
|
940
1043
|
}
|
|
941
1044
|
|
|
@@ -952,14 +1055,12 @@ export const command = createCommand({
|
|
|
952
1055
|
centerTitle: false,
|
|
953
1056
|
});
|
|
954
1057
|
break;
|
|
955
|
-
case 'q':
|
|
956
|
-
void cleanup(true, 0);
|
|
957
|
-
break;
|
|
958
1058
|
default:
|
|
959
1059
|
process.stdout.write(data);
|
|
960
1060
|
break;
|
|
961
1061
|
}
|
|
962
|
-
}
|
|
1062
|
+
};
|
|
1063
|
+
process.stdin.on('data', stdinDataHandler);
|
|
963
1064
|
}
|
|
964
1065
|
|
|
965
1066
|
showWelcome();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate auth schema SQL using drizzle-kit export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { createSubcommand } from '../../../types';
|
|
9
|
+
import * as tui from '../../../tui';
|
|
10
|
+
import { getCommand } from '../../../command-prefix';
|
|
11
|
+
import { generateAuthSchemaSql, getGeneratedSqlDir } from './shared';
|
|
12
|
+
|
|
13
|
+
export const generateSubcommand = createSubcommand({
|
|
14
|
+
name: 'generate',
|
|
15
|
+
description: 'Generate SQL schema for Agentuity Auth tables',
|
|
16
|
+
tags: ['slow'],
|
|
17
|
+
requires: { project: true },
|
|
18
|
+
examples: [
|
|
19
|
+
{
|
|
20
|
+
command: getCommand('project auth generate'),
|
|
21
|
+
description: 'Generate SQL schema and save to agentuity-auth-schema.sql',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: getCommand('project auth generate --output ./migrations/auth.sql'),
|
|
25
|
+
description: 'Generate schema to a custom path',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: getCommand('project auth generate --output -'),
|
|
29
|
+
description: 'Output SQL to stdout',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
schema: {
|
|
33
|
+
options: z.object({
|
|
34
|
+
output: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(
|
|
38
|
+
'Output path for generated SQL (default: ./agentuity-auth-schema.sql). Use "-" for stdout.'
|
|
39
|
+
),
|
|
40
|
+
}),
|
|
41
|
+
response: z.object({
|
|
42
|
+
success: z.boolean().describe('Whether generation succeeded'),
|
|
43
|
+
outputPath: z.string().optional().describe('Path where SQL was written'),
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async handler(ctx) {
|
|
48
|
+
const { logger, opts, projectDir, options } = ctx;
|
|
49
|
+
const explicitOutput = opts?.output as string | undefined;
|
|
50
|
+
const toStdout = explicitOutput === '-';
|
|
51
|
+
const isJson = options?.json === true;
|
|
52
|
+
|
|
53
|
+
if (!toStdout && !isJson) {
|
|
54
|
+
tui.newline();
|
|
55
|
+
tui.info(tui.bold('Agentuity Auth Schema Generation'));
|
|
56
|
+
tui.newline();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const sql = isJson
|
|
61
|
+
? await generateAuthSchemaSql(projectDir)
|
|
62
|
+
: await tui.spinner({
|
|
63
|
+
message: 'Generating auth schema SQL from Drizzle schema',
|
|
64
|
+
clearOnSuccess: true,
|
|
65
|
+
callback: () => generateAuthSchemaSql(projectDir),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (toStdout) {
|
|
69
|
+
console.log(sql);
|
|
70
|
+
return { success: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let outputPath: string;
|
|
74
|
+
let displayPath: string;
|
|
75
|
+
|
|
76
|
+
if (explicitOutput) {
|
|
77
|
+
outputPath = path.resolve(projectDir, explicitOutput);
|
|
78
|
+
displayPath = explicitOutput;
|
|
79
|
+
} else {
|
|
80
|
+
const sqlOutputDir = await getGeneratedSqlDir(projectDir);
|
|
81
|
+
const sqlFileName = 'agentuity-auth-schema.sql';
|
|
82
|
+
outputPath = path.join(sqlOutputDir, sqlFileName);
|
|
83
|
+
displayPath =
|
|
84
|
+
sqlOutputDir === projectDir ? sqlFileName : path.relative(projectDir, outputPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(outputPath, sql);
|
|
88
|
+
|
|
89
|
+
if (!isJson) {
|
|
90
|
+
tui.success(`Auth schema SQL saved to ${tui.bold(displayPath)}`);
|
|
91
|
+
tui.newline();
|
|
92
|
+
tui.info('Next steps:');
|
|
93
|
+
console.log(' 1. Review the generated SQL file');
|
|
94
|
+
console.log(' 2. Run the SQL against your database');
|
|
95
|
+
console.log(` ${tui.muted('Or use: agentuity project auth init')}`);
|
|
96
|
+
tui.newline();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { success: true, outputPath };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error('Schema generation failed', { error });
|
|
102
|
+
|
|
103
|
+
if (!isJson) {
|
|
104
|
+
tui.error(
|
|
105
|
+
`Schema generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
106
|
+
);
|
|
107
|
+
tui.newline();
|
|
108
|
+
tui.info('Make sure you have:');
|
|
109
|
+
console.log(' 1. @agentuity/auth installed as a dependency');
|
|
110
|
+
console.log(' 2. drizzle-kit available (installed with @agentuity/auth)');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { success: false };
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createCommand } from '../../../types';
|
|
2
|
+
import { initSubcommand } from './init';
|
|
3
|
+
import { generateSubcommand } from './generate';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
|
|
6
|
+
export const authCommand = createCommand({
|
|
7
|
+
name: 'auth',
|
|
8
|
+
description: 'Manage project authentication (Agentuity Auth)',
|
|
9
|
+
tags: ['slow', 'requires-auth'],
|
|
10
|
+
examples: [
|
|
11
|
+
{
|
|
12
|
+
command: getCommand('project auth init'),
|
|
13
|
+
description: 'Set up Agentuity Auth for an existing project',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
command: getCommand('project auth generate'),
|
|
17
|
+
description: 'Generate SQL schema for auth tables',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
subcommands: [initSubcommand, generateSubcommand],
|
|
21
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
import {
|
|
6
|
+
selectOrCreateDatabase,
|
|
7
|
+
ensureAuthDependencies,
|
|
8
|
+
runAuthMigrations,
|
|
9
|
+
generateAuthFileContent,
|
|
10
|
+
printIntegrationExamples,
|
|
11
|
+
detectOrmSetup,
|
|
12
|
+
generateAuthSchemaSql,
|
|
13
|
+
getGeneratedSqlDir,
|
|
14
|
+
} from './shared';
|
|
15
|
+
import enquirer from 'enquirer';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
|
|
19
|
+
export const initSubcommand = createSubcommand({
|
|
20
|
+
name: 'init',
|
|
21
|
+
description: 'Set up Agentuity Auth for your project',
|
|
22
|
+
tags: ['mutating', 'slow', 'requires-auth'],
|
|
23
|
+
requires: { auth: true, org: true, region: true },
|
|
24
|
+
idempotent: false,
|
|
25
|
+
examples: [
|
|
26
|
+
{
|
|
27
|
+
command: getCommand('project auth init'),
|
|
28
|
+
description: 'Set up Agentuity Auth with database selection',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
schema: {
|
|
32
|
+
options: z.object({
|
|
33
|
+
skipMigrations: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
'Skip running database migrations (run `agentuity project auth generate` later)'
|
|
38
|
+
),
|
|
39
|
+
}),
|
|
40
|
+
response: z.object({
|
|
41
|
+
success: z.boolean().describe('Whether setup succeeded'),
|
|
42
|
+
database: z.string().optional().describe('Database name used'),
|
|
43
|
+
authFileCreated: z.boolean().describe('Whether auth.ts was created'),
|
|
44
|
+
migrationsRun: z.boolean().describe('Whether migrations were run'),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async handler(ctx) {
|
|
49
|
+
const { logger, opts, auth, orgId, region } = ctx;
|
|
50
|
+
|
|
51
|
+
tui.newline();
|
|
52
|
+
tui.info(tui.bold('Agentuity Auth Setup'));
|
|
53
|
+
tui.newline();
|
|
54
|
+
tui.info('This will:');
|
|
55
|
+
console.log(' • Ensure you have a Postgres database configured');
|
|
56
|
+
console.log(' • Install @agentuity/auth');
|
|
57
|
+
console.log(' • Run database migrations to create auth tables');
|
|
58
|
+
console.log(' • Show you how to wire auth into your API and UI');
|
|
59
|
+
tui.newline();
|
|
60
|
+
|
|
61
|
+
const projectDir = process.cwd();
|
|
62
|
+
|
|
63
|
+
// Check for package.json
|
|
64
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
65
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
66
|
+
tui.fatal('No package.json found. Run this command from your project root.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 1: Check for DATABASE_URL or select/create database
|
|
70
|
+
let databaseUrl = process.env.DATABASE_URL;
|
|
71
|
+
|
|
72
|
+
if (!databaseUrl) {
|
|
73
|
+
// Check .env file
|
|
74
|
+
const envPath = path.join(projectDir, '.env');
|
|
75
|
+
if (fs.existsSync(envPath)) {
|
|
76
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
77
|
+
const match = envContent.match(/^DATABASE_URL=(.+)$/m);
|
|
78
|
+
if (match) {
|
|
79
|
+
databaseUrl = match[1].trim().replace(/^["']|["']$/g, '');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Show database picker (with existing as first option if configured)
|
|
85
|
+
const dbInfo = await selectOrCreateDatabase({
|
|
86
|
+
logger,
|
|
87
|
+
auth,
|
|
88
|
+
orgId,
|
|
89
|
+
region,
|
|
90
|
+
existingUrl: databaseUrl,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const databaseName = dbInfo.name;
|
|
94
|
+
|
|
95
|
+
// Update .env with database URL
|
|
96
|
+
const envPath = path.join(projectDir, '.env');
|
|
97
|
+
let envContent = '';
|
|
98
|
+
|
|
99
|
+
if (fs.existsSync(envPath)) {
|
|
100
|
+
envContent = fs.readFileSync(envPath, 'utf-8');
|
|
101
|
+
if (!envContent.endsWith('\n') && envContent.length > 0) {
|
|
102
|
+
envContent += '\n';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if DATABASE_URL already exists
|
|
107
|
+
const hasDatabaseUrl = envContent.match(/^DATABASE_URL=/m);
|
|
108
|
+
|
|
109
|
+
if (dbInfo.url !== databaseUrl || !hasDatabaseUrl) {
|
|
110
|
+
if (hasDatabaseUrl) {
|
|
111
|
+
// DATABASE_URL exists, use AUTH_DATABASE_URL instead
|
|
112
|
+
envContent += `AUTH_DATABASE_URL="${dbInfo.url}"\n`;
|
|
113
|
+
fs.writeFileSync(envPath, envContent);
|
|
114
|
+
tui.success('AUTH_DATABASE_URL added to .env');
|
|
115
|
+
tui.warning(
|
|
116
|
+
`DATABASE_URL already exists. Update your ${tui.bold('src/auth.ts')} to use AUTH_DATABASE_URL.`
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
envContent += `DATABASE_URL="${dbInfo.url}"\n`;
|
|
120
|
+
fs.writeFileSync(envPath, envContent);
|
|
121
|
+
tui.success('DATABASE_URL added to .env');
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
tui.success(`Using database: ${databaseName}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add AGENTUITY_AUTH_SECRET if not present
|
|
128
|
+
// Re-read envContent to get latest state
|
|
129
|
+
envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
130
|
+
if (!envContent.endsWith('\n') && envContent.length > 0) {
|
|
131
|
+
envContent += '\n';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hasAuthSecret =
|
|
135
|
+
envContent.match(/^AGENTUITY_AUTH_SECRET=/m) || envContent.match(/^BETTER_AUTH_SECRET=/m);
|
|
136
|
+
if (!hasAuthSecret) {
|
|
137
|
+
const devSecret = `dev-${crypto.randomUUID()}-CHANGE-ME`;
|
|
138
|
+
envContent += `AGENTUITY_AUTH_SECRET="${devSecret}"\n`;
|
|
139
|
+
fs.writeFileSync(envPath, envContent);
|
|
140
|
+
tui.success('AGENTUITY_AUTH_SECRET added to .env (development default)');
|
|
141
|
+
tui.warning(
|
|
142
|
+
`Replace ${tui.bold('AGENTUITY_AUTH_SECRET')} with a secure value before deploying.`
|
|
143
|
+
);
|
|
144
|
+
tui.info(`Generate one with: ${tui.muted('openssl rand -hex 32')}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 2: Install dependencies
|
|
148
|
+
tui.newline();
|
|
149
|
+
await ensureAuthDependencies({ projectDir, logger });
|
|
150
|
+
|
|
151
|
+
// Step 3: Generate auth.ts if it doesn't exist
|
|
152
|
+
const authFilePath = path.join(projectDir, 'src', 'auth.ts');
|
|
153
|
+
let authFileCreated = false;
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(authFilePath)) {
|
|
156
|
+
tui.info('src/auth.ts already exists, skipping generation');
|
|
157
|
+
} else {
|
|
158
|
+
const { createFile } = await enquirer.prompt<{ createFile: boolean }>({
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
name: 'createFile',
|
|
161
|
+
message: 'Create src/auth.ts with default configuration?',
|
|
162
|
+
initial: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (createFile) {
|
|
166
|
+
// Ensure src directory exists
|
|
167
|
+
const srcDir = path.join(projectDir, 'src');
|
|
168
|
+
if (!fs.existsSync(srcDir)) {
|
|
169
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.writeFileSync(authFilePath, generateAuthFileContent());
|
|
173
|
+
tui.success('Created src/auth.ts');
|
|
174
|
+
authFileCreated = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 4: Run migrations (ORM-aware)
|
|
179
|
+
let migrationsRun = false;
|
|
180
|
+
|
|
181
|
+
if (opts.skipMigrations) {
|
|
182
|
+
tui.info('Skipping migrations (run `agentuity project auth generate` later)');
|
|
183
|
+
} else if (databaseName) {
|
|
184
|
+
tui.newline();
|
|
185
|
+
|
|
186
|
+
const ormSetup = await detectOrmSetup(projectDir);
|
|
187
|
+
|
|
188
|
+
if (ormSetup === 'drizzle') {
|
|
189
|
+
tui.info(tui.bold('Drizzle detected in your project.'));
|
|
190
|
+
tui.newline();
|
|
191
|
+
console.log(
|
|
192
|
+
' Since you manage your own Drizzle schema, add authSchema to your schema:'
|
|
193
|
+
);
|
|
194
|
+
tui.newline();
|
|
195
|
+
console.log(tui.muted(" import * as authSchema from '@agentuity/auth/schema';"));
|
|
196
|
+
console.log(tui.muted(' export const schema = { ...authSchema, ...yourSchema };'));
|
|
197
|
+
tui.newline();
|
|
198
|
+
console.log(' Then run migrations:');
|
|
199
|
+
console.log(tui.muted(' bunx drizzle-kit push'));
|
|
200
|
+
tui.newline();
|
|
201
|
+
} else if (ormSetup === 'prisma') {
|
|
202
|
+
tui.info(tui.bold('Prisma detected in your project.'));
|
|
203
|
+
tui.newline();
|
|
204
|
+
|
|
205
|
+
const sql = await tui.spinner({
|
|
206
|
+
message: 'Preparing auth database schema...',
|
|
207
|
+
clearOnSuccess: true,
|
|
208
|
+
callback: () => generateAuthSchemaSql(projectDir),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const sqlOutputDir = await getGeneratedSqlDir(projectDir);
|
|
212
|
+
const sqlFileName = 'agentuity-auth-schema.sql';
|
|
213
|
+
const sqlFilePath = path.join(sqlOutputDir, sqlFileName);
|
|
214
|
+
const relativePath =
|
|
215
|
+
sqlOutputDir === projectDir ? sqlFileName : path.relative(projectDir, sqlFilePath);
|
|
216
|
+
fs.writeFileSync(sqlFilePath, sql);
|
|
217
|
+
tui.success(`Auth schema SQL saved to ${tui.bold(relativePath)}`);
|
|
218
|
+
tui.newline();
|
|
219
|
+
console.log(' Run this SQL against your database to create auth tables.');
|
|
220
|
+
tui.newline();
|
|
221
|
+
} else {
|
|
222
|
+
const { runMigrations } = await enquirer.prompt<{ runMigrations: boolean }>({
|
|
223
|
+
type: 'confirm',
|
|
224
|
+
name: 'runMigrations',
|
|
225
|
+
message: 'Run database migrations now? (idempotent, safe to re-run)',
|
|
226
|
+
initial: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (runMigrations) {
|
|
230
|
+
const sql = await tui.spinner({
|
|
231
|
+
message: 'Preparing auth database schema...',
|
|
232
|
+
clearOnSuccess: true,
|
|
233
|
+
callback: () => generateAuthSchemaSql(projectDir),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await runAuthMigrations({
|
|
237
|
+
logger,
|
|
238
|
+
auth,
|
|
239
|
+
orgId,
|
|
240
|
+
region,
|
|
241
|
+
databaseName,
|
|
242
|
+
sql,
|
|
243
|
+
});
|
|
244
|
+
migrationsRun = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
tui.warning(
|
|
249
|
+
'Could not determine database name. Run `agentuity project auth generate` manually.'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Step 5: Print integration examples
|
|
254
|
+
printIntegrationExamples();
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
database: databaseName,
|
|
259
|
+
authFileCreated,
|
|
260
|
+
migrationsRun,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
});
|