crafters 0.0.1 → 0.1.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/CHANGELOG.md +29 -0
- package/README.md +30 -6
- package/bun.lock +190 -0
- package/package.json +12 -2
- package/src/commands/domain.ts +168 -0
- package/src/commands/login.ts +92 -0
- package/src/index.ts +20 -0
- package/src/lib/config.ts +51 -0
- package/src/lib/spaceship.ts +101 -0
- package/src/lib/vercel.ts +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2024-12-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **CLI Commands**
|
|
13
|
+
- `crafters domain add <subdomain> -p <project>` - Add subdomain to Vercel project
|
|
14
|
+
- `crafters domain remove <subdomain> -p <project>` - Remove subdomain from Vercel project
|
|
15
|
+
- `crafters domain list` - List all CNAME records
|
|
16
|
+
- `crafters login` - Configure credentials
|
|
17
|
+
- `crafters logout` - Remove stored credentials
|
|
18
|
+
- `crafters whoami` - Show current configuration
|
|
19
|
+
|
|
20
|
+
- **Features**
|
|
21
|
+
- Automatic CNAME creation in Spaceship DNS
|
|
22
|
+
- Uses Vercel's recommended CNAME (project-specific) instead of generic `cname.vercel-dns.com`
|
|
23
|
+
- Credentials stored securely in `~/.crafters/config.json`
|
|
24
|
+
- Support for environment variables as fallback
|
|
25
|
+
|
|
26
|
+
### Dependencies
|
|
27
|
+
|
|
28
|
+
- `@vercel/sdk` - Vercel API integration
|
|
29
|
+
- `citty` - Lightweight CLI framework
|
package/README.md
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# crafters
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for managing domains on Vercel projects with Spaceship DNS.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
bun install
|
|
8
|
+
bun install -g crafters
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Setup
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
|
|
14
|
+
crafters login \
|
|
15
|
+
--spaceshipKey="YOUR_KEY" \
|
|
16
|
+
--spaceshipSecret="YOUR_SECRET" \
|
|
17
|
+
--vercelToken="YOUR_TOKEN" \
|
|
18
|
+
--vercelTeamId="YOUR_TEAM_ID"
|
|
13
19
|
```
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Add subdomain to project
|
|
25
|
+
crafters domain add myapp -p my-vercel-project
|
|
26
|
+
|
|
27
|
+
# Remove subdomain
|
|
28
|
+
crafters domain remove myapp -p my-vercel-project
|
|
29
|
+
|
|
30
|
+
# List all CNAME records
|
|
31
|
+
crafters domain list
|
|
32
|
+
|
|
33
|
+
# Check current config
|
|
34
|
+
crafters whoami
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT
|
package/bun.lock
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
"workspaces": {
|
|
4
4
|
"": {
|
|
5
5
|
"name": "cs",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@vercel/sdk": "^1.0.0",
|
|
8
|
+
"citty": "^0.1.6",
|
|
9
|
+
},
|
|
6
10
|
"devDependencies": {
|
|
7
11
|
"@types/bun": "latest",
|
|
8
12
|
},
|
|
@@ -12,14 +16,200 @@
|
|
|
12
16
|
},
|
|
13
17
|
},
|
|
14
18
|
"packages": {
|
|
19
|
+
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
|
20
|
+
|
|
21
|
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
|
|
22
|
+
|
|
15
23
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
16
24
|
|
|
17
25
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
18
26
|
|
|
27
|
+
"@vercel/sdk": ["@vercel/sdk@1.18.5", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", "zod": "^3.25.0 || ^4.0.0" }, "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-tzxGuUxYZQpKsf5WrImp4gnCZu3xhHA4j6KLSAQdXgpi/Lfk3JV5YGEDU6ZZBIwXDMdny5DozLd20tz/bKZsfg=="],
|
|
28
|
+
|
|
29
|
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
|
30
|
+
|
|
31
|
+
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
|
32
|
+
|
|
33
|
+
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
|
34
|
+
|
|
35
|
+
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
|
36
|
+
|
|
19
37
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
20
38
|
|
|
39
|
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
|
40
|
+
|
|
41
|
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
42
|
+
|
|
43
|
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
|
44
|
+
|
|
45
|
+
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
|
46
|
+
|
|
47
|
+
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
|
48
|
+
|
|
49
|
+
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
|
50
|
+
|
|
51
|
+
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
|
52
|
+
|
|
53
|
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
54
|
+
|
|
55
|
+
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
|
56
|
+
|
|
57
|
+
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
|
58
|
+
|
|
59
|
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
|
60
|
+
|
|
61
|
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
62
|
+
|
|
63
|
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
|
64
|
+
|
|
65
|
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
|
66
|
+
|
|
67
|
+
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
|
68
|
+
|
|
69
|
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
|
70
|
+
|
|
71
|
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
|
72
|
+
|
|
73
|
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
|
74
|
+
|
|
75
|
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
|
76
|
+
|
|
77
|
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
|
78
|
+
|
|
79
|
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
|
80
|
+
|
|
81
|
+
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
|
82
|
+
|
|
83
|
+
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
|
84
|
+
|
|
85
|
+
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
|
86
|
+
|
|
87
|
+
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
|
88
|
+
|
|
89
|
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
|
90
|
+
|
|
91
|
+
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
|
92
|
+
|
|
93
|
+
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
|
94
|
+
|
|
95
|
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
|
96
|
+
|
|
97
|
+
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
|
98
|
+
|
|
99
|
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
|
100
|
+
|
|
101
|
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
|
102
|
+
|
|
103
|
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
|
104
|
+
|
|
105
|
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
106
|
+
|
|
107
|
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
108
|
+
|
|
109
|
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
|
110
|
+
|
|
111
|
+
"hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="],
|
|
112
|
+
|
|
113
|
+
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
|
114
|
+
|
|
115
|
+
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
|
116
|
+
|
|
117
|
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
|
118
|
+
|
|
119
|
+
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
|
120
|
+
|
|
121
|
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
|
122
|
+
|
|
123
|
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
124
|
+
|
|
125
|
+
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
|
126
|
+
|
|
127
|
+
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
128
|
+
|
|
129
|
+
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
|
130
|
+
|
|
131
|
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
|
132
|
+
|
|
133
|
+
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
|
134
|
+
|
|
135
|
+
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
|
136
|
+
|
|
137
|
+
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
|
138
|
+
|
|
139
|
+
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
|
140
|
+
|
|
141
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
142
|
+
|
|
143
|
+
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
|
144
|
+
|
|
145
|
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
|
146
|
+
|
|
147
|
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
|
148
|
+
|
|
149
|
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
|
150
|
+
|
|
151
|
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
152
|
+
|
|
153
|
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
|
154
|
+
|
|
155
|
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
|
156
|
+
|
|
157
|
+
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
|
158
|
+
|
|
159
|
+
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
|
160
|
+
|
|
161
|
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
|
162
|
+
|
|
163
|
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
|
164
|
+
|
|
165
|
+
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
|
166
|
+
|
|
167
|
+
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
|
168
|
+
|
|
169
|
+
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
|
170
|
+
|
|
171
|
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
|
172
|
+
|
|
173
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
174
|
+
|
|
175
|
+
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
|
176
|
+
|
|
177
|
+
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
|
178
|
+
|
|
179
|
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
|
180
|
+
|
|
181
|
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
|
182
|
+
|
|
183
|
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
|
184
|
+
|
|
185
|
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
|
186
|
+
|
|
187
|
+
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
|
188
|
+
|
|
189
|
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
|
190
|
+
|
|
191
|
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
|
192
|
+
|
|
193
|
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
|
194
|
+
|
|
195
|
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
|
196
|
+
|
|
197
|
+
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
|
198
|
+
|
|
21
199
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
22
200
|
|
|
23
201
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
202
|
+
|
|
203
|
+
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
|
204
|
+
|
|
205
|
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
|
206
|
+
|
|
207
|
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
208
|
+
|
|
209
|
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
210
|
+
|
|
211
|
+
"zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
|
212
|
+
|
|
213
|
+
"zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="],
|
|
24
214
|
}
|
|
25
215
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crafters",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"module": "index.ts",
|
|
3
|
+
"version": "0.1.0",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"author": "raillyhugo",
|
|
7
6
|
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"crafters": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "bun run src/index.ts",
|
|
12
|
+
"build": "bun build src/index.ts --compile --outfile crafters"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@vercel/sdk": "^1.0.0",
|
|
16
|
+
"citty": "^0.1.6"
|
|
17
|
+
},
|
|
8
18
|
"devDependencies": {
|
|
9
19
|
"@types/bun": "latest"
|
|
10
20
|
},
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { createSpaceshipClient } from "../lib/spaceship";
|
|
3
|
+
import { createVercelClient } from "../lib/vercel";
|
|
4
|
+
import { loadConfig } from "../lib/config";
|
|
5
|
+
|
|
6
|
+
async function getConfig() {
|
|
7
|
+
const fileConfig = await loadConfig();
|
|
8
|
+
|
|
9
|
+
const apiKey = process.env.SPACESHIP_API_KEY || fileConfig?.spaceship.apiKey;
|
|
10
|
+
const apiSecret = process.env.SPACESHIP_API_SECRET || fileConfig?.spaceship.apiSecret;
|
|
11
|
+
const baseDomain = process.env.BASE_DOMAIN || fileConfig?.baseDomain || "crafter.run";
|
|
12
|
+
const vercelToken = process.env.VERCEL_TOKEN || fileConfig?.vercel.token;
|
|
13
|
+
const vercelTeamId = process.env.VERCEL_TEAM_ID || fileConfig?.vercel.teamId;
|
|
14
|
+
|
|
15
|
+
if (!apiKey || !apiSecret) {
|
|
16
|
+
throw new Error("Missing credentials. Run `crafters login` or set SPACESHIP_API_KEY/SPACESHIP_API_SECRET");
|
|
17
|
+
}
|
|
18
|
+
if (!vercelToken) {
|
|
19
|
+
throw new Error("Missing Vercel token. Run `crafters login` or set VERCEL_TOKEN");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { apiKey, apiSecret, baseDomain, vercelToken, vercelTeamId };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const domainAdd = defineCommand({
|
|
26
|
+
meta: {
|
|
27
|
+
name: "add",
|
|
28
|
+
description: "Add a subdomain to a Vercel project",
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
subdomain: {
|
|
32
|
+
type: "positional",
|
|
33
|
+
description: "Subdomain to add (e.g., 'myapp' for myapp.crafter.run)",
|
|
34
|
+
required: true,
|
|
35
|
+
},
|
|
36
|
+
project: {
|
|
37
|
+
type: "string",
|
|
38
|
+
alias: "p",
|
|
39
|
+
description: "Vercel project slug",
|
|
40
|
+
required: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async run({ args }) {
|
|
44
|
+
const config = await getConfig();
|
|
45
|
+
const fullDomain = `${args.subdomain}.${config.baseDomain}`;
|
|
46
|
+
|
|
47
|
+
console.log(`\n🚀 Adding domain: ${fullDomain}`);
|
|
48
|
+
console.log(` Project: ${args.project}\n`);
|
|
49
|
+
|
|
50
|
+
const spaceship = createSpaceshipClient({
|
|
51
|
+
apiKey: config.apiKey,
|
|
52
|
+
apiSecret: config.apiSecret,
|
|
53
|
+
baseDomain: config.baseDomain,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const vercel = createVercelClient({
|
|
57
|
+
token: config.vercelToken,
|
|
58
|
+
teamId: config.vercelTeamId,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log("1. Adding domain to Vercel project...");
|
|
62
|
+
await vercel.addDomainToProject(args.project, fullDomain);
|
|
63
|
+
console.log(" ✓ Domain added to Vercel");
|
|
64
|
+
|
|
65
|
+
console.log("2. Getting recommended CNAME from Vercel...");
|
|
66
|
+
const recommendedCNAME = await vercel.getRecommendedCNAME(fullDomain);
|
|
67
|
+
console.log(` ✓ Recommended: ${recommendedCNAME}`);
|
|
68
|
+
|
|
69
|
+
console.log("3. Creating CNAME record in Spaceship...");
|
|
70
|
+
await spaceship.addCNAME(args.subdomain, recommendedCNAME);
|
|
71
|
+
console.log(` ✓ CNAME record created → ${recommendedCNAME}`);
|
|
72
|
+
|
|
73
|
+
console.log(`\n✅ Done! ${fullDomain} is now configured.`);
|
|
74
|
+
console.log(" SSL certificate will be issued automatically.\n");
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const domainRemove = defineCommand({
|
|
79
|
+
meta: {
|
|
80
|
+
name: "remove",
|
|
81
|
+
description: "Remove a subdomain from a Vercel project",
|
|
82
|
+
},
|
|
83
|
+
args: {
|
|
84
|
+
subdomain: {
|
|
85
|
+
type: "positional",
|
|
86
|
+
description: "Subdomain to remove",
|
|
87
|
+
required: true,
|
|
88
|
+
},
|
|
89
|
+
project: {
|
|
90
|
+
type: "string",
|
|
91
|
+
alias: "p",
|
|
92
|
+
description: "Vercel project slug",
|
|
93
|
+
required: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
async run({ args }) {
|
|
97
|
+
const config = await getConfig();
|
|
98
|
+
const fullDomain = `${args.subdomain}.${config.baseDomain}`;
|
|
99
|
+
|
|
100
|
+
console.log(`\n🗑️ Removing domain: ${fullDomain}`);
|
|
101
|
+
console.log(` Project: ${args.project}\n`);
|
|
102
|
+
|
|
103
|
+
const spaceship = createSpaceshipClient({
|
|
104
|
+
apiKey: config.apiKey,
|
|
105
|
+
apiSecret: config.apiSecret,
|
|
106
|
+
baseDomain: config.baseDomain,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const vercel = createVercelClient({
|
|
110
|
+
token: config.vercelToken,
|
|
111
|
+
teamId: config.vercelTeamId,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log("1. Removing domain from Vercel project...");
|
|
115
|
+
await vercel.removeDomainFromProject(args.project, fullDomain);
|
|
116
|
+
console.log(" ✓ Domain removed from Vercel");
|
|
117
|
+
|
|
118
|
+
console.log("2. Removing CNAME record from Spaceship...");
|
|
119
|
+
await spaceship.removeCNAME(args.subdomain);
|
|
120
|
+
console.log(" ✓ CNAME record removed");
|
|
121
|
+
|
|
122
|
+
console.log(`\n✅ Done! ${fullDomain} has been removed.\n`);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export const domainList = defineCommand({
|
|
127
|
+
meta: {
|
|
128
|
+
name: "list",
|
|
129
|
+
description: "List all configured subdomains",
|
|
130
|
+
},
|
|
131
|
+
async run() {
|
|
132
|
+
const config = await getConfig();
|
|
133
|
+
|
|
134
|
+
console.log(`\n📋 Listing DNS records for ${config.baseDomain}\n`);
|
|
135
|
+
|
|
136
|
+
const spaceship = createSpaceshipClient({
|
|
137
|
+
apiKey: config.apiKey,
|
|
138
|
+
apiSecret: config.apiSecret,
|
|
139
|
+
baseDomain: config.baseDomain,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const records = await spaceship.listRecords();
|
|
143
|
+
const cnameRecords = records.items.filter((r) => r.type === "CNAME");
|
|
144
|
+
|
|
145
|
+
if (cnameRecords.length === 0) {
|
|
146
|
+
console.log(" No CNAME records found.\n");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("CNAME Records:");
|
|
151
|
+
for (const record of cnameRecords) {
|
|
152
|
+
console.log(` ${record.name}.${config.baseDomain} → ${record.cname}`);
|
|
153
|
+
}
|
|
154
|
+
console.log(`\nTotal: ${cnameRecords.length} record(s)\n`);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const domain = defineCommand({
|
|
159
|
+
meta: {
|
|
160
|
+
name: "domain",
|
|
161
|
+
description: "Manage domains for Vercel projects",
|
|
162
|
+
},
|
|
163
|
+
subCommands: {
|
|
164
|
+
add: domainAdd,
|
|
165
|
+
remove: domainRemove,
|
|
166
|
+
list: domainList,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { saveConfig, loadConfig, getConfigPath } from "../lib/config";
|
|
3
|
+
|
|
4
|
+
export const login = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "login",
|
|
7
|
+
description: "Configure credentials for Spaceship and Vercel",
|
|
8
|
+
},
|
|
9
|
+
args: {
|
|
10
|
+
spaceshipKey: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Spaceship API Key",
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
spaceshipSecret: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Spaceship API Secret",
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
vercelToken: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Vercel Token",
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
vercelTeamId: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Vercel Team ID",
|
|
28
|
+
},
|
|
29
|
+
baseDomain: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Base domain (default: crafter.run)",
|
|
32
|
+
default: "crafter.run",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async run({ args }) {
|
|
36
|
+
await saveConfig({
|
|
37
|
+
spaceship: {
|
|
38
|
+
apiKey: args.spaceshipKey,
|
|
39
|
+
apiSecret: args.spaceshipSecret,
|
|
40
|
+
},
|
|
41
|
+
vercel: {
|
|
42
|
+
token: args.vercelToken,
|
|
43
|
+
teamId: args.vercelTeamId,
|
|
44
|
+
},
|
|
45
|
+
baseDomain: args.baseDomain,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(`\n✅ Credentials saved to ${getConfigPath()}\n`);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const logout = defineCommand({
|
|
53
|
+
meta: {
|
|
54
|
+
name: "logout",
|
|
55
|
+
description: "Remove stored credentials",
|
|
56
|
+
},
|
|
57
|
+
async run() {
|
|
58
|
+
const fs = await import("fs/promises");
|
|
59
|
+
const configPath = getConfigPath();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await fs.unlink(configPath);
|
|
63
|
+
console.log("\n✅ Credentials removed.\n");
|
|
64
|
+
} catch {
|
|
65
|
+
console.log("\n⚠️ No credentials found.\n");
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const whoami = defineCommand({
|
|
71
|
+
meta: {
|
|
72
|
+
name: "whoami",
|
|
73
|
+
description: "Show current configuration",
|
|
74
|
+
},
|
|
75
|
+
async run() {
|
|
76
|
+
const config = await loadConfig();
|
|
77
|
+
|
|
78
|
+
if (!config) {
|
|
79
|
+
console.log("\n⚠️ Not logged in. Run `crafters login` first.\n");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("\n📋 Current Configuration\n");
|
|
84
|
+
console.log(` Base Domain: ${config.baseDomain}`);
|
|
85
|
+
console.log(` Spaceship API Key: ${config.spaceship.apiKey.slice(0, 8)}...`);
|
|
86
|
+
console.log(` Vercel Token: ${config.vercel.token.slice(0, 8)}...`);
|
|
87
|
+
if (config.vercel.teamId) {
|
|
88
|
+
console.log(` Vercel Team ID: ${config.vercel.teamId}`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\n Config file: ${getConfigPath()}\n`);
|
|
91
|
+
},
|
|
92
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { domain } from "./commands/domain";
|
|
4
|
+
import { login, logout, whoami } from "./commands/login";
|
|
5
|
+
|
|
6
|
+
const main = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "crafters",
|
|
9
|
+
version: "0.0.1",
|
|
10
|
+
description: "Crafter Station CLI - Domain management for Vercel projects",
|
|
11
|
+
},
|
|
12
|
+
subCommands: {
|
|
13
|
+
domain,
|
|
14
|
+
login,
|
|
15
|
+
logout,
|
|
16
|
+
whoami,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
runMain(main);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".crafters");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
|
|
7
|
+
export interface CraftersConfig {
|
|
8
|
+
spaceship: {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
apiSecret: string;
|
|
11
|
+
};
|
|
12
|
+
vercel: {
|
|
13
|
+
token: string;
|
|
14
|
+
teamId?: string;
|
|
15
|
+
};
|
|
16
|
+
baseDomain: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function saveConfig(config: CraftersConfig): Promise<void> {
|
|
20
|
+
const fs = await import("fs/promises");
|
|
21
|
+
|
|
22
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
23
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
|
+
await fs.chmod(CONFIG_FILE, 0o600);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadConfig(): Promise<CraftersConfig | null> {
|
|
28
|
+
const fs = await import("fs/promises");
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
32
|
+
return JSON.parse(data) as CraftersConfig;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function configExists(): Promise<boolean> {
|
|
39
|
+
const fs = await import("fs/promises");
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(CONFIG_FILE);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getConfigPath(): string {
|
|
50
|
+
return CONFIG_FILE;
|
|
51
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const SPACESHIP_API_URL = "https://spaceship.dev/api/v1";
|
|
2
|
+
|
|
3
|
+
interface SpaceshipConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiSecret: string;
|
|
6
|
+
baseDomain: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createSpaceshipClient(config: SpaceshipConfig) {
|
|
10
|
+
const headers = {
|
|
11
|
+
"X-Api-Key": config.apiKey,
|
|
12
|
+
"X-Api-Secret": config.apiSecret,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function addCNAME(subdomain: string, target = "cname.vercel-dns.com") {
|
|
17
|
+
const response = await fetch(
|
|
18
|
+
`${SPACESHIP_API_URL}/dns/records/${config.baseDomain}`,
|
|
19
|
+
{
|
|
20
|
+
method: "PUT",
|
|
21
|
+
headers,
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
force: true,
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
type: "CNAME",
|
|
27
|
+
name: subdomain,
|
|
28
|
+
cname: target,
|
|
29
|
+
ttl: 3600,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const error = await response.json().catch(() => ({}));
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Spaceship API error: ${response.status} - ${JSON.stringify(error)}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { success: true, subdomain, target };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function removeCNAME(subdomain: string) {
|
|
47
|
+
const response = await fetch(
|
|
48
|
+
`${SPACESHIP_API_URL}/dns/records/${config.baseDomain}`,
|
|
49
|
+
{
|
|
50
|
+
method: "DELETE",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify([
|
|
53
|
+
{
|
|
54
|
+
type: "CNAME",
|
|
55
|
+
name: subdomain,
|
|
56
|
+
cname: "cname.vercel-dns.com",
|
|
57
|
+
},
|
|
58
|
+
]),
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = await response.json().catch(() => ({}));
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Spaceship API error: ${response.status} - ${JSON.stringify(error)}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { success: true, subdomain };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function listRecords() {
|
|
73
|
+
const response = await fetch(
|
|
74
|
+
`${SPACESHIP_API_URL}/dns/records/${config.baseDomain}?take=100&skip=0`,
|
|
75
|
+
{
|
|
76
|
+
method: "GET",
|
|
77
|
+
headers,
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.json().catch(() => ({}));
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Spaceship API error: ${response.status} - ${JSON.stringify(error)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response.json() as Promise<{
|
|
89
|
+
items: Array<{ type: string; name: string; cname?: string; ttl: number }>;
|
|
90
|
+
total: number;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
addCNAME,
|
|
96
|
+
removeCNAME,
|
|
97
|
+
listRecords,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type SpaceshipClient = ReturnType<typeof createSpaceshipClient>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Vercel } from "@vercel/sdk";
|
|
2
|
+
|
|
3
|
+
interface VercelConfig {
|
|
4
|
+
token: string;
|
|
5
|
+
teamId?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createVercelClient(config: VercelConfig) {
|
|
9
|
+
const client = new Vercel({
|
|
10
|
+
bearerToken: config.token,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function addDomainToProject(projectSlug: string, domain: string) {
|
|
14
|
+
const result = await client.projects.addProjectDomain({
|
|
15
|
+
idOrName: projectSlug,
|
|
16
|
+
teamId: config.teamId,
|
|
17
|
+
requestBody: {
|
|
18
|
+
name: domain,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function removeDomainFromProject(projectSlug: string, domain: string) {
|
|
26
|
+
const result = await client.projects.removeProjectDomain({
|
|
27
|
+
idOrName: projectSlug,
|
|
28
|
+
domain,
|
|
29
|
+
teamId: config.teamId,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getDomainConfig(projectSlug: string, domain: string) {
|
|
36
|
+
const result = await client.projects.getProjectDomain({
|
|
37
|
+
idOrName: projectSlug,
|
|
38
|
+
domain,
|
|
39
|
+
teamId: config.teamId,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getRecommendedCNAME(domain: string): Promise<string> {
|
|
46
|
+
const result = await client.domains.getDomainConfig({
|
|
47
|
+
domain,
|
|
48
|
+
teamId: config.teamId,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const recommended = result.recommendedCNAME?.find((c) => c.rank === 1);
|
|
52
|
+
return recommended?.value?.replace(/\.$/, "") || "cname.vercel-dns.com";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function verifyDomain(projectSlug: string, domain: string) {
|
|
56
|
+
const result = await client.projects.verifyProjectDomain({
|
|
57
|
+
idOrName: projectSlug,
|
|
58
|
+
domain,
|
|
59
|
+
teamId: config.teamId,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function listProjectDomains(projectSlug: string) {
|
|
66
|
+
const project = await client.projects.getProject({
|
|
67
|
+
idOrName: projectSlug,
|
|
68
|
+
teamId: config.teamId,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return project;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
addDomainToProject,
|
|
76
|
+
removeDomainFromProject,
|
|
77
|
+
getDomainConfig,
|
|
78
|
+
getRecommendedCNAME,
|
|
79
|
+
verifyDomain,
|
|
80
|
+
listProjectDomains,
|
|
81
|
+
client,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type VercelClient = ReturnType<typeof createVercelClient>;
|