crafters 0.0.1 → 0.2.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 ADDED
@@ -0,0 +1,44 @@
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.2.0] - 2025-01-10
9
+
10
+ ### Added
11
+
12
+ - **Claude Code Integration**
13
+ - `crafters claude install` - Install commands and agents from claude-dx repo
14
+ - `crafters claude update` - Update all commands and agents (overwrites existing)
15
+
16
+ - **Features**
17
+ - Clones/updates `crafter-station/claude-dx` to local machine
18
+ - Copies commands to `~/.claude/commands/`
19
+ - Copies agents to `~/.claude/agents/`
20
+ - Intelligent merge of `settings.json` (preserves existing config, adds new permissions)
21
+ - Skip existing commands by default, use `--force` or `update` to overwrite
22
+
23
+ ## [0.1.0] - 2024-12-20
24
+
25
+ ### Added
26
+
27
+ - **CLI Commands**
28
+ - `crafters domain add <subdomain> -p <project>` - Add subdomain to Vercel project
29
+ - `crafters domain remove <subdomain> -p <project>` - Remove subdomain from Vercel project
30
+ - `crafters domain list` - List all CNAME records
31
+ - `crafters login` - Configure credentials
32
+ - `crafters logout` - Remove stored credentials
33
+ - `crafters whoami` - Show current configuration
34
+
35
+ - **Features**
36
+ - Automatic CNAME creation in Spaceship DNS
37
+ - Uses Vercel's recommended CNAME (project-specific) instead of generic `cname.vercel-dns.com`
38
+ - Credentials stored securely in `~/.crafters/config.json`
39
+ - Support for environment variables as fallback
40
+
41
+ ### Dependencies
42
+
43
+ - `@vercel/sdk` - Vercel API integration
44
+ - `citty` - Lightweight CLI framework
package/CLAUDE.md ADDED
@@ -0,0 +1,42 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Run the CLI in development
9
+ bun run src/index.ts
10
+
11
+ # Build standalone executable
12
+ bun build src/index.ts --compile --outfile crafters
13
+
14
+ # Run with specific command
15
+ bun run src/index.ts claude install
16
+ bun run src/index.ts claude update
17
+ bun run src/index.ts domain add myapp -p my-vercel-project
18
+ bun run src/index.ts login --spaceshipKey="..." --spaceshipSecret="..." --vercelToken="..."
19
+ ```
20
+
21
+ ## Architecture
22
+
23
+ This is a CLI tool (`crafters`) for managing domains on Vercel projects with Spaceship DNS. Built with Bun and the `citty` CLI framework.
24
+
25
+ ### Structure
26
+
27
+ - `src/index.ts` - Entry point, defines main command and subcommands using citty's `defineCommand`
28
+ - `src/commands/` - Command implementations
29
+ - `claude.ts` - `claude install/update` for syncing Claude Code config from claude-dx
30
+ - `domain.ts` - `domain add/remove/list` commands (core functionality)
31
+ - `login.ts` - `login/logout/whoami` credential management
32
+ - `src/lib/` - API clients and utilities
33
+ - `spaceship.ts` - Spaceship DNS API client (CNAME management)
34
+ - `vercel.ts` - Vercel SDK wrapper for project domain operations
35
+ - `config.ts` - Config file management (`~/.crafters/config.json`)
36
+
37
+ ### Key Patterns
38
+
39
+ - Commands use citty's `defineCommand` with `meta`, `args`, and `run` properties
40
+ - API clients are factory functions (`createSpaceshipClient`, `createVercelClient`) returning method objects
41
+ - Configuration supports both file-based (`~/.crafters/config.json`) and environment variable fallbacks
42
+ - Domain operations coordinate both Vercel (add domain to project) and Spaceship (create CNAME record)
package/README.md CHANGED
@@ -1,15 +1,70 @@
1
- # cs
1
+ # crafters
2
2
 
3
- To install dependencies:
3
+ Crafter Station CLI for domain management and Claude Code configuration.
4
+
5
+ ## Install
4
6
 
5
7
  ```bash
6
- bun install
8
+ bun install -g crafters
7
9
  ```
8
10
 
9
- To run:
11
+ ## Commands
12
+
13
+ ### Claude Code
14
+
15
+ Sync Claude Code commands and agents from `crafter-station/claude-dx`.
10
16
 
11
17
  ```bash
12
- bun run index.ts
18
+ # First-time install (skips existing commands)
19
+ crafters claude install
20
+
21
+ # Update all commands (overwrites existing)
22
+ crafters claude update
23
+
24
+ # Force overwrite on install
25
+ crafters claude install --force
13
26
  ```
14
27
 
15
- This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
28
+ **What it does:**
29
+ - Clones/pulls `claude-dx` to `~/Programming/crafter-station/`
30
+ - Copies commands to `~/.claude/commands/`
31
+ - Copies agents to `~/.claude/agents/`
32
+ - Merges `settings.json` without overwriting existing config
33
+
34
+ ### Domain Management
35
+
36
+ Manage Vercel project domains with Spaceship DNS.
37
+
38
+ ```bash
39
+ # Setup credentials
40
+ crafters login \
41
+ --spaceshipKey="YOUR_KEY" \
42
+ --spaceshipSecret="YOUR_SECRET" \
43
+ --vercelToken="YOUR_TOKEN" \
44
+ --vercelTeamId="YOUR_TEAM_ID"
45
+
46
+ # Add subdomain to project
47
+ crafters domain add myapp -p my-vercel-project
48
+
49
+ # Remove subdomain
50
+ crafters domain remove myapp -p my-vercel-project
51
+
52
+ # List all CNAME records
53
+ crafters domain list
54
+
55
+ # Check current config
56
+ crafters whoami
57
+
58
+ # Clear credentials
59
+ crafters logout
60
+ ```
61
+
62
+ ## Requirements
63
+
64
+ - [Bun](https://bun.sh)
65
+ - [GitHub CLI](https://cli.github.com) (for claude commands)
66
+ - Access to `crafter-station/claude-dx` repo
67
+
68
+ ## License
69
+
70
+ 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,21 @@
1
1
  {
2
2
  "name": "crafters",
3
- "version": "0.0.1",
4
- "module": "index.ts",
3
+ "version": "0.2.0",
4
+ "description": "Crafter Station CLI - Domain management and Claude Code configuration",
5
5
  "type": "module",
6
6
  "author": "raillyhugo",
7
7
  "license": "MIT",
8
+ "bin": {
9
+ "crafters": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "dev": "bun run src/index.ts",
13
+ "build": "bun build src/index.ts --compile --outfile crafters"
14
+ },
15
+ "dependencies": {
16
+ "@vercel/sdk": "^1.0.0",
17
+ "citty": "^0.1.6"
18
+ },
8
19
  "devDependencies": {
9
20
  "@types/bun": "latest"
10
21
  },
@@ -0,0 +1,184 @@
1
+ import { defineCommand } from "citty";
2
+ import { execSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ readdir,
7
+ copyFile,
8
+ mkdir,
9
+ readFile,
10
+ writeFile,
11
+ stat,
12
+ cp,
13
+ } from "node:fs/promises";
14
+
15
+ const CLAUDE_DX_REPO = "crafter-station/claude-dx";
16
+ const CLAUDE_DX_PATH = join(homedir(), "Programming/crafter-station/claude-dx");
17
+ const CLAUDE_CONFIG_PATH = join(homedir(), ".claude");
18
+
19
+ async function exists(path: string): Promise<boolean> {
20
+ try {
21
+ await stat(path);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ async function cloneOrPullRepo(): Promise<void> {
29
+ if (await exists(CLAUDE_DX_PATH)) {
30
+ console.log("Updating claude-dx...");
31
+ execSync("git pull", { cwd: CLAUDE_DX_PATH, stdio: "inherit" });
32
+ } else {
33
+ console.log("Cloning claude-dx...");
34
+ execSync(`gh repo clone ${CLAUDE_DX_REPO} ${CLAUDE_DX_PATH}`, {
35
+ stdio: "inherit",
36
+ });
37
+ }
38
+ }
39
+
40
+ async function copyCommands(
41
+ force: boolean,
42
+ ): Promise<{ copied: number; skipped: number }> {
43
+ const srcDir = join(CLAUDE_DX_PATH, ".claude/commands");
44
+ const destDir = join(CLAUDE_CONFIG_PATH, "commands");
45
+
46
+ await mkdir(destDir, { recursive: true });
47
+
48
+ const entries = await readdir(srcDir, { withFileTypes: true });
49
+ let copied = 0;
50
+ let skipped = 0;
51
+
52
+ for (const entry of entries) {
53
+ const srcPath = join(srcDir, entry.name);
54
+ const destPath = join(destDir, entry.name);
55
+
56
+ if ((await exists(destPath)) && !force) {
57
+ console.log(` Skip: ${entry.name} (exists)`);
58
+ skipped++;
59
+ continue;
60
+ }
61
+
62
+ if (entry.isDirectory()) {
63
+ await cp(srcPath, destPath, { recursive: true });
64
+ } else {
65
+ await copyFile(srcPath, destPath);
66
+ }
67
+ console.log(` Copy: ${entry.name}`);
68
+ copied++;
69
+ }
70
+
71
+ return { copied, skipped };
72
+ }
73
+
74
+ async function copyAgents(): Promise<number> {
75
+ const srcDir = join(CLAUDE_DX_PATH, ".claude/agents");
76
+ const destDir = join(CLAUDE_CONFIG_PATH, "agents");
77
+
78
+ if (!(await exists(srcDir))) return 0;
79
+
80
+ await mkdir(destDir, { recursive: true });
81
+
82
+ const files = await readdir(srcDir);
83
+ for (const file of files) {
84
+ await copyFile(join(srcDir, file), join(destDir, file));
85
+ console.log(` Copy: ${file}`);
86
+ }
87
+ return files.length;
88
+ }
89
+
90
+ async function mergeSettings(): Promise<void> {
91
+ const srcPath = join(CLAUDE_DX_PATH, ".claude/settings.json");
92
+ const destPath = join(CLAUDE_CONFIG_PATH, "settings.json");
93
+
94
+ if (!(await exists(srcPath))) return;
95
+
96
+ const srcSettings = JSON.parse(await readFile(srcPath, "utf-8"));
97
+ let destSettings: Record<string, unknown> = {};
98
+
99
+ if (await exists(destPath)) {
100
+ destSettings = JSON.parse(await readFile(destPath, "utf-8"));
101
+ }
102
+
103
+ const merged: Record<string, unknown> = { ...destSettings };
104
+
105
+ for (const [key, value] of Object.entries(srcSettings)) {
106
+ if (key === "permissions" && typeof value === "object" && value !== null) {
107
+ merged.permissions = merged.permissions || {};
108
+ const srcPerms = value as Record<string, string[]>;
109
+ const destPerms = (merged.permissions || {}) as Record<string, string[]>;
110
+
111
+ for (const [permKey, permValue] of Object.entries(srcPerms)) {
112
+ const existing = destPerms[permKey] || [];
113
+ (merged.permissions as Record<string, string[]>)[permKey] = [
114
+ ...new Set([...existing, ...permValue]),
115
+ ];
116
+ }
117
+ } else if (!(key in merged)) {
118
+ merged[key] = value;
119
+ }
120
+ }
121
+
122
+ await writeFile(destPath, JSON.stringify(merged, null, 2));
123
+ console.log(" Merged settings.json");
124
+ }
125
+
126
+ async function runInstall(force: boolean): Promise<void> {
127
+ console.log("\n1. Syncing claude-dx repo...");
128
+ await cloneOrPullRepo();
129
+
130
+ console.log("\n2. Installing commands...");
131
+ const { copied, skipped } = await copyCommands(force);
132
+
133
+ console.log("\n3. Installing agents...");
134
+ const agentCount = await copyAgents();
135
+
136
+ console.log("\n4. Merging settings...");
137
+ await mergeSettings();
138
+
139
+ console.log(
140
+ `\nDone! ${copied} commands installed, ${skipped} skipped, ${agentCount} agents installed.`,
141
+ );
142
+ if (skipped > 0 && !force) {
143
+ console.log("Use 'crafters claude update' to overwrite existing commands.\n");
144
+ }
145
+ }
146
+
147
+ export const claudeInstall = defineCommand({
148
+ meta: {
149
+ name: "install",
150
+ description: "Install Claude Code commands and agents from claude-dx",
151
+ },
152
+ args: {
153
+ force: {
154
+ type: "boolean",
155
+ alias: "f",
156
+ description: "Overwrite existing commands",
157
+ default: false,
158
+ },
159
+ },
160
+ async run({ args }) {
161
+ await runInstall(args.force);
162
+ },
163
+ });
164
+
165
+ export const claudeUpdate = defineCommand({
166
+ meta: {
167
+ name: "update",
168
+ description: "Update Claude Code commands and agents from claude-dx",
169
+ },
170
+ async run() {
171
+ await runInstall(true);
172
+ },
173
+ });
174
+
175
+ export const claude = defineCommand({
176
+ meta: {
177
+ name: "claude",
178
+ description: "Manage Claude Code configuration",
179
+ },
180
+ subCommands: {
181
+ install: claudeInstall,
182
+ update: claudeUpdate,
183
+ },
184
+ });
@@ -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,22 @@
1
+ #!/usr/bin/env bun
2
+ import { defineCommand, runMain } from "citty";
3
+ import { claude } from "./commands/claude";
4
+ import { domain } from "./commands/domain";
5
+ import { login, logout, whoami } from "./commands/login";
6
+
7
+ const main = defineCommand({
8
+ meta: {
9
+ name: "crafters",
10
+ version: "0.2.0",
11
+ description: "Crafter Station CLI - Domain management and Claude Code configuration",
12
+ },
13
+ subCommands: {
14
+ claude,
15
+ domain,
16
+ login,
17
+ logout,
18
+ whoami,
19
+ },
20
+ });
21
+
22
+ 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>;