@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 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
@@ -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,2 @@
1
+ export * from './electric-api.js'
2
+ export * from './template-setup.js'
@@ -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
+ }