@everystack/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -0
- package/package.json +104 -0
- package/src/cli/aws.ts +121 -0
- package/src/cli/commands/analyze.ts +61 -0
- package/src/cli/commands/branches.ts +97 -0
- package/src/cli/commands/cache.ts +72 -0
- package/src/cli/commands/certs.ts +117 -0
- package/src/cli/commands/channels.ts +109 -0
- package/src/cli/commands/console.ts +68 -0
- package/src/cli/commands/db.ts +183 -0
- package/src/cli/commands/diag.ts +242 -0
- package/src/cli/commands/logs.ts +282 -0
- package/src/cli/commands/update.ts +432 -0
- package/src/cli/config.ts +98 -0
- package/src/cli/discover.ts +321 -0
- package/src/cli/hydration-analyzer.ts +224 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/output.ts +25 -0
- package/src/cli/ssr-analyzer.ts +445 -0
- package/src/cli/utils/export.ts +8 -0
- package/src/cli/utils/table.ts +39 -0
- package/src/cli/utils/upload.ts +52 -0
- package/src/cli/utils/walk.ts +59 -0
- package/src/client/app-state-provider.tsx +83 -0
- package/src/client/index.ts +2 -0
- package/src/client/updates-provider.tsx +69 -0
- package/src/handler/assets.ts +30 -0
- package/src/handler/branches.ts +70 -0
- package/src/handler/channels-crud.ts +174 -0
- package/src/handler/helpers.ts +239 -0
- package/src/handler/index.ts +78 -0
- package/src/handler/manifest.ts +276 -0
- package/src/handler/multipart.ts +74 -0
- package/src/handler/publish-web.ts +311 -0
- package/src/handler/publish.ts +346 -0
- package/src/handler/signing.ts +29 -0
- package/src/handler/types.ts +16 -0
- package/src/index.ts +4 -0
- package/src/schema.ts +245 -0
- package/src/storage/filesystem.ts +103 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/s3.ts +125 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import forge from 'node-forge';
|
|
4
|
+
|
|
5
|
+
const { pki, md, random, util } = forge;
|
|
6
|
+
|
|
7
|
+
export async function certsCommand(action: 'generate' | 'configure', flags: Record<string, string>): Promise<void> {
|
|
8
|
+
if (action === 'generate') {
|
|
9
|
+
await generateCerts(flags);
|
|
10
|
+
} else {
|
|
11
|
+
await configureCerts(flags);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toPositiveHex(hex: string): string {
|
|
16
|
+
// Ensure the serial number is positive by prepending '00' if high bit is set
|
|
17
|
+
if (hex.length > 0 && parseInt(hex[0], 16) >= 8) {
|
|
18
|
+
return '00' + hex;
|
|
19
|
+
}
|
|
20
|
+
return hex;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function generateCerts(flags: Record<string, string>): Promise<void> {
|
|
24
|
+
const outputDir = flags.output || './certs';
|
|
25
|
+
const commonName = flags.cn || 'everystack';
|
|
26
|
+
const validityYears = parseInt(flags.years || '10', 10);
|
|
27
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
// Generate RSA key pair using node-forge
|
|
30
|
+
const keyPair = pki.rsa.generateKeyPair(2048);
|
|
31
|
+
|
|
32
|
+
// Create self-signed X.509 code signing certificate
|
|
33
|
+
const cert = pki.createCertificate();
|
|
34
|
+
cert.publicKey = keyPair.publicKey;
|
|
35
|
+
cert.serialNumber = toPositiveHex(util.bytesToHex(random.getBytesSync(9)));
|
|
36
|
+
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const expiry = new Date(now);
|
|
39
|
+
expiry.setFullYear(expiry.getFullYear() + validityYears);
|
|
40
|
+
cert.validity.notBefore = now;
|
|
41
|
+
cert.validity.notAfter = expiry;
|
|
42
|
+
|
|
43
|
+
const attrs = [{ name: 'commonName', value: commonName }];
|
|
44
|
+
cert.setSubject(attrs);
|
|
45
|
+
cert.setIssuer(attrs);
|
|
46
|
+
cert.setExtensions([
|
|
47
|
+
{
|
|
48
|
+
name: 'keyUsage',
|
|
49
|
+
critical: true,
|
|
50
|
+
keyCertSign: false,
|
|
51
|
+
digitalSignature: true,
|
|
52
|
+
nonRepudiation: false,
|
|
53
|
+
keyEncipherment: false,
|
|
54
|
+
dataEncipherment: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'extKeyUsage',
|
|
58
|
+
critical: true,
|
|
59
|
+
serverAuth: false,
|
|
60
|
+
clientAuth: false,
|
|
61
|
+
codeSigning: true,
|
|
62
|
+
emailProtection: false,
|
|
63
|
+
timeStamping: false,
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
cert.sign(keyPair.privateKey, md.sha256.create());
|
|
67
|
+
|
|
68
|
+
// Export to PEM
|
|
69
|
+
const privateKeyPem = pki.privateKeyToPem(keyPair.privateKey);
|
|
70
|
+
const certificatePem = pki.certificateToPem(cert);
|
|
71
|
+
|
|
72
|
+
const privatePath = path.join(outputDir, 'private-key.pem');
|
|
73
|
+
const certPath = path.join(outputDir, 'certificate.pem');
|
|
74
|
+
|
|
75
|
+
await fs.writeFile(privatePath, privateKeyPem);
|
|
76
|
+
await fs.writeFile(certPath, certificatePem);
|
|
77
|
+
|
|
78
|
+
console.log(`Generated code signing certificate:`);
|
|
79
|
+
console.log(` Private key: ${privatePath}`);
|
|
80
|
+
console.log(` Certificate: ${certPath}`);
|
|
81
|
+
console.log(` Common Name: ${commonName}`);
|
|
82
|
+
console.log(` Valid for: ${validityYears} years`);
|
|
83
|
+
console.log(`\nKeep the private key secret. The certificate goes in your app config.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function configureCerts(flags: Record<string, string>): Promise<void> {
|
|
87
|
+
const inputDir = flags.input || './certs';
|
|
88
|
+
const keyid = flags.keyid || 'main';
|
|
89
|
+
|
|
90
|
+
const certPath = path.join(inputDir, 'certificate.pem');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(certPath);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(`Certificate not found at ${certPath}. Run 'everystack certs:generate' first.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read app.json and add code signing config
|
|
99
|
+
const appJsonPath = path.resolve('app.json');
|
|
100
|
+
let appJson: any;
|
|
101
|
+
try {
|
|
102
|
+
appJson = JSON.parse(await fs.readFile(appJsonPath, 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error('Could not read app.json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = appJson.expo || appJson;
|
|
108
|
+
config.updates = config.updates || {};
|
|
109
|
+
config.updates.codeSigningCertificate = `./${path.relative('.', certPath)}`;
|
|
110
|
+
config.updates.codeSigningMetadata = {
|
|
111
|
+
keyid,
|
|
112
|
+
alg: 'rsa-v1_5-sha256',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await fs.writeFile(appJsonPath, JSON.stringify(appJson, null, 2) + '\n');
|
|
116
|
+
console.log(`Configured code signing in app.json (keyid: ${keyid})`);
|
|
117
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export async function channelsCommand(subcommand: string, flags: Record<string, string>): Promise<void> {
|
|
2
|
+
switch (subcommand) {
|
|
3
|
+
case 'list':
|
|
4
|
+
await listChannels(flags);
|
|
5
|
+
break;
|
|
6
|
+
case 'create':
|
|
7
|
+
await createChannel(flags);
|
|
8
|
+
break;
|
|
9
|
+
case 'edit':
|
|
10
|
+
await editChannel(flags);
|
|
11
|
+
break;
|
|
12
|
+
default:
|
|
13
|
+
console.log('Usage: everystack channels <list|create|edit> [--name <name>] [--branch <name>]');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function listChannels(_flags: Record<string, string>): Promise<void> {
|
|
18
|
+
const baseUrl = getBaseUrl();
|
|
19
|
+
const token = getToken();
|
|
20
|
+
|
|
21
|
+
const response = await fetch(`${baseUrl}/channels`, {
|
|
22
|
+
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to list channels: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const channels = await response.json() as any[];
|
|
30
|
+
if (channels.length === 0) {
|
|
31
|
+
console.log('No channels found.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('Channels:');
|
|
36
|
+
for (const ch of channels) {
|
|
37
|
+
console.log(` ${ch.name} (created: ${ch.createdAt})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createChannel(flags: Record<string, string>): Promise<void> {
|
|
42
|
+
const name = flags.name;
|
|
43
|
+
if (!name) {
|
|
44
|
+
throw new Error('--name is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const baseUrl = getBaseUrl();
|
|
48
|
+
const token = getToken();
|
|
49
|
+
|
|
50
|
+
const body: Record<string, string> = { name };
|
|
51
|
+
if (flags.branch) {
|
|
52
|
+
body.branchName = flags.branch;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await fetch(`${baseUrl}/channels`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': 'application/json',
|
|
59
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const resBody = await response.json().catch(() => ({}));
|
|
66
|
+
throw new Error(`Failed to create channel: ${(resBody as any).error || response.status}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Channel "${name}" created.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function editChannel(flags: Record<string, string>): Promise<void> {
|
|
73
|
+
const name = flags.name;
|
|
74
|
+
const branchName = flags.branch;
|
|
75
|
+
if (!name) {
|
|
76
|
+
throw new Error('--name is required');
|
|
77
|
+
}
|
|
78
|
+
if (!branchName) {
|
|
79
|
+
throw new Error('--branch is required (the branch to point this channel to)');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseUrl = getBaseUrl();
|
|
83
|
+
const token = getToken();
|
|
84
|
+
|
|
85
|
+
const response = await fetch(`${baseUrl}/channels/${encodeURIComponent(name)}`, {
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
headers: {
|
|
88
|
+
'content-type': 'application/json',
|
|
89
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ branchName }),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await response.json().catch(() => ({}));
|
|
96
|
+
throw new Error(`Failed to edit channel: ${(body as any).error || response.status}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`Channel "${name}" now points to branch "${branchName}".`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getBaseUrl(): string {
|
|
103
|
+
if (process.env.EVERYSTACK_URL) return process.env.EVERYSTACK_URL;
|
|
104
|
+
throw new Error('EVERYSTACK_URL environment variable is required');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getToken(): string | undefined {
|
|
108
|
+
return process.env.EVERYSTACK_TOKEN;
|
|
109
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* everystack console --stage <name>
|
|
3
|
+
*
|
|
4
|
+
* Interactive REPL that executes expressions inside the deployed Lambda.
|
|
5
|
+
* Each line is sent via IAM-authed Lambda invoke (_action: 'console').
|
|
6
|
+
* The Lambda evaluates with db, schema, and Drizzle operators in scope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as readline from 'node:readline';
|
|
10
|
+
import { resolveConfig } from '../config.js';
|
|
11
|
+
import { invokeAction } from '../aws.js';
|
|
12
|
+
import { success, fail, info } from '../output.js';
|
|
13
|
+
import { formatResult } from '../utils/table.js';
|
|
14
|
+
|
|
15
|
+
export async function consoleCommand(flags: Record<string, string>): Promise<void> {
|
|
16
|
+
const stage = flags.stage;
|
|
17
|
+
if (!stage) {
|
|
18
|
+
fail('--stage is required. Example: everystack console --stage dev');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let config;
|
|
23
|
+
try {
|
|
24
|
+
config = await resolveConfig(stage);
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
fail(err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
success(`Connected to ${stage} (${config.region}) — ${config.apiFunctionName}`);
|
|
31
|
+
console.log(' Type expressions to evaluate. Ctrl+C or .exit to quit.\n');
|
|
32
|
+
|
|
33
|
+
const rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
prompt: `everystack:${stage}> `,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
rl.prompt();
|
|
40
|
+
|
|
41
|
+
rl.on('line', async (line) => {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed) { rl.prompt(); return; }
|
|
44
|
+
if (trimmed === '.exit') { rl.close(); return; }
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result: any = await invokeAction(
|
|
48
|
+
config.region,
|
|
49
|
+
config.apiFunctionName,
|
|
50
|
+
'console',
|
|
51
|
+
{ expression: trimmed },
|
|
52
|
+
);
|
|
53
|
+
if (result?.error) {
|
|
54
|
+
fail(result.error);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(formatResult(result?.result));
|
|
57
|
+
}
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
fail(err.message);
|
|
60
|
+
}
|
|
61
|
+
rl.prompt();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
rl.on('close', () => {
|
|
65
|
+
console.log('');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database commands — run migrations and seed via IAM-authed Lambda invoke.
|
|
3
|
+
*
|
|
4
|
+
* These commands invoke the deployed Lambda's onAction handler directly.
|
|
5
|
+
* No HTTP endpoints, no shared secrets — IAM is the authorization layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveConfig } from '../config.js';
|
|
9
|
+
import { invokeAction } from '../aws.js';
|
|
10
|
+
import { step, success, fail, info } from '../output.js';
|
|
11
|
+
|
|
12
|
+
export async function dbMigrateCommand(flags: Record<string, string>): Promise<void> {
|
|
13
|
+
step('Resolving deployed config...');
|
|
14
|
+
let config;
|
|
15
|
+
try {
|
|
16
|
+
config = await resolveConfig(flags.stage);
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
fail(err.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
23
|
+
step('Running migrations via Lambda invoke...');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result: any = await invokeAction(
|
|
27
|
+
config.region,
|
|
28
|
+
config.apiFunctionName,
|
|
29
|
+
'migrate',
|
|
30
|
+
{},
|
|
31
|
+
);
|
|
32
|
+
if (result?.error) {
|
|
33
|
+
fail(`Migration failed: ${result.error}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
success(result?.message || 'Migrations applied');
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
fail(`Migration failed: ${err.message}`);
|
|
39
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function dbSeedCommand(flags: Record<string, string>): Promise<void> {
|
|
45
|
+
step('Resolving deployed config...');
|
|
46
|
+
let config;
|
|
47
|
+
try {
|
|
48
|
+
config = await resolveConfig(flags.stage);
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
fail(err.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
55
|
+
step('Running seed via Lambda invoke...');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result: any = await invokeAction(
|
|
59
|
+
config.region,
|
|
60
|
+
config.apiFunctionName,
|
|
61
|
+
'seed',
|
|
62
|
+
{},
|
|
63
|
+
);
|
|
64
|
+
if (result?.error) {
|
|
65
|
+
fail(`Seed failed: ${result.error}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (result?.skipped) {
|
|
69
|
+
info(result.message);
|
|
70
|
+
} else {
|
|
71
|
+
success(result?.message || 'Seed complete');
|
|
72
|
+
}
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
fail(`Seed failed: ${err.message}`);
|
|
75
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function dbResetCommand(flags: Record<string, string>): Promise<void> {
|
|
81
|
+
step('Resolving deployed config...');
|
|
82
|
+
let config;
|
|
83
|
+
try {
|
|
84
|
+
config = await resolveConfig(flags.stage);
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
fail(err.message);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
91
|
+
info('This will DROP every schema (auth, dist, logs, ops, public) and re-run migrations.');
|
|
92
|
+
step('Resetting database via Lambda invoke...');
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result: any = await invokeAction(
|
|
96
|
+
config.region,
|
|
97
|
+
config.apiFunctionName,
|
|
98
|
+
'db:reset',
|
|
99
|
+
{},
|
|
100
|
+
);
|
|
101
|
+
if (result?.error) {
|
|
102
|
+
fail(`Reset failed: ${result.error}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
success(result?.message || 'Database reset and migrations applied');
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
fail(`Reset failed: ${err.message}`);
|
|
108
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function dbPsqlCommand(flags: Record<string, string>): Promise<void> {
|
|
114
|
+
step('Resolving deployed config...');
|
|
115
|
+
let config;
|
|
116
|
+
try {
|
|
117
|
+
config = await resolveConfig(flags.stage);
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
fail(err.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
info(`Region: ${config.region}, Function: ${config.apiFunctionName}`);
|
|
124
|
+
|
|
125
|
+
// If -c flag is provided, execute SQL via Lambda and return results
|
|
126
|
+
if (flags.c) {
|
|
127
|
+
step(`Executing SQL query...`);
|
|
128
|
+
try {
|
|
129
|
+
const result: any = await invokeAction(
|
|
130
|
+
config.region,
|
|
131
|
+
config.apiFunctionName,
|
|
132
|
+
'db:query',
|
|
133
|
+
{ sql: flags.c },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (result?.error) {
|
|
137
|
+
fail(`Query failed: ${result.error}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Display results in psql-like format
|
|
142
|
+
if (result.rows && result.rows.length > 0) {
|
|
143
|
+
// Get column names from first row
|
|
144
|
+
const columns = Object.keys(result.rows[0]);
|
|
145
|
+
|
|
146
|
+
// Calculate column widths
|
|
147
|
+
const widths = columns.map(col => {
|
|
148
|
+
const maxDataWidth = Math.max(
|
|
149
|
+
...result.rows.map((row: any) => String(row[col] ?? '').length)
|
|
150
|
+
);
|
|
151
|
+
return Math.max(col.length, maxDataWidth);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Print header
|
|
155
|
+
console.log(columns.map((col, i) => col.padEnd(widths[i])).join(' | '));
|
|
156
|
+
console.log(widths.map(w => '-'.repeat(w)).join('-+-'));
|
|
157
|
+
|
|
158
|
+
// Print rows
|
|
159
|
+
result.rows.forEach((row: any) => {
|
|
160
|
+
console.log(
|
|
161
|
+
columns.map((col, i) => String(row[col] ?? '').padEnd(widths[i])).join(' | ')
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log(`(${result.rows.length} row${result.rows.length === 1 ? '' : 's'})`);
|
|
166
|
+
} else {
|
|
167
|
+
success('Query executed successfully (0 rows)');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.exit(0);
|
|
171
|
+
} catch (err: any) {
|
|
172
|
+
fail(`Query failed: ${err.message}`);
|
|
173
|
+
info('Ensure your IAM user/role has lambda:InvokeFunction permission.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Interactive mode: not supported for private RDS
|
|
179
|
+
fail('Interactive psql mode not supported for private RDS instances.');
|
|
180
|
+
info('Use -c flag to execute SQL queries via Lambda: everystack db:psql -c "SELECT * FROM users;"');
|
|
181
|
+
info('The Lambda function executes queries from within the VPC where it can reach the database.');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|