supatypes 1.0.2 → 1.0.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 +2 -0
- package/bin/cli.js +2 -0
- package/lib/generate.js +169 -167
- package/lib/init.js +3 -0
- package/package.json +27 -27
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Running `init` creates a `.supatypes.json` file:
|
|
|
40
40
|
```json
|
|
41
41
|
{
|
|
42
42
|
"server": "root@1.2.3.4",
|
|
43
|
+
"sshPort": 2222,
|
|
43
44
|
"sshKey": "~/.ssh/id_rsa",
|
|
44
45
|
"dbContainer": "supabase-db-abc123",
|
|
45
46
|
"output": "./database.types.ts"
|
|
@@ -51,6 +52,7 @@ Running `init` creates a `.supatypes.json` file:
|
|
|
51
52
|
| Field | Required | Description |
|
|
52
53
|
|-------|----------|-------------|
|
|
53
54
|
| `server` | Yes | SSH connection string (e.g. `root@1.2.3.4`) |
|
|
55
|
+
| `sshPort` | No | SSH port (default: `22`) |
|
|
54
56
|
| `sshKey` | One of these | Path to SSH private key |
|
|
55
57
|
| `sshPassword` | One of these | SSH password (uses `sshpass`) |
|
|
56
58
|
| `dbContainer` | Yes | Docker container name for the Supabase PostgreSQL instance |
|
package/bin/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ Options (generate):
|
|
|
20
20
|
Config file (.supatypes.json):
|
|
21
21
|
{
|
|
22
22
|
"server": "root@1.2.3.4",
|
|
23
|
+
"sshPort": 2222,
|
|
23
24
|
"sshKey": "~/.ssh/id_rsa",
|
|
24
25
|
"dbContainer": "supabase-db-abc123",
|
|
25
26
|
"output": "./database.types.ts"
|
|
@@ -28,6 +29,7 @@ Config file (.supatypes.json):
|
|
|
28
29
|
You can also use a password instead of an SSH key:
|
|
29
30
|
{
|
|
30
31
|
"server": "root@1.2.3.4",
|
|
32
|
+
"sshPort": 2222,
|
|
31
33
|
"sshPassword": "your-password",
|
|
32
34
|
"dbContainer": "supabase-db-abc123",
|
|
33
35
|
"output": "./database.types.ts"
|
package/lib/generate.js
CHANGED
|
@@ -1,167 +1,169 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const REMOTE_SCRIPT = path.join(__dirname, "remote-generator.js");
|
|
8
|
-
const REMOTE_DIR = "/tmp/supatypes";
|
|
9
|
-
const REMOTE_SCRIPT_PATH = `${REMOTE_DIR}/generator.js`;
|
|
10
|
-
const REMOTE_OUTPUT_PATH = `${REMOTE_DIR}/output.ts`;
|
|
11
|
-
|
|
12
|
-
export async function generate({ configPath, outputOverride, dryRun }) {
|
|
13
|
-
// Load config
|
|
14
|
-
const resolvedConfig = path.resolve(configPath);
|
|
15
|
-
if (!fs.existsSync(resolvedConfig)) {
|
|
16
|
-
console.error(`Config file not found: ${resolvedConfig}`);
|
|
17
|
-
console.error('Run "npx supatypes init" to create one.');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const config = JSON.parse(fs.readFileSync(resolvedConfig, "utf8"));
|
|
22
|
-
const { server, dbContainer } = config;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
42
|
-
console.log(`
|
|
43
|
-
console.log(`
|
|
44
|
-
console.log(`
|
|
45
|
-
console.log(`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.log(
|
|
51
|
-
console.log(`
|
|
52
|
-
console.log(`
|
|
53
|
-
console.log(`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const REMOTE_SCRIPT = path.join(__dirname, "remote-generator.js");
|
|
8
|
+
const REMOTE_DIR = "/tmp/supatypes";
|
|
9
|
+
const REMOTE_SCRIPT_PATH = `${REMOTE_DIR}/generator.js`;
|
|
10
|
+
const REMOTE_OUTPUT_PATH = `${REMOTE_DIR}/output.ts`;
|
|
11
|
+
|
|
12
|
+
export async function generate({ configPath, outputOverride, dryRun }) {
|
|
13
|
+
// Load config
|
|
14
|
+
const resolvedConfig = path.resolve(configPath);
|
|
15
|
+
if (!fs.existsSync(resolvedConfig)) {
|
|
16
|
+
console.error(`Config file not found: ${resolvedConfig}`);
|
|
17
|
+
console.error('Run "npx supatypes init" to create one.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = JSON.parse(fs.readFileSync(resolvedConfig, "utf8"));
|
|
22
|
+
const { server, dbContainer } = config;
|
|
23
|
+
const sshPort = config.sshPort || 22;
|
|
24
|
+
const sshKey = config.sshKey ? expandHome(config.sshKey) : null;
|
|
25
|
+
const sshPassword = config.sshPassword || null;
|
|
26
|
+
const output = path.resolve(outputOverride || config.output || "./database.types.ts");
|
|
27
|
+
|
|
28
|
+
// Validate
|
|
29
|
+
if (!server) { console.error("Missing 'server' in config"); process.exit(1); }
|
|
30
|
+
if (!dbContainer) { console.error("Missing 'dbContainer' in config"); process.exit(1); }
|
|
31
|
+
|
|
32
|
+
if (sshKey && !fs.existsSync(sshKey)) {
|
|
33
|
+
console.error(`SSH key not found: ${sshKey}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sshBase = buildSshBase(server, sshPort, sshKey, sshPassword);
|
|
38
|
+
const scpBase = buildScpBase(server, sshPort, sshKey, sshPassword);
|
|
39
|
+
|
|
40
|
+
if (dryRun) {
|
|
41
|
+
console.log("Dry run — would execute:");
|
|
42
|
+
console.log(` 1. Create ${REMOTE_DIR} on ${server}`);
|
|
43
|
+
console.log(` 2. Upload generator script to ${REMOTE_SCRIPT_PATH}`);
|
|
44
|
+
console.log(` 3. Run: node ${REMOTE_SCRIPT_PATH} ${dbContainer} ${REMOTE_OUTPUT_PATH}`);
|
|
45
|
+
console.log(` 4. Download ${REMOTE_OUTPUT_PATH} to ${output}`);
|
|
46
|
+
console.log(` 5. Clean up ${REMOTE_DIR} on server`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log("supatypes\n");
|
|
51
|
+
console.log(` Server: ${server}`);
|
|
52
|
+
console.log(` Port: ${sshPort}`);
|
|
53
|
+
console.log(` Container: ${dbContainer}`);
|
|
54
|
+
console.log(` Output: ${output}`);
|
|
55
|
+
console.log(` Auth: ${sshKey ? "SSH key" : "password"}\n`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Step 1: Create remote directory
|
|
59
|
+
process.stdout.write("Creating remote workspace...");
|
|
60
|
+
ssh(sshBase, `mkdir -p ${REMOTE_DIR}`);
|
|
61
|
+
console.log(" done");
|
|
62
|
+
|
|
63
|
+
// Step 2: Upload generator script
|
|
64
|
+
process.stdout.write("Uploading generator...");
|
|
65
|
+
scp(scpBase, REMOTE_SCRIPT, `${server}:${REMOTE_SCRIPT_PATH}`);
|
|
66
|
+
console.log(" done");
|
|
67
|
+
|
|
68
|
+
// Step 3: Run generator on server
|
|
69
|
+
process.stdout.write("Generating types...");
|
|
70
|
+
const remoteOutput = ssh(
|
|
71
|
+
sshBase,
|
|
72
|
+
`cd ${REMOTE_DIR} && node generator.js ${dbContainer} ${REMOTE_OUTPUT_PATH}`
|
|
73
|
+
);
|
|
74
|
+
console.log(" done");
|
|
75
|
+
|
|
76
|
+
// Parse stats from output
|
|
77
|
+
const statsMatch = remoteOutput.match(/__STATS__(.+)/);
|
|
78
|
+
if (statsMatch) {
|
|
79
|
+
const stats = JSON.parse(statsMatch[1]);
|
|
80
|
+
console.log(`\n Tables: ${stats.tables}`);
|
|
81
|
+
console.log(` Views: ${stats.views}`);
|
|
82
|
+
console.log(` Functions: ${stats.functions}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 4: Download result
|
|
86
|
+
process.stdout.write("\nDownloading types...");
|
|
87
|
+
const outputDir = path.dirname(output);
|
|
88
|
+
if (!fs.existsSync(outputDir)) {
|
|
89
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
scp(scpBase, `${server}:${REMOTE_OUTPUT_PATH}`, output);
|
|
92
|
+
console.log(" done");
|
|
93
|
+
|
|
94
|
+
// Step 5: Clean up
|
|
95
|
+
process.stdout.write("Cleaning up remote files...");
|
|
96
|
+
ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
|
|
97
|
+
console.log(" done");
|
|
98
|
+
|
|
99
|
+
const fileSize = fs.statSync(output).size;
|
|
100
|
+
console.log(`\nTypes saved to ${output} (${formatBytes(fileSize)})`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`\n\nFailed: ${err.message}`);
|
|
103
|
+
|
|
104
|
+
// Try to clean up even on failure
|
|
105
|
+
try {
|
|
106
|
+
ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore cleanup errors
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function expandHome(p) {
|
|
116
|
+
if (p.startsWith("~/")) {
|
|
117
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(2));
|
|
118
|
+
}
|
|
119
|
+
return p;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildSshBase(server, port, sshKey, sshPassword) {
|
|
123
|
+
const opts = ["-p", String(port), "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
|
|
124
|
+
|
|
125
|
+
if (sshPassword) {
|
|
126
|
+
// Use sshpass for password-based auth
|
|
127
|
+
return ["sshpass", "-p", sshPassword, "ssh", ...opts, server];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (sshKey) {
|
|
131
|
+
return ["ssh", "-i", sshKey, ...opts, server];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Default: let SSH use its default key
|
|
135
|
+
return ["ssh", ...opts, server];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildScpBase(server, port, sshKey, sshPassword) {
|
|
139
|
+
const opts = ["-P", String(port), "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
|
|
140
|
+
|
|
141
|
+
if (sshPassword) {
|
|
142
|
+
return ["sshpass", "-p", sshPassword, "scp", ...opts];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (sshKey) {
|
|
146
|
+
return ["scp", "-i", sshKey, ...opts];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ["scp", ...opts];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ssh(base, command) {
|
|
153
|
+
const [bin, ...args] = base;
|
|
154
|
+
return execFileSync(bin, [...args, command], {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function scp(base, src, dest) {
|
|
161
|
+
const [bin, ...args] = base;
|
|
162
|
+
execFileSync(bin, [...args, src, dest], { encoding: "utf8" });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatBytes(bytes) {
|
|
166
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
167
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
168
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
169
|
+
}
|
package/lib/init.js
CHANGED
|
@@ -33,6 +33,7 @@ export async function init() {
|
|
|
33
33
|
|
|
34
34
|
const sshUser = await ask(rl, "SSH username", "root");
|
|
35
35
|
const sshHost = await ask(rl, "SSH server host (e.g. 1.2.3.4)", "");
|
|
36
|
+
const sshPort = await ask(rl, "SSH port", "22");
|
|
36
37
|
const server = `${sshUser}@${sshHost}`;
|
|
37
38
|
const authMethod = await ask(rl, "Auth method: key or password?", "key");
|
|
38
39
|
|
|
@@ -52,6 +53,8 @@ export async function init() {
|
|
|
52
53
|
|
|
53
54
|
const config = { server, dbContainer, output };
|
|
54
55
|
|
|
56
|
+
const parsedPort = parseInt(sshPort, 10);
|
|
57
|
+
if (parsedPort !== 22) config.sshPort = parsedPort;
|
|
55
58
|
if (sshKey) config.sshKey = sshKey;
|
|
56
59
|
if (sshPassword) config.sshPassword = sshPassword;
|
|
57
60
|
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "supatypes",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Generate TypeScript types from a remote Supabase PostgreSQL database via SSH",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "MadStoneDev",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"bin": {
|
|
9
|
-
"supatypes": "bin/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"bin/",
|
|
13
|
-
"lib/",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"keywords": [
|
|
17
|
-
"supabase",
|
|
18
|
-
"typescript",
|
|
19
|
-
"types",
|
|
20
|
-
"codegen",
|
|
21
|
-
"postgresql",
|
|
22
|
-
"database"
|
|
23
|
-
],
|
|
24
|
-
"engines": {
|
|
25
|
-
"node": ">=18"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "supatypes",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Generate TypeScript types from a remote Supabase PostgreSQL database via SSH",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "MadStoneDev",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"supatypes": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"supabase",
|
|
18
|
+
"typescript",
|
|
19
|
+
"types",
|
|
20
|
+
"codegen",
|
|
21
|
+
"postgresql",
|
|
22
|
+
"database"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
}
|
|
27
|
+
}
|