@electric-sql/start 1.0.1 → 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/dist/bin.js +169 -25
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-DWNDVGD3.js → chunk-RTAT4KWN.js} +8 -3
- package/dist/chunk-RTAT4KWN.js.map +1 -0
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/cli.ts +209 -27
- package/src/template-setup.ts +13 -4
- package/dist/chunk-DWNDVGD3.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,39 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import {
|
|
2
3
|
provisionElectricResources,
|
|
3
4
|
setupTemplate
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RTAT4KWN.js";
|
|
5
6
|
|
|
6
7
|
// src/cli.ts
|
|
7
8
|
import { execSync } from "child_process";
|
|
9
|
+
import { createInterface } from "readline";
|
|
8
10
|
import { join } from "path";
|
|
9
|
-
function
|
|
11
|
+
function parseArgs(args) {
|
|
12
|
+
const positionalArgs = [];
|
|
13
|
+
let sourceId;
|
|
14
|
+
let secret;
|
|
15
|
+
let databaseUrl;
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
if (args[i] === `--source`) {
|
|
18
|
+
sourceId = args[i + 1];
|
|
19
|
+
if (!sourceId || sourceId.startsWith(`-`)) {
|
|
20
|
+
console.error(`Error: --source requires a source ID value`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
i++;
|
|
24
|
+
} else if (args[i] === `--secret`) {
|
|
25
|
+
secret = args[i + 1];
|
|
26
|
+
if (!secret || secret.startsWith(`-`)) {
|
|
27
|
+
console.error(`Error: --secret requires a value`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
i++;
|
|
31
|
+
} else if (args[i] === `--database-url`) {
|
|
32
|
+
databaseUrl = args[i + 1];
|
|
33
|
+
if (!databaseUrl || databaseUrl.startsWith(`-`)) {
|
|
34
|
+
console.error(`Error: --database-url requires a value`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
i++;
|
|
38
|
+
} else if (!args[i].startsWith(`-`)) {
|
|
39
|
+
positionalArgs.push(args[i]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (positionalArgs.length === 0) {
|
|
43
|
+
console.error(
|
|
44
|
+
`Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`
|
|
45
|
+
);
|
|
46
|
+
console.error(
|
|
47
|
+
` npx @electric-sql/start . (configure current directory)`
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (positionalArgs.length > 1) {
|
|
52
|
+
console.error(
|
|
53
|
+
`Error: Expected only one app name, but received multiple: ${positionalArgs.join(`, `)}`
|
|
54
|
+
);
|
|
55
|
+
console.error(
|
|
56
|
+
`Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return { appName: positionalArgs[0], sourceId, secret, databaseUrl };
|
|
61
|
+
}
|
|
62
|
+
function prompt(question) {
|
|
63
|
+
const rl = createInterface({
|
|
64
|
+
input: process.stdin,
|
|
65
|
+
output: process.stdout
|
|
66
|
+
});
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
rl.question(question, (answer) => {
|
|
69
|
+
rl.close();
|
|
70
|
+
resolve(answer);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function promptPassword(question) {
|
|
75
|
+
const stdin = process.stdin;
|
|
76
|
+
if (!stdin.isTTY) {
|
|
77
|
+
return prompt(question);
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
process.stdout.write(question);
|
|
81
|
+
const wasRaw = stdin.isRaw;
|
|
82
|
+
stdin.setRawMode(true);
|
|
83
|
+
stdin.resume();
|
|
84
|
+
stdin.setEncoding(`utf8`);
|
|
85
|
+
let password = ``;
|
|
86
|
+
const onData = (char) => {
|
|
87
|
+
const code = char.charCodeAt(0);
|
|
88
|
+
if (char === `\r` || char === `
|
|
89
|
+
` || code === 13) {
|
|
90
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
91
|
+
stdin.removeListener(`data`, onData);
|
|
92
|
+
stdin.pause();
|
|
93
|
+
process.stdout.write(`
|
|
94
|
+
`);
|
|
95
|
+
resolve(password);
|
|
96
|
+
} else if (code === 127 || code === 8) {
|
|
97
|
+
if (password.length > 0) {
|
|
98
|
+
password = password.slice(0, -1);
|
|
99
|
+
process.stdout.write(`\b \b`);
|
|
100
|
+
}
|
|
101
|
+
} else if (code === 3) {
|
|
102
|
+
process.stdout.write(`
|
|
103
|
+
`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
} else if (code >= 32) {
|
|
106
|
+
password += char;
|
|
107
|
+
process.stdout.write(`*`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
stdin.on(`data`, onData);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function printNextSteps(appName, options = {}) {
|
|
114
|
+
const { showInstall = false, showMigrate = false, showClaim = true } = options;
|
|
10
115
|
console.log(`Next steps:`);
|
|
11
116
|
if (appName !== `.`) {
|
|
12
117
|
console.log(` cd ${appName}`);
|
|
13
118
|
}
|
|
14
|
-
if (
|
|
119
|
+
if (showInstall) {
|
|
15
120
|
console.log(` pnpm install`);
|
|
121
|
+
}
|
|
122
|
+
if (showMigrate) {
|
|
16
123
|
console.log(` pnpm migrate`);
|
|
17
124
|
}
|
|
18
125
|
console.log(` pnpm dev`);
|
|
19
126
|
console.log(``);
|
|
20
127
|
console.log(`Commands:`);
|
|
21
128
|
console.log(` pnpm psql # Connect to database`);
|
|
22
|
-
|
|
129
|
+
if (showClaim) {
|
|
130
|
+
console.log(` pnpm claim # Claim cloud resources`);
|
|
131
|
+
}
|
|
23
132
|
console.log(` pnpm deploy:netlify # Deploy to Netlify`);
|
|
24
133
|
console.log(``);
|
|
25
134
|
console.log(`Tutorial: https://electric-sql.com/docs`);
|
|
26
135
|
}
|
|
27
136
|
async function main() {
|
|
28
137
|
const args = process.argv.slice(2);
|
|
29
|
-
|
|
30
|
-
console.error(`Usage: npx @electric-sql/start <app-name>`);
|
|
31
|
-
console.error(
|
|
32
|
-
` npx @electric-sql/start . (configure current directory)`
|
|
33
|
-
);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
const appName = args[0];
|
|
138
|
+
const { appName, sourceId, secret, databaseUrl } = parseArgs(args);
|
|
37
139
|
if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {
|
|
38
140
|
console.error(
|
|
39
141
|
`App name must contain only letters, numbers, hyphens, and underscores`
|
|
@@ -46,7 +148,37 @@ async function main() {
|
|
|
46
148
|
console.log(`Creating app: ${appName}`);
|
|
47
149
|
}
|
|
48
150
|
try {
|
|
49
|
-
|
|
151
|
+
let credentials;
|
|
152
|
+
let userProvidedCredentials = false;
|
|
153
|
+
if (sourceId) {
|
|
154
|
+
let finalSecret = secret;
|
|
155
|
+
if (!finalSecret) {
|
|
156
|
+
finalSecret = await promptPassword(
|
|
157
|
+
`Enter secret for source ${sourceId}: `
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!finalSecret.trim()) {
|
|
161
|
+
console.error(`Error: Secret cannot be empty`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
let finalDatabaseUrl = databaseUrl;
|
|
165
|
+
if (!finalDatabaseUrl) {
|
|
166
|
+
finalDatabaseUrl = await prompt(`Enter DATABASE_URL: `);
|
|
167
|
+
}
|
|
168
|
+
if (!finalDatabaseUrl.trim()) {
|
|
169
|
+
console.error(`Error: DATABASE_URL cannot be empty`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
credentials = {
|
|
173
|
+
source_id: sourceId,
|
|
174
|
+
secret: finalSecret.trim(),
|
|
175
|
+
DATABASE_URL: finalDatabaseUrl.trim()
|
|
176
|
+
};
|
|
177
|
+
userProvidedCredentials = true;
|
|
178
|
+
console.log(`Using provided credentials...`);
|
|
179
|
+
} else {
|
|
180
|
+
credentials = await provisionElectricResources();
|
|
181
|
+
}
|
|
50
182
|
console.log(`Setting up template...`);
|
|
51
183
|
await setupTemplate(appName, credentials);
|
|
52
184
|
console.log(`Installing dependencies...`);
|
|
@@ -57,22 +189,34 @@ async function main() {
|
|
|
57
189
|
});
|
|
58
190
|
} catch (_error) {
|
|
59
191
|
console.log(`Failed to install dependencies`);
|
|
60
|
-
printNextSteps(appName,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
execSync(`pnpm migrate`, {
|
|
66
|
-
stdio: `inherit`,
|
|
67
|
-
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
192
|
+
printNextSteps(appName, {
|
|
193
|
+
showInstall: true,
|
|
194
|
+
showMigrate: true,
|
|
195
|
+
showClaim: !userProvidedCredentials
|
|
68
196
|
});
|
|
69
|
-
} catch (_error) {
|
|
70
|
-
console.log(`Failed to apply migrations`);
|
|
71
|
-
printNextSteps(appName, true);
|
|
72
197
|
process.exit(1);
|
|
73
198
|
}
|
|
199
|
+
if (!userProvidedCredentials) {
|
|
200
|
+
console.log(`Running migrations...`);
|
|
201
|
+
try {
|
|
202
|
+
execSync(`pnpm migrate`, {
|
|
203
|
+
stdio: `inherit`,
|
|
204
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
205
|
+
});
|
|
206
|
+
} catch (_error) {
|
|
207
|
+
console.log(`Failed to apply migrations`);
|
|
208
|
+
printNextSteps(appName, {
|
|
209
|
+
showMigrate: true,
|
|
210
|
+
showClaim: true
|
|
211
|
+
});
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
74
215
|
console.log(`Setup complete`);
|
|
75
|
-
printNextSteps(appName
|
|
216
|
+
printNextSteps(appName, {
|
|
217
|
+
showMigrate: userProvidedCredentials,
|
|
218
|
+
showClaim: !userProvidedCredentials
|
|
219
|
+
});
|
|
76
220
|
} catch (error) {
|
|
77
221
|
console.error(
|
|
78
222
|
`Setup failed:`,
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execSync } from 'child_process'\nimport { provisionElectricResources } from './electric-api.js'\nimport { setupTemplate } from './template-setup.js'\nimport { join } from 'path'\n\nfunction printNextSteps(appName: string, fullSetup: boolean = false) {\n console.log(`Next steps:`)\n if (appName !== `.`) {\n console.log(` cd ${appName}`)\n }\n\n if (fullSetup) {\n console.log(` pnpm install`)\n console.log(` pnpm migrate`)\n }\n\n console.log(` pnpm dev`)\n console.log(``)\n console.log(`Commands:`)\n console.log(` pnpm psql # Connect to database`)\n console.log(` pnpm claim # Claim cloud resources`)\n console.log(` pnpm deploy:netlify # Deploy to Netlify`)\n console.log(``)\n console.log(`Tutorial: https://electric-sql.com/docs`)\n}\n\nasync function main() {\n const args = process.argv.slice(2)\n\n if (args.length === 0) {\n console.error(`Usage: npx @electric-sql/start <app-name>`)\n console.error(\n ` npx @electric-sql/start . (configure current directory)`\n )\n\n process.exit(1)\n }\n\n const appName = args[0]\n\n // Validate app name (skip validation for \".\" which means current directory)\n if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {\n console.error(\n `App name must contain only letters, numbers, hyphens, and underscores`\n )\n\n process.exit(1)\n }\n\n if (appName === `.`) {\n console.log(`Configuring current directory...`)\n } else {\n console.log(`Creating app: ${appName}`)\n }\n\n try {\n const credentials = await provisionElectricResources()\n\n // Step 2: Setup TanStack Start template\n console.log(`Setting up template...`)\n await setupTemplate(appName, credentials)\n\n console.log(`Installing dependencies...`)\n try {\n execSync(`pnpm install`, {\n stdio: `inherit`,\n cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),\n })\n } catch (_error) {\n console.log(`Failed to install dependencies`)\n printNextSteps(appName, true)\n process.exit(1)\n }\n\n console.log(`Running migrations...`)\n try {\n execSync(`pnpm migrate`, {\n stdio: `inherit`,\n cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),\n })\n } catch (_error) {\n console.log(`Failed to apply migrations`)\n printNextSteps(appName, true)\n process.exit(1)\n }\n\n // Step 3: Display completion message\n console.log(`Setup complete`)\n printNextSteps(appName)\n } catch (error) {\n console.error(\n `Setup failed:`,\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n\nexport { main }\n","import { main } from './cli.js'\n\nmain().catch((error) => {\n console.error(`Unexpected error:`, error)\n process.exit(1)\n})\n"],"mappings":";;;;;;AAEA,SAAS,gBAAgB;AAGzB,SAAS,YAAY;AAErB,SAAS,eAAe,SAAiB,YAAqB,OAAO;AACnE,UAAQ,IAAI,aAAa;AACzB,MAAI,YAAY,KAAK;AACnB,YAAQ,IAAI,QAAQ,OAAO,EAAE;AAAA,EAC/B;AAEA,MAAI,WAAW;AACb,YAAQ,IAAI,gBAAgB;AAC5B,YAAQ,IAAI,gBAAgB;AAAA,EAC9B;AAEA,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,WAAW;AACvB,UAAQ,IAAI,+CAA+C;AAC3D,UAAQ,IAAI,iDAAiD;AAC7D,UAAQ,IAAI,6CAA6C;AACzD,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,yCAAyC;AACvD;AAEA,eAAe,OAAO;AACpB,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAEjC,MAAI,KAAK,WAAW,GAAG;AACrB,YAAQ,MAAM,2CAA2C;AACzD,YAAQ;AAAA,MACN;AAAA,IACF;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,UAAU,KAAK,CAAC;AAGtB,MAAI,YAAY,OAAO,CAAC,mBAAmB,KAAK,OAAO,GAAG;AACxD,YAAQ;AAAA,MACN;AAAA,IACF;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,YAAY,KAAK;AACnB,YAAQ,IAAI,kCAAkC;AAAA,EAChD,OAAO;AACL,YAAQ,IAAI,iBAAiB,OAAO,EAAE;AAAA,EACxC;AAEA,MAAI;AACF,UAAM,cAAc,MAAM,2BAA2B;AAGrD,YAAQ,IAAI,wBAAwB;AACpC,UAAM,cAAc,SAAS,WAAW;AAExC,YAAQ,IAAI,4BAA4B;AACxC,QAAI;AACF,eAAS,gBAAgB;AAAA,QACvB,OAAO;AAAA,QACP,KAAK,YAAY,MAAM,QAAQ,IAAI,IAAI,KAAK,QAAQ,IAAI,GAAG,OAAO;AAAA,MACpE,CAAC;AAAA,IACH,SAAS,QAAQ;AACf,cAAQ,IAAI,gCAAgC;AAC5C,qBAAe,SAAS,IAAI;AAC5B,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,YAAQ,IAAI,uBAAuB;AACnC,QAAI;AACF,eAAS,gBAAgB;AAAA,QACvB,OAAO;AAAA,QACP,KAAK,YAAY,MAAM,QAAQ,IAAI,IAAI,KAAK,QAAQ,IAAI,GAAG,OAAO;AAAA,MACpE,CAAC;AAAA,IACH,SAAS,QAAQ;AACf,cAAQ,IAAI,4BAA4B;AACxC,qBAAe,SAAS,IAAI;AAC5B,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,YAAQ,IAAI,gBAAgB;AAC5B,mBAAe,OAAO;AAAA,EACxB,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;;;AChGA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,UAAQ,MAAM,qBAAqB,KAAK;AACxC,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execSync } from 'child_process'\nimport { createInterface } from 'readline'\nimport { provisionElectricResources } from './electric-api.js'\nimport { setupTemplate } from './template-setup.js'\nimport { join } from 'path'\n\ninterface ParsedArgs {\n appName: string\n sourceId?: string\n secret?: string\n databaseUrl?: string\n}\n\nfunction parseArgs(args: string[]): ParsedArgs {\n const positionalArgs: string[] = []\n let sourceId: string | undefined\n let secret: string | undefined\n let databaseUrl: string | undefined\n\n for (let i = 0; i < args.length; i++) {\n if (args[i] === `--source`) {\n sourceId = args[i + 1]\n if (!sourceId || sourceId.startsWith(`-`)) {\n console.error(`Error: --source requires a source ID value`)\n process.exit(1)\n }\n i++ // Skip the value\n } else if (args[i] === `--secret`) {\n secret = args[i + 1]\n if (!secret || secret.startsWith(`-`)) {\n console.error(`Error: --secret requires a value`)\n process.exit(1)\n }\n i++ // Skip the value\n } else if (args[i] === `--database-url`) {\n databaseUrl = args[i + 1]\n if (!databaseUrl || databaseUrl.startsWith(`-`)) {\n console.error(`Error: --database-url requires a value`)\n process.exit(1)\n }\n i++ // Skip the value\n } else if (!args[i].startsWith(`-`)) {\n positionalArgs.push(args[i])\n }\n }\n\n if (positionalArgs.length === 0) {\n console.error(\n `Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`\n )\n console.error(\n ` npx @electric-sql/start . (configure current directory)`\n )\n process.exit(1)\n }\n\n if (positionalArgs.length > 1) {\n console.error(\n `Error: Expected only one app name, but received multiple: ${positionalArgs.join(`, `)}`\n )\n console.error(\n `Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`\n )\n process.exit(1)\n }\n\n return { appName: positionalArgs[0], sourceId, secret, databaseUrl }\n}\n\nfunction prompt(question: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n })\n\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close()\n resolve(answer)\n })\n })\n}\n\nfunction promptPassword(question: string): Promise<string> {\n const stdin = process.stdin\n\n // Fall back to regular readline-based prompt if not a TTY (e.g., piped input, CI)\n if (!stdin.isTTY) {\n return prompt(question)\n }\n\n return new Promise((resolve) => {\n process.stdout.write(question)\n\n const wasRaw = stdin.isRaw\n stdin.setRawMode(true)\n stdin.resume()\n stdin.setEncoding(`utf8`)\n\n let password = ``\n\n const onData = (char: string) => {\n const code = char.charCodeAt(0)\n\n if (char === `\\r` || char === `\\n` || code === 13) {\n // Enter pressed\n stdin.setRawMode(wasRaw ?? false)\n stdin.removeListener(`data`, onData)\n stdin.pause()\n process.stdout.write(`\\n`)\n resolve(password)\n } else if (code === 127 || code === 8) {\n // Backspace\n if (password.length > 0) {\n password = password.slice(0, -1)\n process.stdout.write(`\\b \\b`)\n }\n } else if (code === 3) {\n // Ctrl+C\n process.stdout.write(`\\n`)\n process.exit(1)\n } else if (code >= 32) {\n // Printable character\n password += char\n process.stdout.write(`*`)\n }\n }\n\n stdin.on(`data`, onData)\n })\n}\n\ninterface NextStepsOptions {\n showInstall?: boolean\n showMigrate?: boolean\n showClaim?: boolean\n}\n\nfunction printNextSteps(appName: string, options: NextStepsOptions = {}) {\n const { showInstall = false, showMigrate = false, showClaim = true } = options\n\n console.log(`Next steps:`)\n if (appName !== `.`) {\n console.log(` cd ${appName}`)\n }\n\n if (showInstall) {\n console.log(` pnpm install`)\n }\n\n if (showMigrate) {\n console.log(` pnpm migrate`)\n }\n\n console.log(` pnpm dev`)\n console.log(``)\n console.log(`Commands:`)\n console.log(` pnpm psql # Connect to database`)\n if (showClaim) {\n console.log(` pnpm claim # Claim cloud resources`)\n }\n console.log(` pnpm deploy:netlify # Deploy to Netlify`)\n console.log(``)\n console.log(`Tutorial: https://electric-sql.com/docs`)\n}\n\nasync function main() {\n const args = process.argv.slice(2)\n const { appName, sourceId, secret, databaseUrl } = parseArgs(args)\n\n // Validate app name (skip validation for \".\" which means current directory)\n if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {\n console.error(\n `App name must contain only letters, numbers, hyphens, and underscores`\n )\n\n process.exit(1)\n }\n\n if (appName === `.`) {\n console.log(`Configuring current directory...`)\n } else {\n console.log(`Creating app: ${appName}`)\n }\n\n try {\n let credentials: {\n source_id: string\n secret: string\n DATABASE_URL: string\n claimId?: string\n }\n let userProvidedCredentials = false\n\n if (sourceId) {\n // User provided source ID, get secret and DATABASE_URL from CLI params or prompt\n let finalSecret = secret\n if (!finalSecret) {\n finalSecret = await promptPassword(\n `Enter secret for source ${sourceId}: `\n )\n }\n if (!finalSecret.trim()) {\n console.error(`Error: Secret cannot be empty`)\n process.exit(1)\n }\n\n let finalDatabaseUrl = databaseUrl\n if (!finalDatabaseUrl) {\n finalDatabaseUrl = await prompt(`Enter DATABASE_URL: `)\n }\n if (!finalDatabaseUrl.trim()) {\n console.error(`Error: DATABASE_URL cannot be empty`)\n process.exit(1)\n }\n\n credentials = {\n source_id: sourceId,\n secret: finalSecret.trim(),\n DATABASE_URL: finalDatabaseUrl.trim(),\n }\n userProvidedCredentials = true\n console.log(`Using provided credentials...`)\n } else {\n credentials = await provisionElectricResources()\n }\n\n // Step 2: Setup TanStack Start template\n console.log(`Setting up template...`)\n await setupTemplate(appName, credentials)\n\n console.log(`Installing dependencies...`)\n try {\n execSync(`pnpm install`, {\n stdio: `inherit`,\n cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),\n })\n } catch (_error) {\n console.log(`Failed to install dependencies`)\n printNextSteps(appName, {\n showInstall: true,\n showMigrate: true,\n showClaim: !userProvidedCredentials,\n })\n process.exit(1)\n }\n\n // Skip migrations if user provided credentials (they may have their own DB setup)\n if (!userProvidedCredentials) {\n console.log(`Running migrations...`)\n try {\n execSync(`pnpm migrate`, {\n stdio: `inherit`,\n cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),\n })\n } catch (_error) {\n console.log(`Failed to apply migrations`)\n printNextSteps(appName, {\n showMigrate: true,\n showClaim: true,\n })\n process.exit(1)\n }\n }\n\n // Step 3: Display completion message\n console.log(`Setup complete`)\n printNextSteps(appName, {\n showMigrate: userProvidedCredentials,\n showClaim: !userProvidedCredentials,\n })\n } catch (error) {\n console.error(\n `Setup failed:`,\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n\nexport { main }\n","import { main } from './cli.js'\n\nmain().catch((error) => {\n console.error(`Unexpected error:`, error)\n process.exit(1)\n})\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
1
3
|
// src/electric-api.ts
|
|
2
4
|
var DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api`;
|
|
3
5
|
var DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud`;
|
|
@@ -179,11 +181,14 @@ BETTER_AUTH_SECRET=${betterAuthSecret}
|
|
|
179
181
|
const packageJsonPath = join(appPath, `package.json`);
|
|
180
182
|
if (existsSync(packageJsonPath)) {
|
|
181
183
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`));
|
|
182
|
-
|
|
184
|
+
const scripts = {
|
|
183
185
|
...packageJson.scripts,
|
|
184
|
-
claim: `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`,
|
|
185
186
|
"deploy:netlify": `NODE_ENV=production NITRO_PRESET=netlify pnpm build && NODE_ENV=production npx netlify deploy --no-build --prod --dir=dist --functions=.netlify/functions-internal && npx netlify env:import .env`
|
|
186
187
|
};
|
|
188
|
+
if (credentials.claimId) {
|
|
189
|
+
scripts.claim = `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`;
|
|
190
|
+
}
|
|
191
|
+
packageJson.scripts = scripts;
|
|
187
192
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
188
193
|
}
|
|
189
194
|
console.log(`Template setup complete`);
|
|
@@ -205,4 +210,4 @@ export {
|
|
|
205
210
|
claimResources,
|
|
206
211
|
setupTemplate
|
|
207
212
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
213
|
+
//# sourceMappingURL=chunk-RTAT4KWN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/electric-api.ts","../src/template-setup.ts"],"sourcesContent":["// Using native fetch (Node.js 18+)\n\nexport interface ElectricCredentials {\n source_id: string\n secret: string\n DATABASE_URL: string\n}\n\nexport interface ClaimableSourceResponse {\n claimId: string\n}\n\ninterface ClaimableSourceStatus {\n state: `pending` | `ready` | `failed`\n source: {\n source_id: string\n secret: string\n }\n connection_uri: string\n claim_link?: string\n project_id?: string\n error: string | null\n}\n\nexport const DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api`\nexport const DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud`\nexport const DEFAULT_ELECTRIC_DASHBOARD_URL = `https://dashboard.electric-sql.cloud`\n\nexport function getElectricApiBase(): string {\n return process.env.ELECTRIC_API_BASE_URL ?? DEFAULT_ELECTRIC_API_BASE\n}\n\nexport function getElectricUrl(): string {\n return process.env.ELECTRIC_URL ?? DEFAULT_ELECTRIC_URL\n}\n\nexport function getElectricDashboardUrl(): string {\n return process.env.ELECTRIC_DASHBOARD_URL ?? DEFAULT_ELECTRIC_DASHBOARD_URL\n}\n\nconst POLL_INTERVAL_MS = 1000 // Poll every 1 second\nconst MAX_POLL_ATTEMPTS = 60 // Max 60 seconds\n\nasync function pollClaimableSource(\n claimId: string,\n maxAttempts: number = MAX_POLL_ATTEMPTS\n): Promise<ClaimableSourceStatus> {\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const response = await fetch(\n `${getElectricApiBase()}/public/v1/claimable-sources/${claimId}`,\n {\n method: `GET`,\n headers: {\n 'User-Agent': `@electric-sql/start`,\n },\n }\n )\n\n // Handle 404 as \"still being provisioned\" - continue polling\n if (response.status === 404) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))\n continue\n }\n\n // For other non-OK responses, throw an error\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const status = (await response.json()) as ClaimableSourceStatus\n\n if (status.state === `ready`) {\n return status\n }\n\n if (status.state === `failed` || status.error) {\n throw new Error(\n `Resource provisioning failed${status.error ? `: ${status.error}` : ``}`\n )\n }\n\n // Wait before polling again\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))\n }\n\n throw new Error(\n `Timeout waiting for resources to be provisioned after ${maxAttempts} attempts`\n )\n}\n\nexport async function provisionElectricResources(): Promise<\n ElectricCredentials & ClaimableSourceResponse\n> {\n console.log(`Provisioning resources...`)\n try {\n // Step 1: POST to create claimable source and get claimId\n const response = await fetch(\n `${getElectricApiBase()}/public/v1/claimable-sources`,\n {\n method: `POST`,\n headers: {\n 'Content-Type': `application/json`,\n 'User-Agent': `@electric-sql/start`,\n },\n body: JSON.stringify({}),\n }\n )\n\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const { claimId } = (await response.json()) as ClaimableSourceResponse\n\n if (!claimId) {\n throw new Error(`Invalid response from Electric API - missing claimId`)\n }\n\n // Step 2: Poll until state === 'ready'\n const status = await pollClaimableSource(claimId)\n\n // Step 3: Extract and validate credentials\n if (\n !status.source?.source_id ||\n !status.source?.secret ||\n !status.connection_uri\n ) {\n throw new Error(\n `Invalid response from Electric API - missing required credentials`\n )\n }\n\n return {\n source_id: status.source.source_id,\n secret: status.source.secret,\n DATABASE_URL: status.connection_uri,\n claimId,\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Failed to provision Electric resources: ${error.message}`\n )\n }\n throw new Error(`Failed to provision Electric resources: Unknown error`)\n }\n}\n\nexport async function claimResources(\n sourceId: string,\n secret: string\n): Promise<{ claimUrl: string }> {\n try {\n const response = await fetch(`${getElectricApiBase()}/v1/claim`, {\n method: `POST`,\n headers: {\n 'Content-Type': `application/json`,\n Authorization: `Bearer ${secret}`,\n 'User-Agent': `@electric-sql/start`,\n },\n body: JSON.stringify({\n source_id: sourceId,\n }),\n })\n\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const result = (await response.json()) as { claimUrl: string }\n\n if (!result.claimUrl) {\n throw new Error(`Invalid response from Electric API - missing claim URL`)\n }\n\n return result\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to initiate resource claim: ${error.message}`)\n }\n throw new Error(`Failed to initiate resource claim: Unknown error`)\n }\n}\n","import { execSync } from 'child_process'\nimport { randomBytes } from 'crypto'\nimport { writeFileSync, readFileSync, existsSync } from 'fs'\nimport { join } from 'path'\nimport {\n ElectricCredentials,\n getElectricUrl,\n getElectricDashboardUrl,\n} from './electric-api'\n\ninterface SetupCredentials extends ElectricCredentials {\n claimId?: string\n}\n\n/**\n * Generates a cryptographically secure random string for use as a secret\n * @param length - The length of the secret in bytes (will be hex encoded, so output is 2x length)\n * @returns A random hex string\n */\nfunction generateSecret(length: number = 32): string {\n return randomBytes(length).toString(`hex`)\n}\n\nexport async function setupTemplate(\n appName: string,\n credentials: SetupCredentials\n): Promise<void> {\n const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName)\n\n try {\n // Step 1: Pull TanStack Start template using gitpick (skip for current directory)\n if (appName !== `.`) {\n console.log(`Pulling template...`)\n execSync(\n `npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter ${appName}`,\n { stdio: `inherit` }\n )\n }\n\n // Step 2: Generate .env file with credentials\n console.log(`Configuring environment...`)\n const betterAuthSecret = generateSecret(32)\n const electricUrl = getElectricUrl()\n const envContent = `# Electric SQL Configuration\n# Generated by @electric-sql/start\n# DO NOT COMMIT THIS FILE\n\n# Database\nDATABASE_URL=${credentials.DATABASE_URL}\n\n# Electric Cloud\nELECTRIC_URL=${electricUrl}\nELECTRIC_SOURCE_ID=${credentials.source_id}\nELECTRIC_SECRET=${credentials.secret}\n\n# Authentication\nBETTER_AUTH_SECRET=${betterAuthSecret}\n`\n\n writeFileSync(join(appPath, `.env`), envContent)\n\n // Step 3: Ensure .gitignore includes .env\n console.log(`Updating .gitignore...`)\n const gitignorePath = join(appPath, `.gitignore`)\n let gitignoreContent = ``\n\n if (existsSync(gitignorePath)) {\n gitignoreContent = readFileSync(gitignorePath, `utf8`)\n }\n\n if (!gitignoreContent.includes(`.env`)) {\n gitignoreContent += `\\n# Environment variables\\n.env\\n.env.local\\n.env.*.local\\n`\n writeFileSync(gitignorePath, gitignoreContent)\n }\n\n console.log(`Adding Electric commands...`)\n const packageJsonPath = join(appPath, `package.json`)\n\n if (existsSync(packageJsonPath)) {\n const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`))\n\n // Add/update scripts for cloud mode and Electric commands\n const scripts: Record<string, string> = {\n ...packageJson.scripts,\n 'deploy:netlify': `NODE_ENV=production NITRO_PRESET=netlify pnpm build && NODE_ENV=production npx netlify deploy --no-build --prod --dir=dist --functions=.netlify/functions-internal && npx netlify env:import .env`,\n }\n\n // Only add claim script if claimId is present (i.e., resources were provisioned)\n if (credentials.claimId) {\n scripts.claim = `npx open-cli \"${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}\"`\n }\n\n packageJson.scripts = scripts\n\n writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))\n }\n\n console.log(`Template setup complete`)\n } catch (error) {\n throw new Error(\n `Template setup failed: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_ELECTRIC_API_BASE,
|
|
3
4
|
DEFAULT_ELECTRIC_DASHBOARD_URL,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
getElectricUrl,
|
|
9
10
|
provisionElectricResources,
|
|
10
11
|
setupTemplate
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-RTAT4KWN.js";
|
|
12
13
|
export {
|
|
13
14
|
DEFAULT_ELECTRIC_API_BASE,
|
|
14
15
|
DEFAULT_ELECTRIC_DASHBOARD_URL,
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,18 +1,156 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'child_process'
|
|
4
|
+
import { createInterface } from 'readline'
|
|
4
5
|
import { provisionElectricResources } from './electric-api.js'
|
|
5
6
|
import { setupTemplate } from './template-setup.js'
|
|
6
7
|
import { join } from 'path'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
interface ParsedArgs {
|
|
10
|
+
appName: string
|
|
11
|
+
sourceId?: string
|
|
12
|
+
secret?: string
|
|
13
|
+
databaseUrl?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArgs(args: string[]): ParsedArgs {
|
|
17
|
+
const positionalArgs: string[] = []
|
|
18
|
+
let sourceId: string | undefined
|
|
19
|
+
let secret: string | undefined
|
|
20
|
+
let databaseUrl: string | undefined
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
if (args[i] === `--source`) {
|
|
24
|
+
sourceId = args[i + 1]
|
|
25
|
+
if (!sourceId || sourceId.startsWith(`-`)) {
|
|
26
|
+
console.error(`Error: --source requires a source ID value`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
i++ // Skip the value
|
|
30
|
+
} else if (args[i] === `--secret`) {
|
|
31
|
+
secret = args[i + 1]
|
|
32
|
+
if (!secret || secret.startsWith(`-`)) {
|
|
33
|
+
console.error(`Error: --secret requires a value`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
i++ // Skip the value
|
|
37
|
+
} else if (args[i] === `--database-url`) {
|
|
38
|
+
databaseUrl = args[i + 1]
|
|
39
|
+
if (!databaseUrl || databaseUrl.startsWith(`-`)) {
|
|
40
|
+
console.error(`Error: --database-url requires a value`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
i++ // Skip the value
|
|
44
|
+
} else if (!args[i].startsWith(`-`)) {
|
|
45
|
+
positionalArgs.push(args[i])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (positionalArgs.length === 0) {
|
|
50
|
+
console.error(
|
|
51
|
+
`Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`
|
|
52
|
+
)
|
|
53
|
+
console.error(
|
|
54
|
+
` npx @electric-sql/start . (configure current directory)`
|
|
55
|
+
)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (positionalArgs.length > 1) {
|
|
60
|
+
console.error(
|
|
61
|
+
`Error: Expected only one app name, but received multiple: ${positionalArgs.join(`, `)}`
|
|
62
|
+
)
|
|
63
|
+
console.error(
|
|
64
|
+
`Usage: npx @electric-sql/start <app-name> [--source <source-id>] [--secret <secret>] [--database-url <url>]`
|
|
65
|
+
)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { appName: positionalArgs[0], sourceId, secret, databaseUrl }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function prompt(question: string): Promise<string> {
|
|
73
|
+
const rl = createInterface({
|
|
74
|
+
input: process.stdin,
|
|
75
|
+
output: process.stdout,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
rl.question(question, (answer) => {
|
|
80
|
+
rl.close()
|
|
81
|
+
resolve(answer)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function promptPassword(question: string): Promise<string> {
|
|
87
|
+
const stdin = process.stdin
|
|
88
|
+
|
|
89
|
+
// Fall back to regular readline-based prompt if not a TTY (e.g., piped input, CI)
|
|
90
|
+
if (!stdin.isTTY) {
|
|
91
|
+
return prompt(question)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
process.stdout.write(question)
|
|
96
|
+
|
|
97
|
+
const wasRaw = stdin.isRaw
|
|
98
|
+
stdin.setRawMode(true)
|
|
99
|
+
stdin.resume()
|
|
100
|
+
stdin.setEncoding(`utf8`)
|
|
101
|
+
|
|
102
|
+
let password = ``
|
|
103
|
+
|
|
104
|
+
const onData = (char: string) => {
|
|
105
|
+
const code = char.charCodeAt(0)
|
|
106
|
+
|
|
107
|
+
if (char === `\r` || char === `\n` || code === 13) {
|
|
108
|
+
// Enter pressed
|
|
109
|
+
stdin.setRawMode(wasRaw ?? false)
|
|
110
|
+
stdin.removeListener(`data`, onData)
|
|
111
|
+
stdin.pause()
|
|
112
|
+
process.stdout.write(`\n`)
|
|
113
|
+
resolve(password)
|
|
114
|
+
} else if (code === 127 || code === 8) {
|
|
115
|
+
// Backspace
|
|
116
|
+
if (password.length > 0) {
|
|
117
|
+
password = password.slice(0, -1)
|
|
118
|
+
process.stdout.write(`\b \b`)
|
|
119
|
+
}
|
|
120
|
+
} else if (code === 3) {
|
|
121
|
+
// Ctrl+C
|
|
122
|
+
process.stdout.write(`\n`)
|
|
123
|
+
process.exit(1)
|
|
124
|
+
} else if (code >= 32) {
|
|
125
|
+
// Printable character
|
|
126
|
+
password += char
|
|
127
|
+
process.stdout.write(`*`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stdin.on(`data`, onData)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface NextStepsOptions {
|
|
136
|
+
showInstall?: boolean
|
|
137
|
+
showMigrate?: boolean
|
|
138
|
+
showClaim?: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printNextSteps(appName: string, options: NextStepsOptions = {}) {
|
|
142
|
+
const { showInstall = false, showMigrate = false, showClaim = true } = options
|
|
143
|
+
|
|
9
144
|
console.log(`Next steps:`)
|
|
10
145
|
if (appName !== `.`) {
|
|
11
146
|
console.log(` cd ${appName}`)
|
|
12
147
|
}
|
|
13
148
|
|
|
14
|
-
if (
|
|
149
|
+
if (showInstall) {
|
|
15
150
|
console.log(` pnpm install`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (showMigrate) {
|
|
16
154
|
console.log(` pnpm migrate`)
|
|
17
155
|
}
|
|
18
156
|
|
|
@@ -20,7 +158,9 @@ function printNextSteps(appName: string, fullSetup: boolean = false) {
|
|
|
20
158
|
console.log(``)
|
|
21
159
|
console.log(`Commands:`)
|
|
22
160
|
console.log(` pnpm psql # Connect to database`)
|
|
23
|
-
|
|
161
|
+
if (showClaim) {
|
|
162
|
+
console.log(` pnpm claim # Claim cloud resources`)
|
|
163
|
+
}
|
|
24
164
|
console.log(` pnpm deploy:netlify # Deploy to Netlify`)
|
|
25
165
|
console.log(``)
|
|
26
166
|
console.log(`Tutorial: https://electric-sql.com/docs`)
|
|
@@ -28,17 +168,7 @@ function printNextSteps(appName: string, fullSetup: boolean = false) {
|
|
|
28
168
|
|
|
29
169
|
async function main() {
|
|
30
170
|
const args = process.argv.slice(2)
|
|
31
|
-
|
|
32
|
-
if (args.length === 0) {
|
|
33
|
-
console.error(`Usage: npx @electric-sql/start <app-name>`)
|
|
34
|
-
console.error(
|
|
35
|
-
` npx @electric-sql/start . (configure current directory)`
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
process.exit(1)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const appName = args[0]
|
|
171
|
+
const { appName, sourceId, secret, databaseUrl } = parseArgs(args)
|
|
42
172
|
|
|
43
173
|
// Validate app name (skip validation for "." which means current directory)
|
|
44
174
|
if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {
|
|
@@ -56,7 +186,46 @@ async function main() {
|
|
|
56
186
|
}
|
|
57
187
|
|
|
58
188
|
try {
|
|
59
|
-
|
|
189
|
+
let credentials: {
|
|
190
|
+
source_id: string
|
|
191
|
+
secret: string
|
|
192
|
+
DATABASE_URL: string
|
|
193
|
+
claimId?: string
|
|
194
|
+
}
|
|
195
|
+
let userProvidedCredentials = false
|
|
196
|
+
|
|
197
|
+
if (sourceId) {
|
|
198
|
+
// User provided source ID, get secret and DATABASE_URL from CLI params or prompt
|
|
199
|
+
let finalSecret = secret
|
|
200
|
+
if (!finalSecret) {
|
|
201
|
+
finalSecret = await promptPassword(
|
|
202
|
+
`Enter secret for source ${sourceId}: `
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
if (!finalSecret.trim()) {
|
|
206
|
+
console.error(`Error: Secret cannot be empty`)
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let finalDatabaseUrl = databaseUrl
|
|
211
|
+
if (!finalDatabaseUrl) {
|
|
212
|
+
finalDatabaseUrl = await prompt(`Enter DATABASE_URL: `)
|
|
213
|
+
}
|
|
214
|
+
if (!finalDatabaseUrl.trim()) {
|
|
215
|
+
console.error(`Error: DATABASE_URL cannot be empty`)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
credentials = {
|
|
220
|
+
source_id: sourceId,
|
|
221
|
+
secret: finalSecret.trim(),
|
|
222
|
+
DATABASE_URL: finalDatabaseUrl.trim(),
|
|
223
|
+
}
|
|
224
|
+
userProvidedCredentials = true
|
|
225
|
+
console.log(`Using provided credentials...`)
|
|
226
|
+
} else {
|
|
227
|
+
credentials = await provisionElectricResources()
|
|
228
|
+
}
|
|
60
229
|
|
|
61
230
|
// Step 2: Setup TanStack Start template
|
|
62
231
|
console.log(`Setting up template...`)
|
|
@@ -70,25 +239,38 @@ async function main() {
|
|
|
70
239
|
})
|
|
71
240
|
} catch (_error) {
|
|
72
241
|
console.log(`Failed to install dependencies`)
|
|
73
|
-
printNextSteps(appName,
|
|
242
|
+
printNextSteps(appName, {
|
|
243
|
+
showInstall: true,
|
|
244
|
+
showMigrate: true,
|
|
245
|
+
showClaim: !userProvidedCredentials,
|
|
246
|
+
})
|
|
74
247
|
process.exit(1)
|
|
75
248
|
}
|
|
76
249
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
250
|
+
// Skip migrations if user provided credentials (they may have their own DB setup)
|
|
251
|
+
if (!userProvidedCredentials) {
|
|
252
|
+
console.log(`Running migrations...`)
|
|
253
|
+
try {
|
|
254
|
+
execSync(`pnpm migrate`, {
|
|
255
|
+
stdio: `inherit`,
|
|
256
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),
|
|
257
|
+
})
|
|
258
|
+
} catch (_error) {
|
|
259
|
+
console.log(`Failed to apply migrations`)
|
|
260
|
+
printNextSteps(appName, {
|
|
261
|
+
showMigrate: true,
|
|
262
|
+
showClaim: true,
|
|
263
|
+
})
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
87
266
|
}
|
|
88
267
|
|
|
89
268
|
// Step 3: Display completion message
|
|
90
269
|
console.log(`Setup complete`)
|
|
91
|
-
printNextSteps(appName
|
|
270
|
+
printNextSteps(appName, {
|
|
271
|
+
showMigrate: userProvidedCredentials,
|
|
272
|
+
showClaim: !userProvidedCredentials,
|
|
273
|
+
})
|
|
92
274
|
} catch (error) {
|
|
93
275
|
console.error(
|
|
94
276
|
`Setup failed:`,
|
package/src/template-setup.ts
CHANGED
|
@@ -4,11 +4,14 @@ import { writeFileSync, readFileSync, existsSync } from 'fs'
|
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import {
|
|
6
6
|
ElectricCredentials,
|
|
7
|
-
ClaimableSourceResponse,
|
|
8
7
|
getElectricUrl,
|
|
9
8
|
getElectricDashboardUrl,
|
|
10
9
|
} from './electric-api'
|
|
11
10
|
|
|
11
|
+
interface SetupCredentials extends ElectricCredentials {
|
|
12
|
+
claimId?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Generates a cryptographically secure random string for use as a secret
|
|
14
17
|
* @param length - The length of the secret in bytes (will be hex encoded, so output is 2x length)
|
|
@@ -20,7 +23,7 @@ function generateSecret(length: number = 32): string {
|
|
|
20
23
|
|
|
21
24
|
export async function setupTemplate(
|
|
22
25
|
appName: string,
|
|
23
|
-
credentials:
|
|
26
|
+
credentials: SetupCredentials
|
|
24
27
|
): Promise<void> {
|
|
25
28
|
const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
26
29
|
|
|
@@ -77,12 +80,18 @@ BETTER_AUTH_SECRET=${betterAuthSecret}
|
|
|
77
80
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`))
|
|
78
81
|
|
|
79
82
|
// Add/update scripts for cloud mode and Electric commands
|
|
80
|
-
|
|
83
|
+
const scripts: Record<string, string> = {
|
|
81
84
|
...packageJson.scripts,
|
|
82
|
-
claim: `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`,
|
|
83
85
|
'deploy:netlify': `NODE_ENV=production NITRO_PRESET=netlify pnpm build && NODE_ENV=production npx netlify deploy --no-build --prod --dir=dist --functions=.netlify/functions-internal && npx netlify env:import .env`,
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
// Only add claim script if claimId is present (i.e., resources were provisioned)
|
|
89
|
+
if (credentials.claimId) {
|
|
90
|
+
scripts.claim = `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
packageJson.scripts = scripts
|
|
94
|
+
|
|
86
95
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
87
96
|
}
|
|
88
97
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/electric-api.ts","../src/template-setup.ts"],"sourcesContent":["// Using native fetch (Node.js 18+)\n\nexport interface ElectricCredentials {\n source_id: string\n secret: string\n DATABASE_URL: string\n}\n\nexport interface ClaimableSourceResponse {\n claimId: string\n}\n\ninterface ClaimableSourceStatus {\n state: `pending` | `ready` | `failed`\n source: {\n source_id: string\n secret: string\n }\n connection_uri: string\n claim_link?: string\n project_id?: string\n error: string | null\n}\n\nexport const DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api`\nexport const DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud`\nexport const DEFAULT_ELECTRIC_DASHBOARD_URL = `https://dashboard.electric-sql.cloud`\n\nexport function getElectricApiBase(): string {\n return process.env.ELECTRIC_API_BASE_URL ?? DEFAULT_ELECTRIC_API_BASE\n}\n\nexport function getElectricUrl(): string {\n return process.env.ELECTRIC_URL ?? DEFAULT_ELECTRIC_URL\n}\n\nexport function getElectricDashboardUrl(): string {\n return process.env.ELECTRIC_DASHBOARD_URL ?? DEFAULT_ELECTRIC_DASHBOARD_URL\n}\n\nconst POLL_INTERVAL_MS = 1000 // Poll every 1 second\nconst MAX_POLL_ATTEMPTS = 60 // Max 60 seconds\n\nasync function pollClaimableSource(\n claimId: string,\n maxAttempts: number = MAX_POLL_ATTEMPTS\n): Promise<ClaimableSourceStatus> {\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const response = await fetch(\n `${getElectricApiBase()}/public/v1/claimable-sources/${claimId}`,\n {\n method: `GET`,\n headers: {\n 'User-Agent': `@electric-sql/start`,\n },\n }\n )\n\n // Handle 404 as \"still being provisioned\" - continue polling\n if (response.status === 404) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))\n continue\n }\n\n // For other non-OK responses, throw an error\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const status = (await response.json()) as ClaimableSourceStatus\n\n if (status.state === `ready`) {\n return status\n }\n\n if (status.state === `failed` || status.error) {\n throw new Error(\n `Resource provisioning failed${status.error ? `: ${status.error}` : ``}`\n )\n }\n\n // Wait before polling again\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))\n }\n\n throw new Error(\n `Timeout waiting for resources to be provisioned after ${maxAttempts} attempts`\n )\n}\n\nexport async function provisionElectricResources(): Promise<\n ElectricCredentials & ClaimableSourceResponse\n> {\n console.log(`Provisioning resources...`)\n try {\n // Step 1: POST to create claimable source and get claimId\n const response = await fetch(\n `${getElectricApiBase()}/public/v1/claimable-sources`,\n {\n method: `POST`,\n headers: {\n 'Content-Type': `application/json`,\n 'User-Agent': `@electric-sql/start`,\n },\n body: JSON.stringify({}),\n }\n )\n\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const { claimId } = (await response.json()) as ClaimableSourceResponse\n\n if (!claimId) {\n throw new Error(`Invalid response from Electric API - missing claimId`)\n }\n\n // Step 2: Poll until state === 'ready'\n const status = await pollClaimableSource(claimId)\n\n // Step 3: Extract and validate credentials\n if (\n !status.source?.source_id ||\n !status.source?.secret ||\n !status.connection_uri\n ) {\n throw new Error(\n `Invalid response from Electric API - missing required credentials`\n )\n }\n\n return {\n source_id: status.source.source_id,\n secret: status.source.secret,\n DATABASE_URL: status.connection_uri,\n claimId,\n }\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Failed to provision Electric resources: ${error.message}`\n )\n }\n throw new Error(`Failed to provision Electric resources: Unknown error`)\n }\n}\n\nexport async function claimResources(\n sourceId: string,\n secret: string\n): Promise<{ claimUrl: string }> {\n try {\n const response = await fetch(`${getElectricApiBase()}/v1/claim`, {\n method: `POST`,\n headers: {\n 'Content-Type': `application/json`,\n Authorization: `Bearer ${secret}`,\n 'User-Agent': `@electric-sql/start`,\n },\n body: JSON.stringify({\n source_id: sourceId,\n }),\n })\n\n if (!response.ok) {\n throw new Error(\n `Electric API error: ${response.status} ${response.statusText}`\n )\n }\n\n const result = (await response.json()) as { claimUrl: string }\n\n if (!result.claimUrl) {\n throw new Error(`Invalid response from Electric API - missing claim URL`)\n }\n\n return result\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to initiate resource claim: ${error.message}`)\n }\n throw new Error(`Failed to initiate resource claim: Unknown error`)\n }\n}\n","import { execSync } from 'child_process'\nimport { randomBytes } from 'crypto'\nimport { writeFileSync, readFileSync, existsSync } from 'fs'\nimport { join } from 'path'\nimport {\n ElectricCredentials,\n ClaimableSourceResponse,\n getElectricUrl,\n getElectricDashboardUrl,\n} from './electric-api'\n\n/**\n * Generates a cryptographically secure random string for use as a secret\n * @param length - The length of the secret in bytes (will be hex encoded, so output is 2x length)\n * @returns A random hex string\n */\nfunction generateSecret(length: number = 32): string {\n return randomBytes(length).toString(`hex`)\n}\n\nexport async function setupTemplate(\n appName: string,\n credentials: ElectricCredentials & ClaimableSourceResponse\n): Promise<void> {\n const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName)\n\n try {\n // Step 1: Pull TanStack Start template using gitpick (skip for current directory)\n if (appName !== `.`) {\n console.log(`Pulling template...`)\n execSync(\n `npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter ${appName}`,\n { stdio: `inherit` }\n )\n }\n\n // Step 2: Generate .env file with credentials\n console.log(`Configuring environment...`)\n const betterAuthSecret = generateSecret(32)\n const electricUrl = getElectricUrl()\n const envContent = `# Electric SQL Configuration\n# Generated by @electric-sql/start\n# DO NOT COMMIT THIS FILE\n\n# Database\nDATABASE_URL=${credentials.DATABASE_URL}\n\n# Electric Cloud\nELECTRIC_URL=${electricUrl}\nELECTRIC_SOURCE_ID=${credentials.source_id}\nELECTRIC_SECRET=${credentials.secret}\n\n# Authentication\nBETTER_AUTH_SECRET=${betterAuthSecret}\n`\n\n writeFileSync(join(appPath, `.env`), envContent)\n\n // Step 3: Ensure .gitignore includes .env\n console.log(`Updating .gitignore...`)\n const gitignorePath = join(appPath, `.gitignore`)\n let gitignoreContent = ``\n\n if (existsSync(gitignorePath)) {\n gitignoreContent = readFileSync(gitignorePath, `utf8`)\n }\n\n if (!gitignoreContent.includes(`.env`)) {\n gitignoreContent += `\\n# Environment variables\\n.env\\n.env.local\\n.env.*.local\\n`\n writeFileSync(gitignorePath, gitignoreContent)\n }\n\n console.log(`Adding Electric commands...`)\n const packageJsonPath = join(appPath, `package.json`)\n\n if (existsSync(packageJsonPath)) {\n const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`))\n\n // Add/update scripts for cloud mode and Electric commands\n packageJson.scripts = {\n ...packageJson.scripts,\n claim: `npx open-cli \"${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}\"`,\n 'deploy:netlify': `NODE_ENV=production NITRO_PRESET=netlify pnpm build && NODE_ENV=production npx netlify deploy --no-build --prod --dir=dist --functions=.netlify/functions-internal && npx netlify env:import .env`,\n }\n\n writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))\n }\n\n console.log(`Template setup complete`)\n } catch (error) {\n throw new Error(\n `Template setup failed: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n"],"mappings":"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","names":[]}
|