@electric-sql/start 1.0.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 +79 -0
- package/dist/chunk-DWNDVGD3.js +208 -0
- package/dist/chunk-DWNDVGD3.js.map +1 -0
- package/dist/cli.js +94 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/src/cli.ts +108 -0
- package/src/electric-api.ts +189 -0
- package/src/index.ts +2 -0
- package/src/template-setup.ts +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @electric-sql/start
|
|
2
|
+
|
|
3
|
+
CLI package for the [ElectricSQL Quickstart](https://electric-sql.com/docs/quickstart).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Create a new app using [Electric](https://electric-sql.com/product/electric) with [TanStack DB](https://tanstack.com/db), based on the [examples/tanstack-db-web-starter](https://github.com/electric-sql/electric/tree/main/examples/tanstack-db-web-starter) [TanStack Start](http://tanstack.com/start) template app:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpx @electric-sql/start my-electric-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This command will:
|
|
14
|
+
|
|
15
|
+
1. pull in the template app using gitpick
|
|
16
|
+
2. provision cloud resources
|
|
17
|
+
- a Postgres database using Neon
|
|
18
|
+
- an Electric sync service using Electric Cloud
|
|
19
|
+
- fetch their access credentials
|
|
20
|
+
3. configure the local `.env` to use the credentials
|
|
21
|
+
4. add `psql`, `claim` and `deploy` commands to the package.json
|
|
22
|
+
- also using the generated credentials
|
|
23
|
+
|
|
24
|
+
## Environment Variables
|
|
25
|
+
|
|
26
|
+
The CLI automatically generates these environment variables:
|
|
27
|
+
|
|
28
|
+
- `DATABASE_URL` - PostgreSQL connection string
|
|
29
|
+
- `ELECTRIC_SECRET` - Electric Cloud authentication secret
|
|
30
|
+
- `ELECTRIC_SOURCE_ID` - Electric sync service identifier
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm dev # Start development server
|
|
36
|
+
pnpm psql # Connect to PostgreSQL database
|
|
37
|
+
pnpm claim # Claim temporary resources
|
|
38
|
+
pnpm deploy # Deploy to Netlify
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `pnpm psql`
|
|
42
|
+
|
|
43
|
+
Connect directly to your PostgreSQL database using the configured `DATABASE_URL`:
|
|
44
|
+
|
|
45
|
+
### `pnpm claim`
|
|
46
|
+
|
|
47
|
+
Claim temporary resources to move them to your permanent Electric Cloud and Neon accounts.
|
|
48
|
+
|
|
49
|
+
### `pnpm deploy`
|
|
50
|
+
|
|
51
|
+
Deploy your app to Netlify with all environment variables configured.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
This package is part of the Electric monorepo. To work on it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# From the monorepo root
|
|
59
|
+
pnpm install # Install all workspace dependencies
|
|
60
|
+
pnpm build # Build all packages
|
|
61
|
+
|
|
62
|
+
# From packages/quickstart
|
|
63
|
+
pnpm build # Compile TypeScript
|
|
64
|
+
pnpm dev # Build and test locally
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Testing Against Different API Environments
|
|
68
|
+
|
|
69
|
+
The Electric API base URL can be configured via the `ELECTRIC_API_BASE_URL` environment variable. This is useful for testing against staging or development API servers.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Default (production)
|
|
73
|
+
pnpm test
|
|
74
|
+
|
|
75
|
+
# Against a custom API server
|
|
76
|
+
ELECTRIC_API_BASE_URL=https://api.staging.electric-sql.cloud pnpm test
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The default API base URL is `https://api.electric-sql.cloud`.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/electric-api.ts
|
|
2
|
+
var DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api`;
|
|
3
|
+
var DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud`;
|
|
4
|
+
var DEFAULT_ELECTRIC_DASHBOARD_URL = `https://dashboard.electric-sql.cloud`;
|
|
5
|
+
function getElectricApiBase() {
|
|
6
|
+
return process.env.ELECTRIC_API_BASE_URL ?? DEFAULT_ELECTRIC_API_BASE;
|
|
7
|
+
}
|
|
8
|
+
function getElectricUrl() {
|
|
9
|
+
return process.env.ELECTRIC_URL ?? DEFAULT_ELECTRIC_URL;
|
|
10
|
+
}
|
|
11
|
+
function getElectricDashboardUrl() {
|
|
12
|
+
return process.env.ELECTRIC_DASHBOARD_URL ?? DEFAULT_ELECTRIC_DASHBOARD_URL;
|
|
13
|
+
}
|
|
14
|
+
var POLL_INTERVAL_MS = 1e3;
|
|
15
|
+
var MAX_POLL_ATTEMPTS = 60;
|
|
16
|
+
async function pollClaimableSource(claimId, maxAttempts = MAX_POLL_ATTEMPTS) {
|
|
17
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
18
|
+
const response = await fetch(
|
|
19
|
+
`${getElectricApiBase()}/public/v1/claimable-sources/${claimId}`,
|
|
20
|
+
{
|
|
21
|
+
method: `GET`,
|
|
22
|
+
headers: {
|
|
23
|
+
"User-Agent": `@electric-sql/start`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const status = await response.json();
|
|
37
|
+
if (status.state === `ready`) {
|
|
38
|
+
return status;
|
|
39
|
+
}
|
|
40
|
+
if (status.state === `failed` || status.error) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Resource provisioning failed${status.error ? `: ${status.error}` : ``}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Timeout waiting for resources to be provisioned after ${maxAttempts} attempts`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
async function provisionElectricResources() {
|
|
52
|
+
console.log(`Provisioning resources...`);
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(
|
|
55
|
+
`${getElectricApiBase()}/public/v1/claimable-sources`,
|
|
56
|
+
{
|
|
57
|
+
method: `POST`,
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": `application/json`,
|
|
60
|
+
"User-Agent": `@electric-sql/start`
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({})
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const { claimId } = await response.json();
|
|
71
|
+
if (!claimId) {
|
|
72
|
+
throw new Error(`Invalid response from Electric API - missing claimId`);
|
|
73
|
+
}
|
|
74
|
+
const status = await pollClaimableSource(claimId);
|
|
75
|
+
if (!status.source?.source_id || !status.source?.secret || !status.connection_uri) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Invalid response from Electric API - missing required credentials`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
source_id: status.source.source_id,
|
|
82
|
+
secret: status.source.secret,
|
|
83
|
+
DATABASE_URL: status.connection_uri,
|
|
84
|
+
claimId
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Failed to provision Electric resources: ${error.message}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Failed to provision Electric resources: Unknown error`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function claimResources(sourceId, secret) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(`${getElectricApiBase()}/v1/claim`, {
|
|
98
|
+
method: `POST`,
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": `application/json`,
|
|
101
|
+
Authorization: `Bearer ${secret}`,
|
|
102
|
+
"User-Agent": `@electric-sql/start`
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
source_id: sourceId
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const result = await response.json();
|
|
114
|
+
if (!result.claimUrl) {
|
|
115
|
+
throw new Error(`Invalid response from Electric API - missing claim URL`);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof Error) {
|
|
120
|
+
throw new Error(`Failed to initiate resource claim: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Failed to initiate resource claim: Unknown error`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/template-setup.ts
|
|
127
|
+
import { execSync } from "child_process";
|
|
128
|
+
import { randomBytes } from "crypto";
|
|
129
|
+
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
130
|
+
import { join } from "path";
|
|
131
|
+
function generateSecret(length = 32) {
|
|
132
|
+
return randomBytes(length).toString(`hex`);
|
|
133
|
+
}
|
|
134
|
+
async function setupTemplate(appName, credentials) {
|
|
135
|
+
const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName);
|
|
136
|
+
try {
|
|
137
|
+
if (appName !== `.`) {
|
|
138
|
+
console.log(`Pulling template...`);
|
|
139
|
+
execSync(
|
|
140
|
+
`npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter ${appName}`,
|
|
141
|
+
{ stdio: `inherit` }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
console.log(`Configuring environment...`);
|
|
145
|
+
const betterAuthSecret = generateSecret(32);
|
|
146
|
+
const electricUrl = getElectricUrl();
|
|
147
|
+
const envContent = `# Electric SQL Configuration
|
|
148
|
+
# Generated by @electric-sql/start
|
|
149
|
+
# DO NOT COMMIT THIS FILE
|
|
150
|
+
|
|
151
|
+
# Database
|
|
152
|
+
DATABASE_URL=${credentials.DATABASE_URL}
|
|
153
|
+
|
|
154
|
+
# Electric Cloud
|
|
155
|
+
ELECTRIC_URL=${electricUrl}
|
|
156
|
+
ELECTRIC_SOURCE_ID=${credentials.source_id}
|
|
157
|
+
ELECTRIC_SECRET=${credentials.secret}
|
|
158
|
+
|
|
159
|
+
# Authentication
|
|
160
|
+
BETTER_AUTH_SECRET=${betterAuthSecret}
|
|
161
|
+
`;
|
|
162
|
+
writeFileSync(join(appPath, `.env`), envContent);
|
|
163
|
+
console.log(`Updating .gitignore...`);
|
|
164
|
+
const gitignorePath = join(appPath, `.gitignore`);
|
|
165
|
+
let gitignoreContent = ``;
|
|
166
|
+
if (existsSync(gitignorePath)) {
|
|
167
|
+
gitignoreContent = readFileSync(gitignorePath, `utf8`);
|
|
168
|
+
}
|
|
169
|
+
if (!gitignoreContent.includes(`.env`)) {
|
|
170
|
+
gitignoreContent += `
|
|
171
|
+
# Environment variables
|
|
172
|
+
.env
|
|
173
|
+
.env.local
|
|
174
|
+
.env.*.local
|
|
175
|
+
`;
|
|
176
|
+
writeFileSync(gitignorePath, gitignoreContent);
|
|
177
|
+
}
|
|
178
|
+
console.log(`Adding Electric commands...`);
|
|
179
|
+
const packageJsonPath = join(appPath, `package.json`);
|
|
180
|
+
if (existsSync(packageJsonPath)) {
|
|
181
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`));
|
|
182
|
+
packageJson.scripts = {
|
|
183
|
+
...packageJson.scripts,
|
|
184
|
+
claim: `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`,
|
|
185
|
+
"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
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
188
|
+
}
|
|
189
|
+
console.log(`Template setup complete`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Template setup failed: ${error instanceof Error ? error.message : error}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
DEFAULT_ELECTRIC_API_BASE,
|
|
199
|
+
DEFAULT_ELECTRIC_URL,
|
|
200
|
+
DEFAULT_ELECTRIC_DASHBOARD_URL,
|
|
201
|
+
getElectricApiBase,
|
|
202
|
+
getElectricUrl,
|
|
203
|
+
getElectricDashboardUrl,
|
|
204
|
+
provisionElectricResources,
|
|
205
|
+
claimResources,
|
|
206
|
+
setupTemplate
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=chunk-DWNDVGD3.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 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":[]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
provisionElectricResources,
|
|
4
|
+
setupTemplate
|
|
5
|
+
} from "./chunk-DWNDVGD3.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
function printNextSteps(appName, fullSetup = false) {
|
|
11
|
+
console.log(`Next steps:`);
|
|
12
|
+
if (appName !== `.`) {
|
|
13
|
+
console.log(` cd ${appName}`);
|
|
14
|
+
}
|
|
15
|
+
if (fullSetup) {
|
|
16
|
+
console.log(` pnpm install`);
|
|
17
|
+
console.log(` pnpm migrate`);
|
|
18
|
+
}
|
|
19
|
+
console.log(` pnpm dev`);
|
|
20
|
+
console.log(``);
|
|
21
|
+
console.log(`Commands:`);
|
|
22
|
+
console.log(` pnpm psql # Connect to database`);
|
|
23
|
+
console.log(` pnpm claim # Claim cloud resources`);
|
|
24
|
+
console.log(` pnpm deploy:netlify # Deploy to Netlify`);
|
|
25
|
+
console.log(``);
|
|
26
|
+
console.log(`Tutorial: https://electric-sql.com/docs`);
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
if (args.length === 0) {
|
|
31
|
+
console.error(`Usage: npx @electric-sql/start <app-name>`);
|
|
32
|
+
console.error(
|
|
33
|
+
` npx @electric-sql/start . (configure current directory)`
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const appName = args[0];
|
|
38
|
+
if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {
|
|
39
|
+
console.error(
|
|
40
|
+
`App name must contain only letters, numbers, hyphens, and underscores`
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (appName === `.`) {
|
|
45
|
+
console.log(`Configuring current directory...`);
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`Creating app: ${appName}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const credentials = await provisionElectricResources();
|
|
51
|
+
console.log(`Setting up template...`);
|
|
52
|
+
await setupTemplate(appName, credentials);
|
|
53
|
+
console.log(`Installing dependencies...`);
|
|
54
|
+
try {
|
|
55
|
+
execSync(`pnpm install`, {
|
|
56
|
+
stdio: `inherit`,
|
|
57
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
58
|
+
});
|
|
59
|
+
} catch (_error) {
|
|
60
|
+
console.log(`Failed to install dependencies`);
|
|
61
|
+
printNextSteps(appName, true);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log(`Running migrations...`);
|
|
65
|
+
try {
|
|
66
|
+
execSync(`pnpm migrate`, {
|
|
67
|
+
stdio: `inherit`,
|
|
68
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
69
|
+
});
|
|
70
|
+
} catch (_error) {
|
|
71
|
+
console.log(`Failed to apply migrations`);
|
|
72
|
+
printNextSteps(appName, true);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log(`Setup complete`);
|
|
76
|
+
printNextSteps(appName);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(
|
|
79
|
+
`Setup failed:`,
|
|
80
|
+
error instanceof Error ? error.message : error
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
86
|
+
main().catch((error) => {
|
|
87
|
+
console.error(`Unexpected error:`, error);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
main
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.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\nif (import.meta.url === `file://${process.argv[1]}`) {\n main().catch((error) => {\n console.error(`Unexpected error:`, error)\n process.exit(1)\n })\n}\n\nexport { main }\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;AAEA,IAAI,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,IAAI;AACnD,OAAK,EAAE,MAAM,CAAC,UAAU;AACtB,YAAQ,MAAM,qBAAqB,KAAK;AACxC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ELECTRIC_API_BASE,
|
|
3
|
+
DEFAULT_ELECTRIC_DASHBOARD_URL,
|
|
4
|
+
DEFAULT_ELECTRIC_URL,
|
|
5
|
+
claimResources,
|
|
6
|
+
getElectricApiBase,
|
|
7
|
+
getElectricDashboardUrl,
|
|
8
|
+
getElectricUrl,
|
|
9
|
+
provisionElectricResources,
|
|
10
|
+
setupTemplate
|
|
11
|
+
} from "./chunk-DWNDVGD3.js";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_ELECTRIC_API_BASE,
|
|
14
|
+
DEFAULT_ELECTRIC_DASHBOARD_URL,
|
|
15
|
+
DEFAULT_ELECTRIC_URL,
|
|
16
|
+
claimResources,
|
|
17
|
+
getElectricApiBase,
|
|
18
|
+
getElectricDashboardUrl,
|
|
19
|
+
getElectricUrl,
|
|
20
|
+
provisionElectricResources,
|
|
21
|
+
setupTemplate
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@electric-sql/start",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI package for the ElectricSQL Quickstart.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"start": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "shx rm -rf dist && tsup",
|
|
21
|
+
"prepack": "pnpm build",
|
|
22
|
+
"dev": "pnpm run build && node dist/cli.js",
|
|
23
|
+
"format": "eslint . --fix",
|
|
24
|
+
"stylecheck": "eslint . --quiet",
|
|
25
|
+
"test": "pnpm exec vitest",
|
|
26
|
+
"coverage": "pnpm exec vitest --coverage",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.10.0",
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
|
33
|
+
"@typescript-eslint/parser": "^7.14.1",
|
|
34
|
+
"@vitest/coverage-istanbul": "4.0.15",
|
|
35
|
+
"eslint": "^8.57.0",
|
|
36
|
+
"eslint-config-prettier": "^9.1.0",
|
|
37
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
38
|
+
"prettier": "^3.3.2",
|
|
39
|
+
"shx": "^0.3.4",
|
|
40
|
+
"tsup": "^8.0.1",
|
|
41
|
+
"typescript": "^5.5.2",
|
|
42
|
+
"vitest": "^4.0.15"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src"
|
|
47
|
+
],
|
|
48
|
+
"keywords": [
|
|
49
|
+
"cli",
|
|
50
|
+
"db",
|
|
51
|
+
"electric",
|
|
52
|
+
"electric-sql",
|
|
53
|
+
"start",
|
|
54
|
+
"starter",
|
|
55
|
+
"tanstack"
|
|
56
|
+
],
|
|
57
|
+
"author": "ElectricSQL team and contributors.",
|
|
58
|
+
"license": "Apache-2.0",
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/electric-sql/electric.git"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/electric-sql/electric/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://electric-sql.com"
|
|
67
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import { provisionElectricResources } from './electric-api.js'
|
|
5
|
+
import { setupTemplate } from './template-setup.js'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
function printNextSteps(appName: string, fullSetup: boolean = false) {
|
|
9
|
+
console.log(`Next steps:`)
|
|
10
|
+
if (appName !== `.`) {
|
|
11
|
+
console.log(` cd ${appName}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (fullSetup) {
|
|
15
|
+
console.log(` pnpm install`)
|
|
16
|
+
console.log(` pnpm migrate`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(` pnpm dev`)
|
|
20
|
+
console.log(``)
|
|
21
|
+
console.log(`Commands:`)
|
|
22
|
+
console.log(` pnpm psql # Connect to database`)
|
|
23
|
+
console.log(` pnpm claim # Claim cloud resources`)
|
|
24
|
+
console.log(` pnpm deploy:netlify # Deploy to Netlify`)
|
|
25
|
+
console.log(``)
|
|
26
|
+
console.log(`Tutorial: https://electric-sql.com/docs`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
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]
|
|
42
|
+
|
|
43
|
+
// Validate app name (skip validation for "." which means current directory)
|
|
44
|
+
if (appName !== `.` && !/^[a-zA-Z0-9-_]+$/.test(appName)) {
|
|
45
|
+
console.error(
|
|
46
|
+
`App name must contain only letters, numbers, hyphens, and underscores`
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (appName === `.`) {
|
|
53
|
+
console.log(`Configuring current directory...`)
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`Creating app: ${appName}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const credentials = await provisionElectricResources()
|
|
60
|
+
|
|
61
|
+
// Step 2: Setup TanStack Start template
|
|
62
|
+
console.log(`Setting up template...`)
|
|
63
|
+
await setupTemplate(appName, credentials)
|
|
64
|
+
|
|
65
|
+
console.log(`Installing dependencies...`)
|
|
66
|
+
try {
|
|
67
|
+
execSync(`pnpm install`, {
|
|
68
|
+
stdio: `inherit`,
|
|
69
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),
|
|
70
|
+
})
|
|
71
|
+
} catch (_error) {
|
|
72
|
+
console.log(`Failed to install dependencies`)
|
|
73
|
+
printNextSteps(appName, true)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`Running migrations...`)
|
|
78
|
+
try {
|
|
79
|
+
execSync(`pnpm migrate`, {
|
|
80
|
+
stdio: `inherit`,
|
|
81
|
+
cwd: appName === `.` ? process.cwd() : join(process.cwd(), appName),
|
|
82
|
+
})
|
|
83
|
+
} catch (_error) {
|
|
84
|
+
console.log(`Failed to apply migrations`)
|
|
85
|
+
printNextSteps(appName, true)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 3: Display completion message
|
|
90
|
+
console.log(`Setup complete`)
|
|
91
|
+
printNextSteps(appName)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(
|
|
94
|
+
`Setup failed:`,
|
|
95
|
+
error instanceof Error ? error.message : error
|
|
96
|
+
)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
102
|
+
main().catch((error) => {
|
|
103
|
+
console.error(`Unexpected error:`, error)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { main }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Using native fetch (Node.js 18+)
|
|
2
|
+
|
|
3
|
+
export interface ElectricCredentials {
|
|
4
|
+
source_id: string
|
|
5
|
+
secret: string
|
|
6
|
+
DATABASE_URL: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClaimableSourceResponse {
|
|
10
|
+
claimId: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ClaimableSourceStatus {
|
|
14
|
+
state: `pending` | `ready` | `failed`
|
|
15
|
+
source: {
|
|
16
|
+
source_id: string
|
|
17
|
+
secret: string
|
|
18
|
+
}
|
|
19
|
+
connection_uri: string
|
|
20
|
+
claim_link?: string
|
|
21
|
+
project_id?: string
|
|
22
|
+
error: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_ELECTRIC_API_BASE = `https://dashboard.electric-sql.cloud/api`
|
|
26
|
+
export const DEFAULT_ELECTRIC_URL = `https://api.electric-sql.cloud`
|
|
27
|
+
export const DEFAULT_ELECTRIC_DASHBOARD_URL = `https://dashboard.electric-sql.cloud`
|
|
28
|
+
|
|
29
|
+
export function getElectricApiBase(): string {
|
|
30
|
+
return process.env.ELECTRIC_API_BASE_URL ?? DEFAULT_ELECTRIC_API_BASE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getElectricUrl(): string {
|
|
34
|
+
return process.env.ELECTRIC_URL ?? DEFAULT_ELECTRIC_URL
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getElectricDashboardUrl(): string {
|
|
38
|
+
return process.env.ELECTRIC_DASHBOARD_URL ?? DEFAULT_ELECTRIC_DASHBOARD_URL
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const POLL_INTERVAL_MS = 1000 // Poll every 1 second
|
|
42
|
+
const MAX_POLL_ATTEMPTS = 60 // Max 60 seconds
|
|
43
|
+
|
|
44
|
+
async function pollClaimableSource(
|
|
45
|
+
claimId: string,
|
|
46
|
+
maxAttempts: number = MAX_POLL_ATTEMPTS
|
|
47
|
+
): Promise<ClaimableSourceStatus> {
|
|
48
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
49
|
+
const response = await fetch(
|
|
50
|
+
`${getElectricApiBase()}/public/v1/claimable-sources/${claimId}`,
|
|
51
|
+
{
|
|
52
|
+
method: `GET`,
|
|
53
|
+
headers: {
|
|
54
|
+
'User-Agent': `@electric-sql/start`,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Handle 404 as "still being provisioned" - continue polling
|
|
60
|
+
if (response.status === 404) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// For other non-OK responses, throw an error
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const status = (await response.json()) as ClaimableSourceStatus
|
|
73
|
+
|
|
74
|
+
if (status.state === `ready`) {
|
|
75
|
+
return status
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (status.state === `failed` || status.error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Resource provisioning failed${status.error ? `: ${status.error}` : ``}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Wait before polling again
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Timeout waiting for resources to be provisioned after ${maxAttempts} attempts`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function provisionElectricResources(): Promise<
|
|
94
|
+
ElectricCredentials & ClaimableSourceResponse
|
|
95
|
+
> {
|
|
96
|
+
console.log(`Provisioning resources...`)
|
|
97
|
+
try {
|
|
98
|
+
// Step 1: POST to create claimable source and get claimId
|
|
99
|
+
const response = await fetch(
|
|
100
|
+
`${getElectricApiBase()}/public/v1/claimable-sources`,
|
|
101
|
+
{
|
|
102
|
+
method: `POST`,
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': `application/json`,
|
|
105
|
+
'User-Agent': `@electric-sql/start`,
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({}),
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { claimId } = (await response.json()) as ClaimableSourceResponse
|
|
118
|
+
|
|
119
|
+
if (!claimId) {
|
|
120
|
+
throw new Error(`Invalid response from Electric API - missing claimId`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 2: Poll until state === 'ready'
|
|
124
|
+
const status = await pollClaimableSource(claimId)
|
|
125
|
+
|
|
126
|
+
// Step 3: Extract and validate credentials
|
|
127
|
+
if (
|
|
128
|
+
!status.source?.source_id ||
|
|
129
|
+
!status.source?.secret ||
|
|
130
|
+
!status.connection_uri
|
|
131
|
+
) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Invalid response from Electric API - missing required credentials`
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
source_id: status.source.source_id,
|
|
139
|
+
secret: status.source.secret,
|
|
140
|
+
DATABASE_URL: status.connection_uri,
|
|
141
|
+
claimId,
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Failed to provision Electric resources: ${error.message}`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Failed to provision Electric resources: Unknown error`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function claimResources(
|
|
154
|
+
sourceId: string,
|
|
155
|
+
secret: string
|
|
156
|
+
): Promise<{ claimUrl: string }> {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(`${getElectricApiBase()}/v1/claim`, {
|
|
159
|
+
method: `POST`,
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': `application/json`,
|
|
162
|
+
Authorization: `Bearer ${secret}`,
|
|
163
|
+
'User-Agent': `@electric-sql/start`,
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
source_id: sourceId,
|
|
167
|
+
}),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Electric API error: ${response.status} ${response.statusText}`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = (await response.json()) as { claimUrl: string }
|
|
177
|
+
|
|
178
|
+
if (!result.claimUrl) {
|
|
179
|
+
throw new Error(`Invalid response from Electric API - missing claim URL`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Error) {
|
|
185
|
+
throw new Error(`Failed to initiate resource claim: ${error.message}`)
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`Failed to initiate resource claim: Unknown error`)
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { randomBytes } from 'crypto'
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import {
|
|
6
|
+
ElectricCredentials,
|
|
7
|
+
ClaimableSourceResponse,
|
|
8
|
+
getElectricUrl,
|
|
9
|
+
getElectricDashboardUrl,
|
|
10
|
+
} from './electric-api'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a cryptographically secure random string for use as a secret
|
|
14
|
+
* @param length - The length of the secret in bytes (will be hex encoded, so output is 2x length)
|
|
15
|
+
* @returns A random hex string
|
|
16
|
+
*/
|
|
17
|
+
function generateSecret(length: number = 32): string {
|
|
18
|
+
return randomBytes(length).toString(`hex`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function setupTemplate(
|
|
22
|
+
appName: string,
|
|
23
|
+
credentials: ElectricCredentials & ClaimableSourceResponse
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const appPath = appName === `.` ? process.cwd() : join(process.cwd(), appName)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Step 1: Pull TanStack Start template using gitpick (skip for current directory)
|
|
29
|
+
if (appName !== `.`) {
|
|
30
|
+
console.log(`Pulling template...`)
|
|
31
|
+
execSync(
|
|
32
|
+
`npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter ${appName}`,
|
|
33
|
+
{ stdio: `inherit` }
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 2: Generate .env file with credentials
|
|
38
|
+
console.log(`Configuring environment...`)
|
|
39
|
+
const betterAuthSecret = generateSecret(32)
|
|
40
|
+
const electricUrl = getElectricUrl()
|
|
41
|
+
const envContent = `# Electric SQL Configuration
|
|
42
|
+
# Generated by @electric-sql/start
|
|
43
|
+
# DO NOT COMMIT THIS FILE
|
|
44
|
+
|
|
45
|
+
# Database
|
|
46
|
+
DATABASE_URL=${credentials.DATABASE_URL}
|
|
47
|
+
|
|
48
|
+
# Electric Cloud
|
|
49
|
+
ELECTRIC_URL=${electricUrl}
|
|
50
|
+
ELECTRIC_SOURCE_ID=${credentials.source_id}
|
|
51
|
+
ELECTRIC_SECRET=${credentials.secret}
|
|
52
|
+
|
|
53
|
+
# Authentication
|
|
54
|
+
BETTER_AUTH_SECRET=${betterAuthSecret}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
writeFileSync(join(appPath, `.env`), envContent)
|
|
58
|
+
|
|
59
|
+
// Step 3: Ensure .gitignore includes .env
|
|
60
|
+
console.log(`Updating .gitignore...`)
|
|
61
|
+
const gitignorePath = join(appPath, `.gitignore`)
|
|
62
|
+
let gitignoreContent = ``
|
|
63
|
+
|
|
64
|
+
if (existsSync(gitignorePath)) {
|
|
65
|
+
gitignoreContent = readFileSync(gitignorePath, `utf8`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!gitignoreContent.includes(`.env`)) {
|
|
69
|
+
gitignoreContent += `\n# Environment variables\n.env\n.env.local\n.env.*.local\n`
|
|
70
|
+
writeFileSync(gitignorePath, gitignoreContent)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`Adding Electric commands...`)
|
|
74
|
+
const packageJsonPath = join(appPath, `package.json`)
|
|
75
|
+
|
|
76
|
+
if (existsSync(packageJsonPath)) {
|
|
77
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, `utf8`))
|
|
78
|
+
|
|
79
|
+
// Add/update scripts for cloud mode and Electric commands
|
|
80
|
+
packageJson.scripts = {
|
|
81
|
+
...packageJson.scripts,
|
|
82
|
+
claim: `npx open-cli "${getElectricDashboardUrl()}/claim?uuid=${credentials.claimId}"`,
|
|
83
|
+
'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
|
+
}
|
|
85
|
+
|
|
86
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`Template setup complete`)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Template setup failed: ${error instanceof Error ? error.message : error}`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|