@specific.dev/cli 0.1.48 → 0.1.50

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.
Files changed (37) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.__PAGE__.txt +1 -1
  4. package/dist/admin/__next._full.txt +1 -1
  5. package/dist/admin/__next._head.txt +1 -1
  6. package/dist/admin/__next._index.txt +1 -1
  7. package/dist/admin/__next._tree.txt +1 -1
  8. package/dist/admin/_not-found/__next._full.txt +1 -1
  9. package/dist/admin/_not-found/__next._head.txt +1 -1
  10. package/dist/admin/_not-found/__next._index.txt +1 -1
  11. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  13. package/dist/admin/_not-found/__next._tree.txt +1 -1
  14. package/dist/admin/_not-found/index.html +1 -1
  15. package/dist/admin/_not-found/index.txt +1 -1
  16. package/dist/admin/databases/__next._full.txt +1 -1
  17. package/dist/admin/databases/__next._head.txt +1 -1
  18. package/dist/admin/databases/__next._index.txt +1 -1
  19. package/dist/admin/databases/__next._tree.txt +1 -1
  20. package/dist/admin/databases/__next.databases.__PAGE__.txt +1 -1
  21. package/dist/admin/databases/__next.databases.txt +1 -1
  22. package/dist/admin/databases/index.html +1 -1
  23. package/dist/admin/databases/index.txt +1 -1
  24. package/dist/admin/index.html +1 -1
  25. package/dist/admin/index.txt +1 -1
  26. package/dist/cli.js +1814 -739
  27. package/dist/docs/builds.md +22 -0
  28. package/dist/docs/index.md +3 -0
  29. package/dist/docs/postgres/reshape/actions.md +90 -0
  30. package/dist/docs/postgres/reshape/index.md +149 -0
  31. package/dist/docs/postgres.md +35 -1
  32. package/dist/docs/services.md +62 -8
  33. package/dist/postinstall.js +141 -0
  34. package/package.json +3 -2
  35. /package/dist/admin/_next/static/{dyH4SZNKyN31L1iV-yPZA → o2Qo92jA0gWbtB1ZWKQFF}/_buildManifest.js +0 -0
  36. /package/dist/admin/_next/static/{dyH4SZNKyN31L1iV-yPZA → o2Qo92jA0gWbtB1ZWKQFF}/_clientMiddlewareManifest.json +0 -0
  37. /package/dist/admin/_next/static/{dyH4SZNKyN31L1iV-yPZA → o2Qo92jA0gWbtB1ZWKQFF}/_ssgManifest.js +0 -0
@@ -16,6 +16,7 @@ build "api" {
16
16
  ## Optional fields
17
17
 
18
18
  - `command` - Build command to run after dependencies are installed (e.g., `npm run build`, `go build -o api`).
19
+ - `env` - Environment variables available during the build. Supports string literals and `service.<name>.public_url` references. These are passed as Docker build args.
19
20
 
20
21
  ## Automatic dependency installation
21
22
 
@@ -51,6 +52,27 @@ For **go**, **rust**, and **java**, a multi-stage build is used:
51
52
  - **rust**: Only `target/release` is copied to the working directory in the runtime image.
52
53
  - **java**: JAR files from `target/*.jar` or `build/libs/*.jar` are copied to `app.jar` in the working directory.
53
54
 
55
+ ## Build-time environment variables
56
+
57
+ Use `env` to pass environment variables during the build step. This is useful for frameworks like Next.js that inline environment variables at build time.
58
+
59
+ ```hcl
60
+ build "web" {
61
+ base = "node"
62
+ command = "npm run build"
63
+
64
+ env = {
65
+ NEXT_PUBLIC_API_URL = service.api.public_url
66
+ NODE_ENV = "production"
67
+ }
68
+ }
69
+ ```
70
+
71
+ Supported reference types:
72
+
73
+ - String literals (e.g., `"production"`)
74
+ - `service.<name>.public_url` - The public domain of another service
75
+
54
76
  ## Dev configuration
55
77
 
56
78
  Override the build command for local development. If no `dev` block is defined, the build is skipped in development.
@@ -17,6 +17,9 @@ A full development environment can be started with `specific dev`. To deploy any
17
17
  - [Services](/services): define how to build, deploy and run the code in this project as services, both in development and production.
18
18
  - [Secrets and configuration](/secrets-config): define secrets and configuration variables that the user may or should provide. These can be injected into services through environment variables.
19
19
  - [Postgres](/postgres): define managed PostgreSQL databases that services depend on.
20
+ <!-- beta:reshape -->
21
+ - [Reshape](/postgres/reshape): zero-downtime database schema migrations.
22
+ <!-- /beta:reshape -->
20
23
  - [Sync](/sync): define real-time, partial synchronisation out of PostgreSQL into frontends or other services. Should be used to implement real-time and collaborative apps.
21
24
  - [Storage](/storage): define S3-compatible object/blob storage for services to store files in.
22
25
  - [Redis](/redis): define non-durable Redis-compatible databases for caching and more.
@@ -0,0 +1,90 @@
1
+ <!-- beta:reshape -->
2
+ # Reshape Actions
3
+
4
+ Reshape migrations are composed of **actions** that define schema changes. Each migration file contains one or more actions that are applied together.
5
+
6
+ ## Available Actions
7
+
8
+ Reshape supports many different actions for modifying your database schema:
9
+
10
+ - `create_table` - Create a new table
11
+ - `add_column` - Add a column to an existing table
12
+ - `alter_column` - Modify an existing column (type, default, nullable)
13
+ - `remove_column` - Remove a column from a table
14
+ - `create_index` - Create an index
15
+ - `remove_index` - Remove an index
16
+ - `add_foreign_key` - Add a foreign key constraint
17
+ - `remove_foreign_key` - Remove a foreign key constraint
18
+ - `create_enum` - Create an enum type
19
+ - `alter_enum` - Add or remove variants from an enum
20
+ - `remove_enum` - Remove an enum type
21
+ - `rename_table` - Rename a table
22
+ - `remove_table` - Remove a table
23
+ - `custom` - Run custom SQL
24
+
25
+ ## Action Documentation
26
+
27
+ For detailed documentation on each action including all available options and examples, use the Reshape CLI directly:
28
+
29
+ ```bash
30
+ # List all available actions
31
+ reshape docs /actions
32
+
33
+ # Get documentation for a specific action
34
+ reshape docs /actions/create-table
35
+ reshape docs /actions/add-column
36
+ reshape docs /actions/alter-column
37
+ reshape docs /actions/remove-column
38
+ reshape docs /actions/create-enum
39
+ reshape docs /actions/alter-enum
40
+ reshape docs /actions/remove-enum
41
+ reshape docs /actions/add-index
42
+ reshape docs /actions/remove-index
43
+ reshape docs /actions/add-foreign-key
44
+ reshape docs /actions/remove-foreign-key
45
+ reshape docs /actions/rename-table
46
+ reshape docs /actions/remove-table
47
+ reshape docs /actions/custom
48
+ ```
49
+
50
+ ## Example Migration
51
+
52
+ Here's an example migration that creates a table and adds an index:
53
+
54
+ ```toml
55
+ # migrations/001_create_users.toml
56
+
57
+ [[actions]]
58
+ type = "create_table"
59
+ name = "users"
60
+ primary_key = ["id"]
61
+
62
+ [[actions.columns]]
63
+ name = "id"
64
+ type = "INTEGER"
65
+ generated = "ALWAYS AS IDENTITY"
66
+
67
+ [[actions.columns]]
68
+ name = "email"
69
+ type = "TEXT"
70
+ nullable = false
71
+
72
+ [[actions.columns]]
73
+ name = "created_at"
74
+ type = "TIMESTAMPTZ"
75
+ default = "NOW()"
76
+
77
+ [[actions]]
78
+ type = "add_index"
79
+ table = "users"
80
+ name = "users_email_idx"
81
+ columns = ["email"]
82
+ unique = true
83
+ ```
84
+
85
+ ---
86
+
87
+ Related topics:
88
+ - Run `specific docs /postgres/reshape` for general Reshape documentation
89
+ - Run `reshape docs /actions/{ACTION}` for detailed action documentation
90
+ <!-- /beta:reshape -->
@@ -0,0 +1,149 @@
1
+ <!-- beta:reshape -->
2
+ # Reshape Migrations
3
+
4
+ Zero-downtime database schema migrations using [Reshape](https://github.com/fabianlindfors/reshape).
5
+
6
+ Reshape is a schema migration tool that uses a 3-phase approach to achieve zero-downtime migrations:
7
+
8
+ 1. **Start**: Creates new schema version with views and triggers. Both old and new schemas are available simultaneously.
9
+ 2. **Rollout**: Deploy new application code. Services use the new schema via `search_path`.
10
+ 3. **Complete**: Removes old schema and cleanup artifacts.
11
+
12
+ ## Enabling Reshape
13
+
14
+ Add a `reshape` block to your postgres definition in `specific.hcl`:
15
+
16
+ ```hcl
17
+ postgres "main" {
18
+ reshape {
19
+ enabled = true
20
+ }
21
+ }
22
+ ```
23
+
24
+ With a custom migrations directory:
25
+
26
+ ```hcl
27
+ postgres "main" {
28
+ reshape {
29
+ enabled = true
30
+ migrations_dir = "db/migrations"
31
+ }
32
+ }
33
+ ```
34
+
35
+ The default migrations directory is `migrations/` relative to `specific.hcl`.
36
+
37
+ ## Automatic Migration Management
38
+
39
+ When `specific dev` starts with Reshape enabled, migrations are automatically managed:
40
+
41
+ 1. **All migrations except the last one are completed** - These become permanent schema changes
42
+ 2. **The last migration is started but not completed** - This allows iteration during development
43
+ 3. **Completed migration files are made read-only** - Prevents accidental modification
44
+
45
+ This means you can focus on writing migrations without manually running commands.
46
+
47
+ ### File Change Handling
48
+
49
+ While `specific dev` is running:
50
+
51
+ - **Modifying the last migration file**: The migration is automatically aborted and restarted. Your services continue running (no restart needed since the schema name doesn't change).
52
+
53
+ - **Adding a new migration file**: The previous migration is completed (made permanent), and the new migration is started. Services are automatically restarted to use the new schema.
54
+
55
+ ## Connection Strings
56
+
57
+ When Reshape is enabled, `postgres.<name>.url` automatically includes the correct `search_path` option. You don't need to change your service configuration:
58
+
59
+ ```hcl
60
+ service "api" {
61
+ build = build.api
62
+ command = "node server.js"
63
+
64
+ env = {
65
+ DATABASE_URL = postgres.main.url # Automatically includes search_path
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Writing Migrations
71
+
72
+ Place migration files in your migrations directory. Files are processed in **lexical order** (e.g., `001_first.toml` before `002_second.toml`).
73
+
74
+ **IMPORTANT:** You MUST run `specific reshape check` every time you create or edit a migration file. This validates the migration syntax and catches errors before the watcher applies them. Without this step, invalid migrations will silently fail to apply.
75
+
76
+ ```bash
77
+ # Validate all migration files
78
+ specific reshape check
79
+
80
+ # Validate migrations for a specific database
81
+ specific reshape check main
82
+ ```
83
+
84
+ Example migration (`migrations/001_create_users.toml`):
85
+
86
+ ```toml
87
+ [[actions]]
88
+ type = "create_table"
89
+ name = "users"
90
+ primary_key = ["id"]
91
+
92
+ [[actions.columns]]
93
+ name = "id"
94
+ type = "INTEGER"
95
+ generated = "ALWAYS AS IDENTITY"
96
+
97
+ [[actions.columns]]
98
+ name = "email"
99
+ type = "TEXT"
100
+ nullable = false
101
+ ```
102
+
103
+ For full documentation on available actions and migration syntax, see [Actions](/postgres/reshape/actions).
104
+
105
+ ## Manual Migration Commands
106
+
107
+ You can also manually control migrations using the `specific reshape` command:
108
+
109
+ ```bash
110
+ # Validate migration files (does NOT require specific dev to be running)
111
+ specific reshape check
112
+
113
+ # Start a migration (applies new schema while keeping old one available)
114
+ specific reshape start
115
+
116
+ # Start migration on a specific database
117
+ specific reshape start main
118
+
119
+ # Check migration status
120
+ specific reshape status main
121
+
122
+ # Complete a migration (removes old schema)
123
+ specific reshape complete main
124
+
125
+ # Abort a migration (rolls back to old schema)
126
+ specific reshape abort main
127
+ ```
128
+
129
+ **Note:** Most commands require `specific dev` to be running as they need access to the database. The `check` command is an exception - it only validates migration file syntax and can be run anytime.
130
+
131
+ ## Development Workflow
132
+
133
+ 1. Write your migration file in the migrations directory
134
+ 2. Run `specific reshape check` to validate the migration syntax
135
+ 3. `specific dev` automatically applies it
136
+ 4. Iterate on the migration - run `specific reshape check` after **every** change
137
+ 5. When ready, add a new migration file to "lock in" the previous one
138
+
139
+ **Remember:** Always run `specific reshape check` after any change to a migration file. This is required to catch syntax errors before they are applied.
140
+
141
+ Completed migrations are made read-only to prevent accidental changes. If you need to modify a completed migration, you'll need to manually change the file permissions first.
142
+
143
+ ---
144
+
145
+ Related topics:
146
+ - Run `specific docs /postgres/reshape/actions` for available migration actions
147
+ - Run `specific docs /postgres` for general PostgreSQL documentation
148
+ - Run `specific docs /sync` for real-time data synchronization
149
+ <!-- /beta:reshape -->
@@ -34,8 +34,42 @@ postgres "main" {}
34
34
  - `password` - Database password
35
35
  - `name` - Database name
36
36
 
37
+ ## Querying the database
38
+
39
+ Use `specific psql` to connect to a development database. This will start the database if it's not already running.
40
+
41
+ ```sh
42
+ # Interactive session
43
+ specific psql main
44
+
45
+ # Run a single query (useful for coding agents)
46
+ specific psql main -- -c "SELECT * FROM users LIMIT 5"
47
+
48
+ # List tables
49
+ specific psql main -- -c "\dt"
50
+ ```
51
+
52
+ <!-- beta:reshape -->
53
+ ## Schema Migrations
54
+
55
+ For zero-downtime schema migrations, use Reshape:
56
+
57
+ ```hcl
58
+ postgres "main" {
59
+ reshape {
60
+ enabled = true
61
+ }
62
+ }
63
+ ```
64
+
65
+ Run `specific docs /postgres/reshape` for full documentation on managing migrations.
66
+
67
+ <!-- /beta:reshape -->
37
68
  ---
38
69
 
39
70
  Related topics:
71
+ <!-- beta:reshape -->
72
+ - Run `specific docs /postgres/reshape` for zero-downtime schema migrations
73
+ <!-- /beta:reshape -->
40
74
  - Run `specific docs sync` for real-time data synchronization
41
- - Run `specific docs exec` for running migrations
75
+ - Run `specific docs exec` for running one-off commands
@@ -4,9 +4,28 @@ Services define how to run the code and connect it to other parts of the infrast
4
4
 
5
5
  ## Dynamic services
6
6
 
7
- Run a server process exposed via HTTP. TLS is handled automatically.
7
+ Example of running publicly exposed API and frontend web services:
8
8
 
9
9
  ```hcl
10
+ build "web" {
11
+ base = "node"
12
+ command = "npm run build"
13
+ }
14
+
15
+ service "web" {
16
+ build = build.web
17
+ command = "npm start"
18
+
19
+ endpoint {
20
+ public = true
21
+ }
22
+
23
+ env = {
24
+ PORT = port
25
+ API_URL = service.api.public_url
26
+ }
27
+ }
28
+
10
29
  build "api" {
11
30
  base = "go"
12
31
  command = "go build -o api"
@@ -93,9 +112,9 @@ service "worker" {
93
112
  }
94
113
  ```
95
114
 
96
- ## Inter-service communication
115
+ ## Private inter-service communication
97
116
 
98
- Services can reference other services' endpoints to communicate with each other:
117
+ Services can reference other services' endpoints using `private_url` to communicate with each other over a private network:
99
118
 
100
119
  ```hcl
101
120
  service "api" {
@@ -116,23 +135,58 @@ service "worker" {
116
135
  command = "./worker"
117
136
 
118
137
  env = {
119
- API_URL = service.api.url # localhost:PORT in dev, api:80 in prod
138
+ API_URL = service.api.private_url
120
139
  }
121
140
  }
122
141
  ```
123
142
 
124
143
  Available service reference attributes:
125
144
 
126
- - `service.<name>.url` - Full URL without scheme (e.g., `localhost:3000`)
145
+ - `service.<name>.private_url` - Internal URL without scheme (host and port)
127
146
  - `service.<name>.host` - Host only (e.g., `localhost`)
128
147
  - `service.<name>.port` - Port only (e.g., `3000`)
148
+ - `service.<name>.public_url` - Public URL without scheme (host and port). Only available for endpoints with `public = true`. Served over HTTPS.
129
149
 
130
150
  For services with multiple named endpoints, you must specify the endpoint:
131
151
 
132
152
  ```hcl
133
153
  env = {
134
- MAIN_URL = service.api.endpoint.main.url
135
- ADMIN_URL = service.api.endpoint.admin.url
154
+ MAIN_URL = service.api.endpoint.main.private_url
155
+ ADMIN_URL = service.api.endpoint.admin.private_url
156
+ PUBLIC_API = service.api.endpoint.main.public_url
157
+ }
158
+ ```
159
+
160
+ ### Public inter-service communication
161
+
162
+ The private communication does not work in the case of a frontend or client service needing to speak with a backend service, as `private_url` is not externally reachable. In that case, the backend service MUST expose a public endpoint. The frontend or client service can then reference that endpoint using `public_url`:
163
+
164
+ ```hcl
165
+ service "web" {
166
+ build = build.web
167
+ command = "npm start"
168
+
169
+ endpoint {
170
+ public = true
171
+ }
172
+
173
+ env = {
174
+ PORT = port
175
+ }
176
+ }
177
+
178
+ service "api" {
179
+ build = build.api
180
+ command = "./api"
181
+
182
+ endpoint {
183
+ public = true
184
+ }
185
+
186
+ env = {
187
+ PORT = port
188
+ CORS_ORIGIN = service.web.public_url
189
+ }
136
190
  }
137
191
  ```
138
192
 
@@ -224,7 +278,7 @@ service "worker" {
224
278
  dev {
225
279
  command = "node --watch worker.js"
226
280
  env = {
227
- TEMPORAL_URL = service.temporal.url # Override with local URL in dev
281
+ TEMPORAL_URL = service.temporal.private_url # Override with local URL in dev
228
282
  }
229
283
  }
230
284
  }
@@ -0,0 +1,141 @@
1
+ // src/lib/analytics/index.ts
2
+ import { PostHog } from "posthog-node";
3
+ import * as os2 from "os";
4
+ import * as crypto from "crypto";
5
+
6
+ // src/lib/project/config.ts
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ var PROJECT_ID_FILE = ".specific/project_id";
10
+ var ProjectNotLinkedError = class extends Error {
11
+ constructor() {
12
+ super(
13
+ `Project not linked to Specific.
14
+
15
+ The file ${PROJECT_ID_FILE} does not exist.
16
+
17
+ Run: specific deploy`
18
+ );
19
+ this.name = "ProjectNotLinkedError";
20
+ }
21
+ };
22
+ function readProjectId(projectDir = process.cwd()) {
23
+ const projectIdPath = path.join(projectDir, PROJECT_ID_FILE);
24
+ if (!fs.existsSync(projectIdPath)) {
25
+ throw new ProjectNotLinkedError();
26
+ }
27
+ const projectId = fs.readFileSync(projectIdPath, "utf-8").trim();
28
+ if (!projectId) {
29
+ throw new Error(`${PROJECT_ID_FILE} is empty`);
30
+ }
31
+ return projectId;
32
+ }
33
+ function hasProjectId(projectDir = process.cwd()) {
34
+ const projectIdPath = path.join(projectDir, PROJECT_ID_FILE);
35
+ return fs.existsSync(projectIdPath);
36
+ }
37
+
38
+ // src/lib/auth/credentials.ts
39
+ import * as fs2 from "fs";
40
+ import * as path2 from "path";
41
+ import * as os from "os";
42
+
43
+ // src/lib/auth/login.tsx
44
+ import React from "react";
45
+ import { render, Box, Text } from "ink";
46
+ import Spinner from "ink-spinner";
47
+
48
+ // src/lib/auth/credentials.ts
49
+ function getUserCredentialsDir() {
50
+ return path2.join(os.homedir(), ".specific");
51
+ }
52
+ function getCredentialsPath() {
53
+ return path2.join(getUserCredentialsDir(), "credentials.json");
54
+ }
55
+ function readUserCredentials() {
56
+ const credentialsPath = getCredentialsPath();
57
+ if (!fs2.existsSync(credentialsPath)) {
58
+ return null;
59
+ }
60
+ try {
61
+ const content = fs2.readFileSync(credentialsPath, "utf-8");
62
+ return JSON.parse(content);
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function getUserId() {
68
+ const credentials = readUserCredentials();
69
+ return credentials?.userId ?? null;
70
+ }
71
+
72
+ // src/lib/analytics/index.ts
73
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
74
+ var client = null;
75
+ var anonymousId = null;
76
+ function isEnabled() {
77
+ return true;
78
+ }
79
+ function getAnonymousId() {
80
+ if (anonymousId) return anonymousId;
81
+ const machineId = `${os2.hostname()}-${os2.userInfo().username}`;
82
+ anonymousId = crypto.createHash("sha256").update(machineId).digest("hex").slice(0, 16);
83
+ return anonymousId;
84
+ }
85
+ function getProjectId() {
86
+ try {
87
+ if (hasProjectId()) {
88
+ return readProjectId();
89
+ }
90
+ } catch {
91
+ }
92
+ return void 0;
93
+ }
94
+ function getClient() {
95
+ if (!isEnabled()) return null;
96
+ if (!client) {
97
+ client = new PostHog("phc_qNQCEUXh6ErdciQRRmeG2xlVvwFjkcW6A5bnOFJ8vXZ", {
98
+ host: POSTHOG_HOST,
99
+ flushAt: 1,
100
+ // Flush immediately for CLI
101
+ flushInterval: 0
102
+ });
103
+ }
104
+ return client;
105
+ }
106
+ function trackEvent(event, properties) {
107
+ const ph = getClient();
108
+ if (!ph) return;
109
+ ph.capture({
110
+ distinctId: getAnonymousId(),
111
+ event,
112
+ properties: {
113
+ ...properties,
114
+ cli_version: "0.1.50",
115
+ platform: process.platform,
116
+ node_version: process.version,
117
+ project_id: getProjectId(),
118
+ user_id: getUserId() ?? void 0
119
+ }
120
+ });
121
+ }
122
+ async function shutdown() {
123
+ if (client) {
124
+ await client.shutdown();
125
+ client = null;
126
+ }
127
+ }
128
+
129
+ // src/postinstall.ts
130
+ async function main() {
131
+ try {
132
+ trackEvent("cli_installed");
133
+ await shutdown();
134
+ } catch {
135
+ }
136
+ console.log();
137
+ console.log("Thanks for installing Specific!");
138
+ console.log("Get started: https://docs.specific.dev/");
139
+ console.log();
140
+ }
141
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -16,7 +16,8 @@
16
16
  "build:staging": "NODE_ENV=staging node build.mjs && cp -r src/docs dist/",
17
17
  "link:dev": "npm run build:dev && npm link",
18
18
  "link:staging": "npm run build:staging && npm link",
19
- "link:prod": "npm run build && npm link"
19
+ "link:prod": "npm run build && npm link",
20
+ "postinstall": "node dist/postinstall.js || true"
20
21
  },
21
22
  "keywords": [
22
23
  "infrastructure",